Merge upstream main into resolve-local-deps

Keep the SOURCE_DATE_EPOCH argument on tarFromLocalDir together with
resolver.IsLocalDependency. Restore the strings import used by the
circular-dependency test.

Signed-off-by: Alexandr Danilin <alexandr.danilin@ooma.com>
pull/31524/head
Alexandr Danilin 7 days ago
commit 573f2d42d6

@ -37,3 +37,17 @@ updates:
directory: "/"
schedule:
interval: "daily"
groups:
github-actions:
patterns:
- "*"
- # Keep dev-v3 GitHub Actions up to date, while Helm v3 is within support
package-ecosystem: "github-actions"
target-branch: "dev-v3"
directory: "/"
schedule:
interval: "daily"
groups:
github-actions:
patterns:
- "*"

4
.github/env vendored

@ -1,2 +1,2 @@
GOLANG_VERSION=1.25
GOLANGCI_LINT_VERSION=v2.5.0
GOLANG_VERSION=1.27
GOLANGCI_LINT_VERSION=v2.13.1

@ -0,0 +1,2 @@
"v4.x":
- base-branch: "main"

@ -18,11 +18,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # pin@v7.0.1
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # pin@7.0.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # pin@v7.0.1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # pinv4.30.7
uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # pinv4.38.1
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@1c5b675653bb5c22dbe9b12b556ec555138e09fd # pinv4.38.1
# ℹ️ 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@1c5b675653bb5c22dbe9b12b556ec555138e09fd # pinv4.38.1

@ -13,15 +13,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # pin@v7.0.1
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # pin@7.0.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true
- name: golangci-lint
uses: golangci/golangci-lint-action@0a35821d5c230e903fcfe077583637dea1b27b47 #pin@9.0.0
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a #pin@9.3.0
with:
version: ${{ env.GOLANGCI_LINT_VERSION }}

@ -3,6 +3,11 @@ on:
push:
paths:
- go.sum
- .github/workflows/govulncheck.yml
pull_request:
paths:
- go.sum
- .github/workflows/govulncheck.yml
schedule:
- cron: "0 0 * * *"
@ -14,15 +19,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # pin@v7.0.1
with:
persist-credentials: false
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # pin@7.0.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true
- name: govulncheck
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # pin@1.0.4
uses: golang/govulncheck-action@032d45514ae346b1db93c04b0c90b841c370344f # pin@v1.1.0
with:
go-package: ./...

@ -0,0 +1,16 @@
name: Label PRs
on:
pull_request_target:
branches: [main]
types: [opened, reopened, edited, synchronize]
permissions: {}
jobs:
label:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # pin@v7.0.0
with:
sync-labels: true

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest-16-cores
steps:
- name: Checkout source code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # pin@v7.0.1
with:
fetch-depth: 0
@ -28,9 +28,10 @@ jobs:
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # pin@7.0.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true
- name: Run unit tests
run: make test-coverage
- name: Build Helm Binaries
@ -84,13 +85,15 @@ jobs:
if: github.ref == 'refs/heads/main' && github.repository == 'helm/helm'
steps:
- name: Checkout source code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # pin@v7.0.1
with:
fetch-depth: 0
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # pin@7.0.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true

@ -28,12 +28,12 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
with:
results_file: results.sarif
results_format: sarif
@ -55,7 +55,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@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@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
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@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.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.'

9
.gitignore vendored

@ -10,7 +10,14 @@ _dist/
_dist_versions/
bin/
vendor/
.pre-commit-config.yaml
# Ignores charts pulled for dependency build tests
cmd/helm/testdata/testcharts/issue-7233/charts/*
pkg/cmd/testdata/testcharts/issue-7233/charts/*
.pre-commit-config.yaml
# AI Vendor specific paths
CLAUDE.md
.claude/
.codex/
.cursor/

@ -1,6 +1,7 @@
formatters:
enable:
- gofmt
- gofumpt
- goimports
exclusions:
@ -10,6 +11,11 @@ formatters:
gofmt:
simplify: true
gofumpt:
module-path: helm.sh/helm/v4
extra:
group-params: true
goimports:
local-prefixes:
- helm.sh/helm/v4
@ -17,23 +23,37 @@ formatters:
linters:
default: none
# Keep sorted alphabetically
enable:
- bidichk
- contextcheck
- depguard
- dupl
- gomodguard
- errorlint
- exhaustive
- fatcontext
- gocritic
- gomodguard_v2
- govet
- ineffassign
- misspell
- modernize
- nakedret
- noctx
- nolintlint
- perfsprint
- revive
- sloglint
- staticcheck
- testifylint
- thelper
- unused
- usestdlibvars
- usetesting
- whitespace
exclusions:
generated: lax
presets:
@ -42,16 +62,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:
@ -65,12 +78,74 @@ linters:
dupl:
threshold: 400
gomodguard:
errorlint:
# Check for plain type assertions and type switches.
asserts: true
# Check for plain error comparisons.
comparison: true
exhaustive:
default-signifies-exhaustive: true
gocritic:
disabled-checks:
- appendAssign
- badCall
- commentedOutCode
- deferInLoop
- exposedSyncMutex
- hugeParam
- importShadow
- ptrToRefParam
- rangeValCopy
- todoCommentWithoutDetail
- tooManyResultsChecker
- uncheckedInlineErr
- unnamedResult
- whyNoLint
enable-all: true
gomodguard_v2:
blocked:
modules:
- github.com/evanphx/json-patch:
recommendations:
- github.com/evanphx/json-patch/v5
- module: github.com/evanphx/json-patch
recommendations:
- github.com/evanphx/json-patch/v5
nolintlint:
require-specific: true
revive:
enable-all-rules: false
enable-default-rules: true
max-open-files: 2048
rules:
- name: bare-return
- name: early-return
arguments:
- "preserve-scope"
- name: if-return
- name: indent-error-flow
arguments:
- "preserve-scope"
- name: superfluous-else
arguments:
- "preserve-scope"
- 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:
# 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:'

@ -12,7 +12,7 @@
- [New Relic](https://www.newrelic.com)
- [Octopus Deploy](https://octopus.com/)
- [Omnistrate](https://omnistrate.com)
- [Oracle](www.oracle.com)
- [Oracle](https://www.oracle.com)
- [Percona](https://www.percona.com)
- [Qovery](https://www.qovery.com/)
- [Samsung SDS](https://www.samsungsds.com/)

@ -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
@ -11,9 +11,13 @@ vulnerability_, please email a report to
[cncf-helm-security@lists.cncf.io](mailto:cncf-helm-security@lists.cncf.io). This will give us a
chance to try to fix the issue before it is exploited in the wild.
## Helm v3 and v4
## Helm v3
Helm v4 is currently under development on the `main` branch. During the development of Helm v4 and for some time after its released, Helm v3 will continue to be supported and developed on the `dev-v3` branch. Helm v3 will continue to get bug fixes and updates for new Kubernetes releases. Helm v4 is where new features and major changes will happen. For features to be backported to Helm v3, an exception will be needed. Bugs should first be fixed on Helm v4 and then backported to Helm v3.
Helm v4 development takes place on the `main` branch while Helm v3 is on the `dev-v3` branch.
Helm v3 will continue to receive bug fixes and updates for new Kubernetes releases until July 8th 2026. Security enhancements will still be applied until November 11th 2026. See the blog <https://helm.sh/blog/helm-4-released#helm-v3-support> for more details.
Bugs should first be fixed on Helm v4 and then backported to Helm v3. Helm v3 (and the `dev-v3` branch) is no longer accepting new features.
## Sign Your Work
@ -106,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
@ -133,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:
@ -158,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.

20
KEYS

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

@ -1,7 +1,6 @@
BINDIR := $(CURDIR)/bin
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)
@ -58,20 +57,6 @@ LDFLAGS += -X helm.sh/helm/v4/internal/version.gitCommit=${GIT_COMMIT}
LDFLAGS += -X helm.sh/helm/v4/internal/version.gitTreeState=${GIT_DIRTY}
LDFLAGS += $(EXT_LDFLAGS)
# Define constants based on the client-go version
K8S_MODULES_VER=$(subst ., ,$(subst v,,$(shell go list -f '{{.Version}}' -m k8s.io/client-go)))
K8S_MODULES_MAJOR_VER=$(shell echo $$(($(firstword $(K8S_MODULES_VER)) + 1)))
K8S_MODULES_MINOR_VER=$(word 2,$(K8S_MODULES_VER))
LDFLAGS += -X helm.sh/helm/v4/pkg/chart/v2/lint/rules.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/chart/v2/lint/rules.k8sVersionMinor=$(K8S_MODULES_MINOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/internal/v3/lint/rules.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/internal/v3/lint/rules.k8sVersionMinor=$(K8S_MODULES_MINOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/chart/common.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/chart/common.k8sVersionMinor=$(K8S_MODULES_MINOR_VER)
LDFLAGS += -X helm.sh/helm/v4/internal/version.kubeClientVersionMajor=$(K8S_MODULES_MAJOR_VER)
LDFLAGS += -X helm.sh/helm/v4/internal/version.kubeClientVersionMinor=$(K8S_MODULES_MINOR_VER)
.PHONY: all
all: build
@ -129,6 +114,13 @@ test-coverage:
.PHONY: test-style
test-style:
@EXPECTED_VERSION=$$(grep GOLANGCI_LINT_VERSION .github/env | cut -d= -f2); \
ACTUAL_VERSION=$$(golangci-lint --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1); \
if [ "v$$ACTUAL_VERSION" != "$$EXPECTED_VERSION" ]; then \
echo "Warning: golangci-lint version is v$$ACTUAL_VERSION (expected $$EXPECTED_VERSION from CI)"; \
echo "To install the correct version, run:"; \
echo " curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b \$$(go env GOPATH)/bin $$EXPECTED_VERSION"; \
fi
golangci-lint run ./...
@scripts/validate-license.sh
@ -137,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; \
@ -169,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)
@ -180,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:
@ -209,11 +200,10 @@ sign:
# The contents of the .sha256sum file are compatible with tools like
# shasum. For example, using the following command will verify
# the file helm-3.1.0-rc.1-darwin-amd64.tar.gz:
# shasum -a 256 -c helm-3.1.0-rc.1-darwin-amd64.tar.gz.sha256sum
# the file helm-4.0.0-darwin-amd64.tar.gz:
# shasum -a 256 -c helm-4.0.0-darwin-amd64.tar.gz.sha256sum
# The .sha256 files hold only the hash and are not compatible with
# verification tools like shasum or sha256sum. This method and file can be
# removed in Helm v4.
# verification tools like shasum or sha256sum.
.PHONY: checksum
checksum:
for f in $$(ls _dist/*.{gz,zip} 2>/dev/null) ; do \

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

@ -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
@ -49,6 +49,7 @@ If you want to use a package manager:
- [Scoop](https://scoop.sh/) users can use `scoop install helm`.
- [Snapcraft](https://snapcraft.io/) users can use `snap install helm --classic`.
- [Flox](https://flox.dev) users can use `flox install kubernetes-helm`.
- [Mise-en-place](https://mise.jdx.dev/) users can use `mise use -g helm@latest`
To rapidly get Helm up and running, start with the [Quick Start Guide](https://helm.sh/docs/intro/quickstart/).
@ -63,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

@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package main // import "helm.sh/helm/v4/cmd/helm"
package main
import (
"errors"
"log/slog"
"os"
@ -41,7 +42,7 @@ func main() {
}
if err := cmd.Execute(); err != nil {
if cerr, ok := err.(helmcmd.CommandError); ok {
if cerr, ok := errors.AsType[helmcmd.CommandError](err); ok {
os.Exit(cerr.ExitCode)
}
os.Exit(1)

@ -24,6 +24,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCliPluginExitCode(t *testing.T) {
@ -45,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",
@ -60,20 +61,14 @@ func TestCliPluginExitCode(t *testing.T) {
cmd.Stderr = stderr
err := cmd.Run()
exiterr, ok := err.(*exec.ExitError)
if !ok {
t.Fatalf("Unexpected error type returned by os.Exit: %T", err)
}
exiterr := &exec.ExitError{}
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())
}
}

216
go.mod

@ -1,55 +1,54 @@
module helm.sh/helm/v4
go 1.24.0
go 1.26.0
require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24
github.com/BurntSushi/toml v1.5.0
github.com/BurntSushi/toml v1.6.0
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/Masterminds/semver/v3 v3.4.0
github.com/Masterminds/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.0
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.36.0-flux.14
github.com/foxcpp/go-mockdns v1.1.0
github.com/gobwas/glob v0.2.3
github.com/gofrs/flock v0.13.0
github.com/fatih/color v1.19.0
github.com/fluxcd/cli-utils v1.3.0
github.com/foxcpp/go-mockdns v1.2.0
github.com/gobwas/glob v1.0.0
github.com/gofrs/flock v0.13.1
github.com/gosuri/uitable v0.0.4
github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.10.9
github.com/mattn/go-shellwords v1.0.12
github.com/lib/pq v1.12.3
github.com/mattn/go-shellwords v1.0.15
github.com/moby/term v0.5.2
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
github.com/rubenv/sql-migrate v1.8.0
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
github.com/spf13/cobra v1.10.1
github.com/rubenv/sql-migrate v1.8.1
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/tetratelabs/wazero v1.10.1
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.44.0
golang.org/x/term v0.37.0
golang.org/x/text v0.31.0
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.34.2
k8s.io/apiextensions-apiserver v0.34.2
k8s.io/apimachinery v0.34.2
k8s.io/apiserver v0.34.2
k8s.io/cli-runtime v0.34.2
k8s.io/client-go v0.34.2
k8s.io/klog/v2 v2.130.1
k8s.io/kubectl v0.34.2
oras.land/oras-go/v2 v2.6.0
sigs.k8s.io/controller-runtime v0.22.4
sigs.k8s.io/kustomize/kyaml v0.21.0
github.com/stretchr/testify v1.12.1
github.com/tetratelabs/wazero v1.12.0
go.yaml.in/yaml/v3 v3.0.5
golang.org/x/crypto v0.57.0
golang.org/x/term v0.46.0
golang.org/x/text v0.42.0
k8s.io/api v0.37.0
k8s.io/apiextensions-apiserver v0.37.0
k8s.io/apimachinery v0.37.0
k8s.io/apiserver v0.37.0
k8s.io/cli-runtime v0.37.0
k8s.io/client-go v0.37.0
k8s.io/klog/v2 v2.140.0
k8s.io/kubectl v0.37.0
oras.land/oras-go/v2 v2.6.2
sigs.k8s.io/controller-runtime v0.25.1
sigs.k8s.io/kustomize/kyaml v0.21.1
sigs.k8s.io/yaml v1.6.0
)
@ -60,127 +59,132 @@ 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
github.com/fxamacker/cbor/v2 v2.9.3 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/go-openapi/jsonpointer v1.0.0 // indirect
github.com/go-openapi/jsonreference v1.0.1 // indirect
github.com/go-openapi/swag v0.29.1 // indirect
github.com/go-openapi/swag/cmdutils v0.29.1 // indirect
github.com/go-openapi/swag/conv v0.29.1 // indirect
github.com/go-openapi/swag/fileutils v0.29.1 // indirect
github.com/go-openapi/swag/jsonutils v0.29.1 // indirect
github.com/go-openapi/swag/loading v0.29.1 // indirect
github.com/go-openapi/swag/mangling v0.29.1 // indirect
github.com/go-openapi/swag/netutils v0.29.1 // indirect
github.com/go-openapi/swag/pools v0.29.1 // indirect
github.com/go-openapi/swag/stringutils v0.29.1 // indirect
github.com/go-openapi/swag/typeutils v0.29.1 // indirect
github.com/go-openapi/swag/yamlutils v0.29.1 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/gnostic-models v0.7.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/handlers v1.5.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.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
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/compress v1.19.1 // 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
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/spdystream v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/onsi/gomega v1.37.0 // indirect
github.com/onsi/gomega v1.42.1 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/prometheus/client_golang v1.24.1 // indirect
github.com/prometheus/client_model v0.6.3 // indirect
github.com/prometheus/common v0.71.0 // indirect
github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/prometheus/procfs v0.22.0 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 // indirect
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.58.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.34.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.8.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.38.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/grpc v1.72.1 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
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.69.0 // indirect
go.opentelemetry.io/otel v1.45.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.21.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.45.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.64.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.21.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.21.0 // indirect
go.opentelemetry.io/otel/metric v1.45.0 // indirect
go.opentelemetry.io/otel/sdk v1.45.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.21.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect
go.opentelemetry.io/otel/trace v1.45.0 // indirect
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/mod v0.41.0 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.23.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.49.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect
google.golang.org/grpc v1.83.2 // indirect
google.golang.org/protobuf v1.36.12 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/component-base v0.34.2 // indirect
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/kustomize/api v0.20.1 // indirect
k8s.io/component-base v0.37.0 // indirect
k8s.io/kube-openapi v0.0.0-20260821135717-be32def86098 // indirect
k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // 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.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect
)

486
go.sum

@ -6,28 +6,28 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
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/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/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=
@ -36,104 +36,131 @@ 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.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is=
github.com/cyphar/filepath-securejoin v0.6.0/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.36.0-flux.14 h1:I//AMVUXTc+M04UtIXArMXQZCazGMwfemodV1j/yG8c=
github.com/fluxcd/cli-utils v0.36.0-flux.14/go.mod h1:uDo7BYOfbdmk/asnHuI0IQPl6u0FCgcN54AHDu3Y5As=
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/fluxcd/cli-utils v1.3.0 h1:doEjNgeNb4klO/1mNgU9bkeou5zH/YpHBQqZ7HTBgkc=
github.com/fluxcd/cli-utils v1.3.0/go.mod h1:qW5YGrml58qvAv+fBqp9Vp4xGkkIc6h6eJLd77eqFoM=
github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0=
github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/fxamacker/cbor/v2 v2.9.3 h1:oQBnFATpNdY8gJHTndDDv5Xl4QqNaz51G5LLEPhng3Q=
github.com/fxamacker/cbor/v2 v2.9.3/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s=
github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y=
github.com/go-openapi/jsonreference v1.0.1 h1:4zJ7AmYDKNmD3aSpfPnFNCFA5E80/xMHUNKgydaLh38=
github.com/go-openapi/jsonreference v1.0.1/go.mod h1:dYplQXa6p5lXprLcJ8LE2iU7vNpXsAHDQ5ZAgL+Qx3A=
github.com/go-openapi/swag v0.29.1 h1:C6EeWzUwQtcWEhE9eqBdUubGXxhWY4PlzHMLD7kLaiQ=
github.com/go-openapi/swag v0.29.1/go.mod h1:BzxEXKiPlSXRsRTv1KSBF/BpGKHxA/YciCnr4tv9bvA=
github.com/go-openapi/swag/cmdutils v0.29.1 h1:3DorPGfUdE80BogKY22EzoHBcHMrkVomZMoV7kS4ANY=
github.com/go-openapi/swag/cmdutils v0.29.1/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM=
github.com/go-openapi/swag/conv v0.29.1 h1:AC4Eh/5c/eUDOUCzzsRC9ghmFgOSBHeRMGIngY0ZUGA=
github.com/go-openapi/swag/conv v0.29.1/go.mod h1:S1X7/ZrBEZOC0Wc8AGxjbcGS92l3WEjA7aPtpl+RaqM=
github.com/go-openapi/swag/fileutils v0.29.1 h1:ZcPzMceVhU1WPbK6N1G6sNQKdd1CWJlf3cA08UHuoM0=
github.com/go-openapi/swag/fileutils v0.29.1/go.mod h1:/wofKYckbtRl2p3+EwQsosie5CT1B38+dQ+PS579BzI=
github.com/go-openapi/swag/jsonutils v0.29.1 h1:AFCxs0eQZ24/QyfhVHM2t49rMz7Vv3XCsZQI6yrNy+c=
github.com/go-openapi/swag/jsonutils v0.29.1/go.mod h1:u3+sCfJpttDpcmS5kpm0yxL6GK0eWgODsx8Yw8fcqNM=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.1 h1:BiiXE31Bx9SfpsMmOQj5KYpUhTZBpLVriVhJDuLuY2o=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.1/go.mod h1:julgTUKZ9/D0j6O7GKajmRs+812FWxQg/mMpGunWSjg=
github.com/go-openapi/swag/loading v0.29.1 h1:FCv5fG8UhTdDJa2R7w+5O9Ekpcbw7tt0nFWvmDKGBjc=
github.com/go-openapi/swag/loading v0.29.1/go.mod h1:N0ESuem4p2oedKal8EJhciqnJ9Q9Wmt83L1CRB3Fouw=
github.com/go-openapi/swag/mangling v0.29.1 h1:lHALtvYCdxVnRl4GrHmFPwfBTZYIObqdGNSKyu/8D6I=
github.com/go-openapi/swag/mangling v0.29.1/go.mod h1:SAop9pB7PUjQ/CGCNf/JmCKTRK+GDO+RqE9UHqC/N6s=
github.com/go-openapi/swag/netutils v0.29.1 h1:IjIvdEP5duKcghFqJEPSUraRnkKYHoM65kTluTu+Jb4=
github.com/go-openapi/swag/netutils v0.29.1/go.mod h1:DUde7x4Bx00k5jYl2AdRpNAO0m7atUvD2x6X+bWkbno=
github.com/go-openapi/swag/pools v0.29.1 h1:NRogYxdEW9SjRM4mkAOji9iefO4MRXq3p/ZJcoQbUKg=
github.com/go-openapi/swag/pools v0.29.1/go.mod h1:leDcaghjkRAhCuCRv9NfJU5f0mjoU3cT/XZObhMk3pc=
github.com/go-openapi/swag/stringutils v0.29.1 h1:1ykunK7iJQk1uOO7+oUH1ukbsK85fFCOiCFMOVSY+F0=
github.com/go-openapi/swag/stringutils v0.29.1/go.mod h1:7fSqZ+z8Qc0tOfAAK0jVa5qFGrnIlRi6n7NeGGrr1vc=
github.com/go-openapi/swag/typeutils v0.29.1 h1:Nzv9nhnlLCRBPQqfOX+7lB6Guju370or8StT+lIOf6M=
github.com/go-openapi/swag/typeutils v0.29.1/go.mod h1:hxpgDZJVBkBsi/d3MIUosafoFdE5exaQRmVp0zwu3YE=
github.com/go-openapi/swag/yamlutils v0.29.1 h1:69w3tsBajm7MR/fejLy7HD/3J68Ys1SeeZMEzZ3w2sk=
github.com/go-openapi/swag/yamlutils v0.29.1/go.mod h1:rgsp3vT/QdWzKwn43CigDwjOGIenPyTZMKnxEM8jZOA=
github.com/go-openapi/testify/enable/yaml/v2 v2.6.1 h1:Jm+/ze2rMtbD98yen92AhATGLGREDYXG56Xr4gMjEtE=
github.com/go-openapi/testify/enable/yaml/v2 v2.6.1/go.mod h1:YDPnwCRDu38/oJBVMBVXOUDiJ9cIeBHWvfImHaXqnv4=
github.com/go-openapi/testify/v2 v2.6.1 h1:6CNJhTjMzgaeaH8WhshcsZNPIvRemiOcFpU7seO/y7Q=
github.com/go-openapi/testify/v2 v2.6.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
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/gobwas/glob v1.0.0 h1:p+FKbLEIsK1yZ39/OINwFvqNb5oyPY4H8xcy6uYu8dg=
github.com/gobwas/glob v1.0.0/go.mod h1:oWCdo522i2P1n/hMXGNWs7yoV4wy/ciZuUIbvKj5rkc=
github.com/gofrs/flock v0.13.1 h1:jjREztyBeSKBZYAC+mgc1laB+xsgy4kYMf3FbKF2UBo=
github.com/gofrs/flock v0.13.1/go.mod h1:sf4BFiHwnvgxa25DlQoDqXQnwRMEOwqxRq37P6MzzmE=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -141,28 +168,24 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
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-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg=
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/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=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
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.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
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=
@ -175,18 +198,14 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
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=
@ -199,21 +218,19 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/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 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/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.15 h1:rx0n8+ZdM9JWZMlr2BMPAjtLU0rfluLNtwMC2FJOTtY=
github.com/mattn/go-shellwords v1.0.15/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=
@ -225,8 +242,6 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -242,12 +257,10 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/onsi/ginkgo/v2 v2.32.0 h1:Hw7s2pVrQo/8Yz5N77qdnpHaoc+c6cC9WIV1Jce+J6E=
github.com/onsi/ginkgo/v2 v2.32.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
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=
@ -255,8 +268,6 @@ github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgr
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -265,21 +276,23 @@ github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjz
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/common v0.71.0 h1:9KDAKb7Mj3HEVKyFCK6Dc/HIwlBzZIN2l7/lrHl3KK8=
github.com/prometheus/common v0.71.0/go.mod h1:CLJ5H8TEsGX8bl31BdMkfhIZ+QmZ9tBPPotUxUbfcmk=
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.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
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=
@ -289,129 +302,126 @@ github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o=
github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw=
github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0=
github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/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.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk=
github.com/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8=
github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
github.com/tetratelabs/wazero v1.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.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
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.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
go.opentelemetry.io/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.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs=
go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
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.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0 h1:WseeVYf5dJZTsyPiyW5L14k5qsSibqXAMTSiFEDiWr0=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0/go.mod h1:SiLZnQS6Qk2eCpvr2CH/XMAOa64TWGXxEZJZCpD2Lmc=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.21.0 h1:fvNHGyo3CdRv/DQveXqhqBxnKTDyRaC5sMSQxilX/A0=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.21.0/go.mod h1:zyGrjRKL2B/6+Jc/m4/otPoZqV2MY9ZjC/aBraRO7zc=
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.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 h1:fG5MCxGz8+2VtrN/WgqSpJFctVz24gpxj8CxkKmc8Ww=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0/go.mod h1:BmAYTn+3ysbRe+IU2msxmf5Rx3g6DHvex+tWI3LdhYI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 h1:QBajQ2SrwQijzHyZbQlPsuIzpl/ll8DY6wPWsajeGcI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0/go.mod h1:08ZQLjrPLQ6R4kAXvuOvODEer5Yh4CoFvll5qB2BCI8=
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.21.0 h1:2lpf4hnrasYIsUyEXwnTZq5lsxrMm4T2Bwb06IctAZQ=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.21.0/go.mod h1:YWOW6h7jwApz9Pl76ie/izUsSPj0s2MdIlpqbPqaf3U=
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.21.0 h1:SLsVDGmtyBrdw8/a2Z0bOIxou/+bN4z56GebH7T0LvA=
go.opentelemetry.io/otel/log v0.21.0/go.mod h1:iReetQrZL9Wyg84cCkOoCmqDHS5RCFfyxC7J+r8fn8g=
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
go.opentelemetry.io/otel/metric/x v0.67.0 h1:PcicCNZFkZ4bXfSooXdo3WN7RBOVOtjVdo1wD358Uns=
go.opentelemetry.io/otel/metric/x v0.67.0/go.mod h1:FBjCWZe6wgcqxcMtjdGiClDKXb2YxxXii0CXftE4QtI=
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
go.opentelemetry.io/otel/sdk/log v0.21.0 h1:QsE7XSR0ktQdKmRKGnR+f1ObGF32WG+7MER/P9KgmYc=
go.opentelemetry.io/otel/sdk/log v0.21.0/go.mod h1:m9mApjCoD2/1QuKCAptjv+BrG9WKOvQLVdNx+iBldTo=
go.opentelemetry.io/otel/sdk/log/logtest v0.21.0 h1:X+JBBgKlswCGYsmgL0CnoUUtlE//VB345c84jYAYkdQ=
go.opentelemetry.io/otel/sdk/log/logtest v0.21.0/go.mod h1:HD1575K8e6sIFBBDd5tZB3t9DlMytWXq9FuR+Y4rfjE=
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
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.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
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.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
@ -419,43 +429,38 @@ 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.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
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.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
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=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -463,8 +468,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@ -472,38 +477,35 @@ 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
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.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
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.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
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-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc=
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/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=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@ -512,41 +514,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.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY=
k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw=
k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo=
k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE=
k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4=
k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apiserver v0.34.2 h1:2/yu8suwkmES7IzwlehAovo8dDE07cFRC7KMDb1+MAE=
k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI=
k8s.io/cli-runtime v0.34.2 h1:cct1GEuWc3IyVT8MSCoIWzRGw9HJ/C5rgP32H60H6aE=
k8s.io/cli-runtime v0.34.2/go.mod h1:X13tsrYexYUCIq8MarCBy8lrm0k0weFPTpcaNo7lms4=
k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M=
k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE=
k8s.io/component-base v0.34.2 h1:HQRqK9x2sSAsd8+R4xxRirlTjowsg6fWCPwWYeSvogQ=
k8s.io/component-base v0.34.2/go.mod h1:9xw2FHJavUHBFpiGkZoKuYZ5pdtLKe97DEByaA+hHbM=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
k8s.io/kubectl v0.34.2 h1:+fWGrVlDONMUmmQLDaGkQ9i91oszjjRAa94cr37hzqA=
k8s.io/kubectl v0.34.2/go.mod h1:X2KTOdtZZNrTWmUD4oHApJ836pevSl+zvC5sI6oO2YQ=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I=
sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM=
sigs.k8s.io/kustomize/kyaml v0.21.0 h1:7mQAf3dUwf0wBerWJd8rXhVcnkk5Tvn/q91cGkaP6HQ=
sigs.k8s.io/kustomize/kyaml v0.21.0/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ=
k8s.io/api v0.37.0 h1:Z//Vj9N7RA/yS2sDmxyeo7h+RR4zbUrd2vrd3Z0TbB4=
k8s.io/api v0.37.0/go.mod h1:LKXgcJWMc+f4OLbP5SFR8rulEg07zZhpi/zMULiBImk=
k8s.io/apiextensions-apiserver v0.37.0 h1:zRMQ3+/LIE5oZ0tVvXwYHC+dIkSP5cjNWju7AZU1LOI=
k8s.io/apiextensions-apiserver v0.37.0/go.mod h1:HU0PfSBwchHL5iDau6jjt9zU6ryWkDDlaVUiq91NK80=
k8s.io/apimachinery v0.37.0 h1:Np2AbDtf8x6RDHiD8T9LbKJ9gaegeVNa8yNm5FuGKm0=
k8s.io/apimachinery v0.37.0/go.mod h1:RN3nhprFSCxOi5Selxd7oMTXOe/c+ZbcE7Im+TS2zkE=
k8s.io/apiserver v0.37.0 h1:TXg7OxsOWrAH8J4Zi/gBAZuMw1Dfdd+6cca2h4qjRqo=
k8s.io/apiserver v0.37.0/go.mod h1:OddHDF4gy9qyIb8o/3+qaeP6S0vEObWLgOygVqXksv0=
k8s.io/cli-runtime v0.37.0 h1:U3XakUeirBQJMz5688r04z74SIHSE7V5SIZ6Ho5JyBM=
k8s.io/cli-runtime v0.37.0/go.mod h1:qiQMFkKwFFuPH6zy953On+nc3qfpEHAIDrJmAuRz5Vg=
k8s.io/client-go v0.37.0 h1:nsN31fy8wBySuZ+QRnKmrjRSQLOG2rvoGN0tKd12zhQ=
k8s.io/client-go v0.37.0/go.mod h1:FcGqw+Ll/gNQiq+nPGY1Oyt9y7SgDh1d3MW3RFDEbn0=
k8s.io/component-base v0.37.0 h1:3SdSa4+itMdFTDFTeR8CxKGmSTSMXFlKL4ky8OqjguM=
k8s.io/component-base v0.37.0/go.mod h1:LjOebp4R9y6LODWZQv102ZQxGheLcDO2ZJLAw6bbh4I=
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-20260821135717-be32def86098 h1:z5+pcu1jTyKK5mNTe2/+x+U6Uuv9jRVOJQLaBJJMpeI=
k8s.io/kube-openapi v0.0.0-20260821135717-be32def86098/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I=
k8s.io/kubectl v0.37.0 h1:cici6hiofx93ASldmprDmZF55SfhVt4o3HniltVLjTc=
k8s.io/kubectl v0.37.0/go.mod h1:RSeEl8e/yqDx6srG8Azr0uAtVPNIZljA0PNh9HCBcdg=
k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 h1:jVkFFVfXdXP74B/zbO3hM3hpSFD0xvhQ5U686DPurkE=
k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3/go.mod h1:M2s5JB1lIYP3jzZdorPLHXIPJzt9vv2muW5a6L9DtNM=
oras.land/oras-go/v2 v2.6.2 h1:N04RXngAp1LJKTG6ifz3xHPipasEkWr+hFmInja5YKo=
oras.land/oras-go/v2 v2.6.2/go.mod h1:PlTtg4JTDJkDe8yVHpM2wz7/YDc00GVas+i4jAW2TZ4=
sigs.k8s.io/controller-runtime v0.25.1 h1:BKgU9OeE8xv8EbbM8cY0NVzTQs35rokkdq1jh12fMb4=
sigs.k8s.io/controller-runtime v0.25.1/go.mod h1:4QqLdT6z/L6Olj8JJCtvztid4/fnIiYsfaTFScegctc=
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.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.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3CIki3aW8c4Q=
sigs.k8s.io/structured-merge-diff/v6 v6.4.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=

@ -45,11 +45,11 @@ type Chart struct {
// Templates for this chart.
Templates []*common.File `json:"templates"`
// Values are default config for this chart.
Values map[string]interface{} `json:"values"`
Values map[string]any `json:"values"`
// Schema is an optional JSON schema for imposing structure on Values
Schema []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,7 +140,8 @@ func (ch *Chart) AppVersion() string {
}
// CRDs returns a list of File objects in the 'crds/' directory of a Helm chart.
// Deprecated: use CRDObjects()
//
// Deprecated: Use CRDObjects instead.
func (ch *Chart) CRDs() []*common.File {
files := []*common.File{}
// Find all resources in the crds/ directory

@ -21,6 +21,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/pkg/chart/common"
)
@ -59,7 +60,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)
}
@ -76,15 +77,12 @@ func TestSaveChartNoRawData(t *testing.T) {
}
is := assert.New(t)
req := require.New(t)
data, err := json.Marshal(chrt)
if err != nil {
t.Fatal(err)
}
req.NoError(err)
res := &Chart{}
if err := json.Unmarshal(data, res); err != nil {
t.Fatal(err)
}
req.NoError(json.Unmarshal(data, res))
is.Equal([]*common.File(nil), res.Raw)
}
@ -104,7 +102,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 +122,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,7 @@ type Dependency struct {
Enabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty"`
// ImportValues holds the mapping of source values to parent key to be imported. Each item can be a
// string or pair of child/parent sublist items.
ImportValues []interface{} `json:"import-values,omitempty" yaml:"import-values,omitempty"`
ImportValues []any `json:"import-values,omitempty" yaml:"import-values,omitempty"`
// Alias usable alias to be used for the chart
Alias string `json:"alias,omitempty" yaml:"alias,omitempty"`
}

@ -17,6 +17,8 @@ package v3
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestValidateDependency(t *testing.T) {
@ -35,10 +37,10 @@ func TestValidateDependency(t *testing.T) {
} {
dep.Alias = value
res := dep.Validate()
if res != nil && !shouldFail {
t.Errorf("Failed on case %q", dep.Alias)
} else if res == nil && shouldFail {
t.Errorf("Expected failure for %q", dep.Alias)
if shouldFail {
assert.Errorf(t, res, "Expected failure for %q", dep.Alias)
} else {
assert.NoErrorf(t, res, "Failed on case %q", dep.Alias)
}
}
}

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

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package lint // import "helm.sh/helm/v4/internal/chart/v3/lint"
package lint
import (
"path/filepath"
@ -43,8 +43,7 @@ func WithSkipSchemaValidation(skipSchemaValidation bool) LinterOption {
}
}
func RunAll(baseDir string, values map[string]interface{}, namespace string, options ...LinterOption) support.Linter {
func RunAll(baseDir string, values map[string]any, namespace string, options ...LinterOption) support.Linter {
chartDir, _ := filepath.Abs(baseDir)
lo := linterOptions{}

@ -22,6 +22,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/internal/chart/v3/lint/support"
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
@ -29,14 +30,16 @@ import (
const namespace = "testNamespace"
const badChartDir = "rules/testdata/badchartfile"
const badValuesFileDir = "rules/testdata/badvaluesfile"
const badYamlFileDir = "rules/testdata/albatross"
const badCrdFileDir = "rules/testdata/badcrdfile"
const goodChartDir = "rules/testdata/goodone"
const subChartValuesDir = "rules/testdata/withsubchart"
const malformedTemplate = "rules/testdata/malformed-template"
const invalidChartFileDir = "rules/testdata/invalidchartfile"
const (
badChartDir = "rules/testdata/badchartfile"
badValuesFileDir = "rules/testdata/badvaluesfile"
badYamlFileDir = "rules/testdata/albatross"
badCrdFileDir = "rules/testdata/badcrdfile"
goodChartDir = "rules/testdata/goodone"
subChartValuesDir = "rules/testdata/withsubchart"
malformedTemplate = "rules/testdata/malformed-template"
invalidChartFileDir = "rules/testdata/invalidchartfile"
)
func TestBadChartV3(t *testing.T) {
var values map[string]any
@ -91,50 +94,37 @@ func TestBadChartV3(t *testing.T) {
func TestInvalidYaml(t *testing.T) {
var values map[string]any
m := RunAll(badYamlFileDir, values, namespace).Messages
if len(m) != 1 {
t.Fatalf("All didn't fail with expected errors, got %#v", m)
}
if !strings.Contains(m[0].Err.Error(), "deliberateSyntaxError") {
t.Errorf("All didn't have the error for deliberateSyntaxError")
}
require.Len(t, m, 1, "All didn't fail with expected errors, got %#v", m)
assert.ErrorContains(t, m[0].Err, "deliberateSyntaxError", "All didn't have the error for deliberateSyntaxError")
}
func TestInvalidChartYamlV3(t *testing.T) {
var values map[string]any
m := RunAll(invalidChartFileDir, values, namespace).Messages
t.Log(m)
if len(m) != 3 {
t.Fatalf("All didn't fail with expected errors, got %#v", m)
}
if !strings.Contains(m[0].Err.Error(), "failed to strictly parse chart metadata file") {
t.Errorf("All didn't have the error for duplicate YAML keys")
}
require.Len(t, m, 3, "All didn't fail with expected errors, got %#v", m)
assert.ErrorContains(t, m[0].Err, "failed to strictly parse chart metadata file", "All didn't have the error for duplicate YAML keys")
}
func TestBadValuesV3(t *testing.T) {
var values map[string]any
m := RunAll(badValuesFileDir, values, namespace).Messages
if len(m) < 1 {
t.Fatalf("All didn't fail with expected errors, got %#v", m)
}
if !strings.Contains(m[0].Err.Error(), "unable to parse YAML") {
t.Errorf("All didn't have the error for invalid key format: %s", m[0].Err)
}
require.NotEmptyf(t, m, "All didn't fail with expected errors")
assert.ErrorContains(t, m[0].Err, "unable to parse YAML", "All didn't have the error for invalid key format: %s", m[0].Err)
}
func TestBadCrdFileV3(t *testing.T) {
var values map[string]any
m := RunAll(badCrdFileDir, values, namespace).Messages
assert.Lenf(t, m, 2, "All didn't fail with expected errors, got %#v", m)
assert.ErrorContains(t, m[0].Err, "apiVersion is not in 'apiextensions.k8s.io'")
require.Lenf(t, m, 2, "All didn't fail with expected errors, got %#v", m)
require.ErrorContains(t, m[0].Err, "apiVersion is not in 'apiextensions.k8s.io'")
assert.ErrorContains(t, m[1].Err, "object kind is not 'CustomResourceDefinition'")
}
func TestGoodChart(t *testing.T) {
var values map[string]any
m := RunAll(goodChartDir, values, namespace).Messages
if len(m) != 0 {
t.Error("All returned linter messages when it shouldn't have")
if !assert.Empty(t, m, "All returned linter messages when it shouldn't have") {
for i, msg := range m {
t.Logf("Message %d: %s", i, msg)
}
@ -149,22 +139,17 @@ func TestHelmCreateChart(t *testing.T) {
dir := t.TempDir()
createdChart, err := chartutil.Create("testhelmcreatepasseslint", dir)
if err != nil {
t.Error(err)
// Fatal is bad because of the defer.
return
}
require.NoError(t, err)
// Note: we test with strict=true here, even though others have
// strict = false.
m := RunAll(createdChart, values, namespace, WithSkipSchemaValidation(true)).Messages
if ll := len(m); ll != 1 {
t.Errorf("All should have had exactly 1 error. Got %d", ll)
if !assert.Len(t, m, 1, "All should have had exactly 1 error") {
for i, msg := range m {
t.Logf("Message %d: %s", i, msg.Error())
}
} else if msg := m[0].Err.Error(); !strings.Contains(msg, "icon is recommended") {
t.Errorf("Unexpected lint error: %s", msg)
} else {
assert.ErrorContains(t, m[0].Err, "icon is recommended")
}
}
@ -175,22 +160,9 @@ func TestHelmCreateChart(t *testing.T) {
//
// Resources like hpa and ingress, which are disabled by default in values.yaml are enabled here using the equivalent
// of the `--set` flag.
//
// Note: This test requires the following ldflags to be set per the current Kubernetes version to avoid false-positive
// results.
// 1. -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMajor=<k8s-major-version>
// 2. -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMinor=<k8s-minor-version>
// or directly use '$(LDFLAGS)' in Makefile.
//
// When run without ldflags, the test passes giving a false-positive result. This is because the variables
// `k8sVersionMajor` and `k8sVersionMinor` by default are set to an older version of Kubernetes, with which, there
// might not be the deprecation warning.
func TestHelmCreateChart_CheckDeprecatedWarnings(t *testing.T) {
createdChart, err := chartutil.Create("checkdeprecatedwarnings", t.TempDir())
if err != nil {
t.Error(err)
return
}
require.NoError(t, err)
// Add values to enable hpa, and ingress which are disabled by default.
// This is the equivalent of:
@ -220,8 +192,7 @@ func TestHelmCreateChart_CheckDeprecatedWarnings(t *testing.T) {
func TestSubChartValuesChart(t *testing.T) {
var values map[string]any
m := RunAll(subChartValuesDir, values, namespace).Messages
if len(m) != 0 {
t.Error("All returned linter messages when it shouldn't have")
if !assert.Empty(t, m, "All returned linter messages when it shouldn't have") {
for i, msg := range m {
t.Logf("Message %d: %s", i, msg)
}
@ -241,13 +212,9 @@ 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 '{'")
}
require.Len(t, m, 1, "All didn't fail with expected errors, got %#v", m)
assert.ErrorContains(t, m[0].Err, "invalid character '{'", "All didn't have the error for invalid character '{'")
}
}

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules"
package rules
import (
"errors"
@ -69,15 +69,15 @@ func Chartfile(linter *support.Linter) {
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartDependencies(chartFile))
}
func validateChartVersionType(data map[string]interface{}) error {
func validateChartVersionType(data map[string]any) error {
return isStringValue(data, "version")
}
func validateChartAppVersionType(data map[string]interface{}) error {
func validateChartAppVersionType(data map[string]any) error {
return isStringValue(data, "appVersion")
}
func isStringValue(data map[string]interface{}, key string) error {
func isStringValue(data map[string]any, key string) error {
value, ok := data[key]
if !ok {
return nil
@ -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
@ -160,14 +160,14 @@ func validateChartVersion(cf *chart.Metadata) error {
func validateChartMaintainer(cf *chart.Metadata) error {
for _, maintainer := range cf.Maintainers {
if maintainer == nil {
switch {
case maintainer == nil:
return errors.New("a maintainer entry is empty")
}
if maintainer.Name == "" {
case maintainer.Name == "":
return errors.New("each maintainer requires a name")
} else if maintainer.Email != "" && !govalidator.IsEmail(maintainer.Email) {
case maintainer.Email != "" && !govalidator.IsEmail(maintainer.Email):
return fmt.Errorf("invalid email '%s' for maintainer '%s'", maintainer.Email, maintainer.Name)
} else if maintainer.URL != "" && !govalidator.IsURL(maintainer.URL) {
case maintainer.URL != "" && !govalidator.IsURL(maintainer.URL):
return fmt.Errorf("invalid url '%s' for maintainer '%s'", maintainer.URL, maintainer.Name)
}
}
@ -205,7 +205,7 @@ func validateChartDependencies(cf *chart.Metadata) error {
}
func validateChartType(cf *chart.Metadata) error {
if len(cf.Type) > 0 && cf.APIVersion != chart.APIVersionV3 {
if cf.Type != "" && cf.APIVersion != chart.APIVersionV3 {
return fmt.Errorf("chart type is not valid in apiVersion '%s'. It is valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV3)
}
return nil
@ -214,12 +214,12 @@ func validateChartType(cf *chart.Metadata) error {
// loadChartFileForTypeCheck loads the Chart.yaml
// in a generic form of a map[string]interface{}, so that the type
// of the values can be checked
func loadChartFileForTypeCheck(filename string) (map[string]interface{}, error) {
func loadChartFileForTypeCheck(filename string) (map[string]any, error) {
b, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
y := make(map[string]interface{})
y := make(map[string]any)
err = yaml.Unmarshal(b, &y)
return y, err
}

@ -18,11 +18,15 @@ package rules
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/internal/chart/v3/lint/support"
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
@ -40,46 +44,33 @@ var (
nonExistingChartFilePath = filepath.Join(os.TempDir(), "Chart.yaml")
)
var badChart, _ = chartutil.LoadChartfile(badChartFilePath)
var badChartName, _ = chartutil.LoadChartfile(badChartNamePath)
var (
badChart, _ = chartutil.LoadChartfile(badChartFilePath)
badChartName, _ = chartutil.LoadChartfile(badChartNamePath)
)
// Validation functions Test
func TestValidateChartYamlNotDirectory(t *testing.T) {
_ = os.Mkdir(nonExistingChartFilePath, os.ModePerm)
defer os.Remove(nonExistingChartFilePath)
err := validateChartYamlNotDirectory(nonExistingChartFilePath)
if err == nil {
t.Errorf("validateChartYamlNotDirectory to return a linter error, got no error")
}
assert.Error(t, validateChartYamlNotDirectory(nonExistingChartFilePath), "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")
}
require.Error(t, validateChartYamlFormat(errors.New("Read 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")
}
assert.NoError(t, validateChartYamlFormat(nil), "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")
}
require.Error(t, validateChartName(badChart), "validateChartName to return a linter error, got no error")
err = validateChartName(badChartName)
if err == nil {
t.Error("expected validateChartName to return a linter error for an invalid name, got no error")
}
assert.Error(t, validateChartName(badChartName), "expected validateChartName to return a linter error for an invalid name, got no error")
}
func TestValidateChartVersion(t *testing.T) {
var failTest = []struct {
failTest := []struct {
Version string
ErrorMsg string
}{
@ -91,27 +82,23 @@ func TestValidateChartVersion(t *testing.T) {
{"1", "'1' is not a valid SemVerV2"},
}
var successTest = []string{"0.0.1", "0.0.1+build", "0.0.1-beta"}
successTest := []string{"0.0.1", "0.0.1+build", "0.0.1-beta"}
for _, test := range failTest {
badChart.Version = test.Version
err := validateChartVersion(badChart)
if err == nil || !strings.Contains(err.Error(), test.ErrorMsg) {
t.Errorf("validateChartVersion(%s) to return \"%s\", got no error", test.Version, test.ErrorMsg)
}
for i, test := range failTest {
t.Run(strconv.Itoa(i), func(t *testing.T) {
badChart.Version = test.Version
require.ErrorContains(t, validateChartVersion(badChart), test.ErrorMsg, "validateChartVersion(%s) to return \"%s\", got no error", test.Version, test.ErrorMsg)
})
}
for _, version := range successTest {
badChart.Version = version
err := validateChartVersion(badChart)
if err != nil {
t.Errorf("validateChartVersion(%s) to return no error, got a linter error", version)
}
assert.NoError(t, validateChartVersion(badChart), "validateChartVersion(%s) to return no error, got a linter error", version)
}
}
func TestValidateChartMaintainer(t *testing.T) {
var failTest = []struct {
failTest := []struct {
Name string
Email string
ErrorMsg string
@ -121,7 +108,7 @@ func TestValidateChartMaintainer(t *testing.T) {
{"John Snow", "wrongFormatEmail.com", "invalid email"},
}
var successTest = []struct {
successTest := []struct {
Name string
Email string
}{
@ -130,49 +117,37 @@ func TestValidateChartMaintainer(t *testing.T) {
}
for _, test := range failTest {
badChart.Maintainers = []*chart.Maintainer{{Name: test.Name, Email: test.Email}}
err := validateChartMaintainer(badChart)
if err == nil || !strings.Contains(err.Error(), test.ErrorMsg) {
t.Errorf("validateChartMaintainer(%s, %s) to return \"%s\", got no error", test.Name, test.Email, test.ErrorMsg)
}
t.Run(fmt.Sprintf("%s, %s", test.Name, test.Email), func(t *testing.T) {
badChart.Maintainers = []*chart.Maintainer{{Name: test.Name, Email: test.Email}}
require.ErrorContains(t, validateChartMaintainer(badChart), test.ErrorMsg, "validateChartMaintainer(%s, %s) to return \"%s\", got no error", test.Name, test.Email, test.ErrorMsg)
})
}
for _, test := range successTest {
badChart.Maintainers = []*chart.Maintainer{{Name: test.Name, Email: test.Email}}
err := validateChartMaintainer(badChart)
if err != nil {
t.Errorf("validateChartMaintainer(%s, %s) to return no error, got %s", test.Name, test.Email, err.Error())
}
t.Run(fmt.Sprintf("%s, %s", test.Name, test.Email), func(t *testing.T) {
badChart.Maintainers = []*chart.Maintainer{{Name: test.Name, Email: test.Email}}
require.NoError(t, validateChartMaintainer(badChart), "validateChartMaintainer(%s, %s) to return no error", test.Name, test.Email)
})
}
// Testing for an empty maintainer
badChart.Maintainers = []*chart.Maintainer{nil}
err := validateChartMaintainer(badChart)
if err == nil {
t.Errorf("validateChartMaintainer did not return error for nil maintainer as expected")
}
if err.Error() != "a maintainer entry is empty" {
t.Errorf("validateChartMaintainer returned unexpected error for nil maintainer: %s", err.Error())
}
assert.EqualError(t, validateChartMaintainer(badChart), "a maintainer entry is empty")
}
func TestValidateChartSources(t *testing.T) {
var failTest = []string{"", "RiverRun", "john@winterfell", "riverrun.io"}
var successTest = []string{"http://riverrun.io", "https://riverrun.io", "https://riverrun.io/blackfish"}
failTest := []string{"", "RiverRun", "john@winterfell", "riverrun.io"}
successTest := []string{"http://riverrun.io", "https://riverrun.io", "https://riverrun.io/blackfish"}
for _, test := range failTest {
badChart.Sources = []string{test}
err := validateChartSources(badChart)
if err == nil || !strings.Contains(err.Error(), "invalid source URL") {
t.Errorf("validateChartSources(%s) to return \"invalid source URL\", got no error", test)
}
t.Run(test, func(t *testing.T) {
badChart.Sources = []string{test}
require.ErrorContains(t, validateChartSources(badChart), "invalid source URL", "validateChartSources(%s) to return \"invalid source URL\", got no error", test)
})
}
for _, test := range successTest {
badChart.Sources = []string{test}
err := validateChartSources(badChart)
if err != nil {
t.Errorf("validateChartSources(%s) to return no error, got %s", test, err.Error())
}
assert.NoError(t, validateChartSources(badChart), "validateChartSources(%s) to return no error", test)
}
}
@ -182,44 +157,29 @@ func TestValidateChartIconPresence(t *testing.T) {
Icon: "",
}
err := validateChartIconPresence(testChart)
if err == nil {
t.Errorf("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())
}
assert.ErrorContains(t, validateChartIconPresence(testChart), "icon is recommended", "expected %q", "icon is recommended")
})
t.Run("Icon present", func(t *testing.T) {
testChart := &chart.Metadata{
Icon: "http://example.org/icon.png",
}
err := validateChartIconPresence(testChart)
if err != nil {
t.Errorf("Unexpected error: %q", err.Error())
}
assert.NoError(t, validateChartIconPresence(testChart))
})
}
func TestValidateChartIconURL(t *testing.T) {
var failTest = []string{"RiverRun", "john@winterfell", "riverrun.io"}
var successTest = []string{"http://riverrun.io", "https://riverrun.io", "https://riverrun.io/blackfish.png"}
failTest := []string{"RiverRun", "john@winterfell", "riverrun.io"}
successTest := []string{"http://riverrun.io", "https://riverrun.io", "https://riverrun.io/blackfish.png"}
for _, test := range failTest {
badChart.Icon = test
err := validateChartIconURL(badChart)
if err == nil || !strings.Contains(err.Error(), "invalid icon URL") {
t.Errorf("validateChartIconURL(%s) to return \"invalid icon URL\", got no error", test)
}
t.Run(test, func(t *testing.T) {
badChart.Icon = test
require.ErrorContains(t, validateChartIconURL(badChart), "invalid icon URL", "validateChartIconURL(%s) to return \"invalid icon URL\", got no error", test)
})
}
for _, test := range successTest {
badChart.Icon = test
err := validateChartSources(badChart)
if err != nil {
t.Errorf("validateChartIconURL(%s) to return no error, got %s", test, err.Error())
}
assert.NoError(t, validateChartIconURL(badChart), "validateChartIconURL(%s) to return no error", test)
}
}
@ -230,26 +190,11 @@ func TestV3Chartfile(t *testing.T) {
msgs := linter.Messages
expectedNumberOfErrorMessages := 6
if len(msgs) != expectedNumberOfErrorMessages {
t.Errorf("Expected %d errors, got %d", expectedNumberOfErrorMessages, len(msgs))
return
}
if !strings.Contains(msgs[0].Err.Error(), "name is required") {
t.Errorf("Unexpected message 0: %s", msgs[0].Err)
}
if !strings.Contains(msgs[1].Err.Error(), "apiVersion is required. The value must be \"v3\"") {
t.Errorf("Unexpected message 1: %s", msgs[1].Err)
}
if !strings.Contains(msgs[2].Err.Error(), "version '0.0.0.0' is not a valid SemVer") {
t.Errorf("Unexpected message 2: %s", msgs[2].Err)
}
if !strings.Contains(msgs[3].Err.Error(), "icon is recommended") {
t.Errorf("Unexpected message 3: %s", msgs[3].Err)
}
require.Lenf(t, msgs, expectedNumberOfErrorMessages, "Expected %d errors", expectedNumberOfErrorMessages)
require.ErrorContains(t, msgs[0].Err, "name is required", "Unexpected message 0")
require.ErrorContains(t, msgs[1].Err, "apiVersion is required. The value must be \"v3\"", "Unexpected message 1")
require.ErrorContains(t, msgs[2].Err, "version '0.0.0.0' is not a valid SemVer", "Unexpected message 2")
assert.ErrorContains(t, msgs[3].Err, "icon is recommended", "Unexpected message 3")
})
t.Run("Chart.yaml validity issues due to type mismatch", func(t *testing.T) {
@ -258,21 +203,9 @@ func TestV3Chartfile(t *testing.T) {
msgs := linter.Messages
expectedNumberOfErrorMessages := 3
if len(msgs) != expectedNumberOfErrorMessages {
t.Errorf("Expected %d errors, got %d", expectedNumberOfErrorMessages, len(msgs))
return
}
if !strings.Contains(msgs[0].Err.Error(), "version should be of type string") {
t.Errorf("Unexpected message 0: %s", msgs[0].Err)
}
if !strings.Contains(msgs[1].Err.Error(), "version '7.2445e+06' is not a valid SemVer") {
t.Errorf("Unexpected message 1: %s", msgs[1].Err)
}
if !strings.Contains(msgs[2].Err.Error(), "appVersion should be of type string") {
t.Errorf("Unexpected message 2: %s", msgs[2].Err)
}
require.Lenf(t, msgs, expectedNumberOfErrorMessages, "Expected %d errors", expectedNumberOfErrorMessages)
require.ErrorContains(t, msgs[0].Err, "version should be of type string", "Unexpected message 0")
require.ErrorContains(t, msgs[1].Err, "version '7.2445e+06' is not a valid SemVer", "Unexpected message 1")
assert.ErrorContains(t, msgs[2].Err, "appVersion should be of type string", "Unexpected message 2")
})
}

@ -19,7 +19,6 @@ package rules
import (
"bytes"
"errors"
"fmt"
"io"
"io/fs"
"os"
@ -70,7 +69,7 @@ func Crds(linter *support.Linter) {
var yamlStruct *k8sYamlStruct
err := decoder.Decode(&yamlStruct)
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
@ -80,8 +79,10 @@ func Crds(linter *support.Linter) {
return
}
linter.RunLinterRule(support.ErrorSev, fpath, validateCrdAPIVersion(yamlStruct))
linter.RunLinterRule(support.ErrorSev, fpath, validateCrdKind(yamlStruct))
if yamlStruct != nil {
linter.RunLinterRule(support.ErrorSev, fpath, validateCrdAPIVersion(yamlStruct))
linter.RunLinterRule(support.ErrorSev, fpath, validateCrdKind(yamlStruct))
}
}
}
}
@ -100,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
}

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

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules"
package rules
import (
"fmt"

@ -19,6 +19,9 @@ import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/internal/chart/v3/lint/support"
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
@ -63,17 +66,13 @@ func chartWithBadDependencies() chart.Chart {
func TestValidateDependencyInChartsDir(t *testing.T) {
c := chartWithBadDependencies()
if err := validateDependencyInChartsDir(&c); err == nil {
t.Error("chart should have been flagged for missing deps in chart directory")
}
assert.Error(t, validateDependencyInChartsDir(&c), "chart should have been flagged for missing deps in chart directory")
}
func TestValidateDependencyInMetadata(t *testing.T) {
c := chartWithBadDependencies()
if err := validateDependencyInMetadata(&c); err == nil {
t.Errorf("chart should have been flagged for missing deps in chart metadata")
}
assert.Error(t, validateDependencyInMetadata(&c), "chart should have been flagged for missing deps in chart metadata")
}
func TestValidateDependenciesUnique(t *testing.T) {
@ -131,9 +130,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")
}
assert.Error(t, validateDependenciesUnique(&tt.chart), "chart should have been flagged for dependency shadowing")
}
}
@ -141,15 +138,11 @@ func TestDependencies(t *testing.T) {
tmp := t.TempDir()
c := chartWithBadDependencies()
err := chartutil.SaveDir(&c, tmp)
if err != nil {
t.Fatal(err)
}
require.NoError(t, chartutil.SaveDir(&c, tmp))
linter := support.Linter{ChartDir: filepath.Join(tmp, c.Metadata.Name)}
Dependencies(&linter)
if l := len(linter.Messages); l != 2 {
t.Errorf("expected 2 linter errors for bad chart dependencies. Got %d.", l)
if !assert.Len(t, linter.Messages, 2, "expected 2 linter errors for bad chart dependencies") {
for i, msg := range linter.Messages {
t.Logf("Message: %d, Error: %#v", i, msg)
}

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

@ -14,9 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package rules // import "helm.sh/helm/v4/internal/chart/v3/lint/rules"
package rules
import "testing"
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestValidateNoDeprecations(t *testing.T) {
deprecated := &k8sYamlStruct{
@ -24,18 +29,12 @@ func TestValidateNoDeprecations(t *testing.T) {
Kind: "Deployment",
}
err := validateNoDeprecations(deprecated, nil)
if err == nil {
t.Fatal("Expected deprecated extension to be flagged")
}
depErr := err.(deprecatedAPIError)
if depErr.Message == "" {
t.Fatalf("Expected error message to be non-blank: %v", err)
}
if err := validateNoDeprecations(&k8sYamlStruct{
require.Error(t, err, "Expected deprecated extension to be flagged")
var depErr deprecatedAPIError
require.ErrorAs(t, err, &depErr, "Expected error to be of type deprecatedAPIError")
require.NotEmpty(t, depErr.Message, "Expected error message to be non-blank")
assert.NoError(t, validateNoDeprecations(&k8sYamlStruct{
APIVersion: "v1",
Kind: "Pod",
}, nil); err != nil {
t.Errorf("Expected a v1 Pod to not be deprecated")
}
}, nil), "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"
@ -42,17 +43,17 @@ import (
)
// Templates lints the templates in the Linter.
func Templates(linter *support.Linter, values map[string]interface{}, namespace string, _ bool) {
func Templates(linter *support.Linter, values map[string]any, namespace string, _ bool) {
TemplatesWithKubeVersion(linter, values, namespace, nil)
}
// TemplatesWithKubeVersion lints the templates in the Linter, allowing to specify the kubernetes version.
func TemplatesWithKubeVersion(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion) {
func TemplatesWithKubeVersion(linter *support.Linter, values map[string]any, namespace string, kubeVersion *common.KubeVersion) {
TemplatesWithSkipSchemaValidation(linter, values, namespace, kubeVersion, false)
}
// TemplatesWithSkipSchemaValidation lints the templates in the Linter, allowing to specify the kubernetes version and if schema validation is enabled or not.
func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion, skipSchemaValidation bool) {
func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string]any, namespace string, kubeVersion *common.KubeVersion, skipSchemaValidation bool) {
fpath := "templates/"
templatesPath := filepath.Join(linter.ChartDir, fpath)
@ -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)
@ -150,7 +151,7 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string
var yamlStruct *k8sYamlStruct
err := decoder.Decode(&yamlStruct)
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
@ -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

@ -20,10 +20,12 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/internal/chart/v3/lint/support"
chartutil "helm.sh/helm/v4/internal/chart/v3/util"
@ -33,43 +35,36 @@ import (
const templateTestBasedir = "./testdata/albatross"
func TestValidateAllowedExtension(t *testing.T) {
var failTest = []string{"/foo", "/test.toml"}
failTest := []string{"/foo", "/test.toml"}
for _, test := range failTest {
err := validateAllowedExtension(test)
if err == nil || !strings.Contains(err.Error(), "Valid extensions are .yaml, .yml, .tpl, or .txt") {
t.Errorf("validateAllowedExtension('%s') to return \"Valid extensions are .yaml, .yml, .tpl, or .txt\", got no error", test)
}
require.ErrorContains(t, validateAllowedExtension(test), "Valid extensions are .yaml, .yml, .tpl, or .txt", "validateAllowedExtension('%s') to return \"Valid extensions are .yaml, .yml, .tpl, or .txt\", got no error", test)
}
var successTest = []string{"/foo.yaml", "foo.yaml", "foo.tpl", "/foo/bar/baz.yaml", "NOTES.txt"}
successTest := []string{"/foo.yaml", "foo.yaml", "foo.tpl", "/foo/bar/baz.yaml", "NOTES.txt"}
for _, test := range successTest {
err := validateAllowedExtension(test)
if err != nil {
t.Errorf("validateAllowedExtension('%s') to return no error but got \"%s\"", test, err.Error())
}
assert.NoError(t, validateAllowedExtension(test), "validateAllowedExtension('%s') to return no error", test)
}
}
var values = map[string]interface{}{"nameOverride": "", "httpPort": 80}
var values = map[string]any{"nameOverride": "", "httpPort": 80}
const namespace = "testNamespace"
const strict = false
const (
namespace = "testNamespace"
strict = false
)
func TestTemplateParsing(t *testing.T) {
linter := support.Linter{ChartDir: templateTestBasedir}
Templates(&linter, values, namespace, strict)
res := linter.Messages
if len(res) != 1 {
t.Fatalf("Expected one error, got %d, %v", len(res), res)
}
if !strings.Contains(res[0].Err.Error(), "deliberateSyntaxError") {
t.Errorf("Unexpected error: %s", res[0])
}
require.Len(t, res, 1, "Expected one error, got %d, %v", len(res), res)
assert.ErrorContains(t, res[0].Err, "deliberateSyntaxError")
}
var wrongTemplatePath = filepath.Join(templateTestBasedir, "templates", "fail.yaml")
var ignoredTemplatePath = filepath.Join(templateTestBasedir, "fail.yaml.ignored")
var (
wrongTemplatePath = filepath.Join(templateTestBasedir, "templates", "fail.yaml")
ignoredTemplatePath = filepath.Join(templateTestBasedir, "fail.yaml.ignored")
)
// Test a template with all the existing features:
// namespaces, partial templates
@ -82,9 +77,7 @@ func TestTemplateIntegrationHappyPath(t *testing.T) {
Templates(&linter, values, namespace, strict)
res := linter.Messages
if len(res) != 0 {
t.Fatalf("Expected no error, got %d, %v", len(res), res)
}
require.Empty(t, res, "Expected no error, got %d, %v", len(res), res)
}
func TestMultiTemplateFail(t *testing.T) {
@ -92,13 +85,9 @@ func TestMultiTemplateFail(t *testing.T) {
Templates(&linter, values, namespace, strict)
res := linter.Messages
if len(res) != 1 {
t.Fatalf("Expected 1 error, got %d, %v", len(res), res)
}
require.Len(t, res, 1, "Expected 1 error, got %d, %v", len(res), res)
if !strings.Contains(res[0].Err.Error(), "object name does not conform to Kubernetes naming requirements") {
t.Errorf("Unexpected error: %s", res[0].Err)
}
assert.ErrorContains(t, res[0].Err, "object name does not conform to Kubernetes naming requirements")
}
func TestValidateMetadataName(t *testing.T) {
@ -176,8 +165,11 @@ func TestValidateMetadataName(t *testing.T) {
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%s/%s", tt.obj.Kind, tt.obj.Metadata.Name), func(t *testing.T) {
if err := validateMetadataName(tt.obj); (err != nil) != tt.wantErr {
t.Errorf("validateMetadataName() error = %v, wantErr %v", err, tt.wantErr)
err := validateMetadataName(tt.obj)
if tt.wantErr {
require.Error(t, err, "validateMetadataName()")
} else {
require.NoError(t, err, "validateMetadataName()")
}
})
}
@ -207,23 +199,20 @@ func TestDeprecatedAPIFails(t *testing.T) {
}
tmpdir := t.TempDir()
if err := chartutil.SaveDir(&mychart, tmpdir); err != nil {
t.Fatal(err)
}
require.NoError(t, chartutil.SaveDir(&mychart, tmpdir))
linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())}
Templates(&linter, values, namespace, strict)
if l := len(linter.Messages); l != 1 {
if !assert.Len(t, linter.Messages, 1) {
for i, msg := range linter.Messages {
t.Logf("Message %d: %s", i, msg)
}
t.Fatalf("Expected 1 lint error, got %d", l)
}
require.Len(t, linter.Messages, 1, "Expected 1 lint error")
err := linter.Messages[0].Err.(deprecatedAPIError)
if err.Deprecated != "apps/v1beta1 Deployment" {
t.Errorf("Surprised to learn that %q is deprecated", err.Deprecated)
}
var err deprecatedAPIError
require.ErrorAs(t, linter.Messages[0].Err, &err, "Expected error to be of type deprecatedAPIError")
assert.Equal(t, "apps/v1beta1 Deployment", err.Deprecated, "Surprised to learn that %q is deprecated", err.Deprecated)
}
const manifest = `apiVersion: v1
@ -242,14 +231,13 @@ data:
//
// See https://github.com/helm/helm/issues/7483
func TestStrictTemplateParsingMapError(t *testing.T) {
ch := chart.Chart{
Metadata: &chart.Metadata{
Name: "regression7483",
APIVersion: "v2",
Version: "0.1.0",
},
Values: map[string]interface{}{
Values: map[string]any{
"mymap": map[string]string{
"key1": "val1",
},
@ -263,15 +251,12 @@ func TestStrictTemplateParsingMapError(t *testing.T) {
},
}
dir := t.TempDir()
if err := chartutil.SaveDir(&ch, dir); err != nil {
t.Fatal(err)
}
require.NoError(t, chartutil.SaveDir(&ch, dir))
linter := &support.Linter{
ChartDir: filepath.Join(dir, ch.Metadata.Name),
}
Templates(linter, ch.Values, namespace, strict)
if len(linter.Messages) != 0 {
t.Errorf("expected zero messages, got %d", len(linter.Messages))
if !assert.Empty(t, linter.Messages, "expected zero messages") {
for i, msg := range linter.Messages {
t.Logf("Message %d: %q", i, msg)
}
@ -307,9 +292,7 @@ spec:
- name: nginx
image: nginx:1.14.2
`
if err := validateMatchSelector(md, manifest); err != nil {
t.Error(err)
}
require.NoError(t, validateMatchSelector(md, manifest))
manifest = `
apiVersion: apps/v1
kind: Deployment
@ -331,9 +314,7 @@ spec:
- name: nginx
image: nginx:1.14.2
`
if err := validateMatchSelector(md, manifest); err != nil {
t.Error(err)
}
require.NoError(t, validateMatchSelector(md, manifest))
manifest = `
apiVersion: apps/v1
kind: Deployment
@ -352,9 +333,7 @@ spec:
- name: nginx
image: nginx:1.14.2
`
if err := validateMatchSelector(md, manifest); err == nil {
t.Error("expected Deployment with no selector to fail")
}
assert.Error(t, validateMatchSelector(md, manifest), "expected Deployment with no selector to fail")
}
func TestValidateTopIndentLevel(t *testing.T) {
@ -367,11 +346,13 @@ func TestValidateTopIndentLevel(t *testing.T) {
" apiVersion:foo": true,
"\n\n apiVersion:foo\n\n": true,
} {
if err := validateTopIndentLevel(doc); (err == nil) == shouldFail {
t.Errorf("Expected %t for %q", shouldFail, doc)
err := validateTopIndentLevel(doc)
if shouldFail {
assert.Errorf(t, err, "Expected %t for %q", shouldFail, doc)
} else {
assert.NoErrorf(t, err, "Expected %t for %q", shouldFail, doc)
}
}
}
// TestEmptyWithCommentsManifests checks the lint is not failing against empty manifests that contains only comments
@ -394,19 +375,18 @@ func TestEmptyWithCommentsManifests(t *testing.T) {
}
tmpdir := t.TempDir()
if err := chartutil.SaveDir(&mychart, tmpdir); err != nil {
t.Fatal(err)
}
require.NoError(t, chartutil.SaveDir(&mychart, tmpdir))
linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())}
Templates(&linter, values, namespace, strict)
if l := len(linter.Messages); l > 0 {
if !assert.Empty(t, linter.Messages) {
for i, msg := range linter.Messages {
t.Logf("Message %d: %s", i, msg)
}
t.Fatalf("Expected 0 lint errors, got %d", l)
}
require.Empty(t, linter.Messages, "Expected 0 lint errors")
}
func TestValidateListAnnotations(t *testing.T) {
md := &k8sYamlStruct{
APIVersion: "v1",
@ -426,9 +406,7 @@ items:
helm.sh/resource-policy: keep
`
if err := validateListAnnotations(md, manifest); err == nil {
t.Fatal("expected list with nested keep annotations to fail")
}
require.Error(t, validateListAnnotations(md, manifest), "expected list with nested keep annotations to fail")
manifest = `
apiVersion: v1
@ -441,9 +419,7 @@ items:
kind: ConfigMap
`
if err := validateListAnnotations(md, manifest); err != nil {
t.Fatalf("List objects keep annotations should pass. got: %s", err)
}
require.NoError(t, validateListAnnotations(md, manifest), "List objects keep annotations should pass")
}
func TestIsYamlFileExtension(t *testing.T) {
@ -459,9 +435,6 @@ func TestIsYamlFileExtension(t *testing.T) {
for _, test := range tests {
result := isYamlFileExtension(test.filename)
if result != test.expected {
t.Errorf("isYamlFileExtension(%s) = %v; want %v", test.filename, result, test.expected)
}
assert.Equal(t, test.expected, result, "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"
@ -32,7 +33,7 @@ import (
// they are only tested for well-formedness.
//
// If additional values are supplied, they are coalesced into the values in values.yaml.
func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]interface{}, skipSchemaValidation bool) {
func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]any, skipSchemaValidation bool) {
file := "values.yaml"
vf := filepath.Join(linter.ChartDir, file)
fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(vf))
@ -47,12 +48,12 @@ func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]inter
func validateValuesFileExistence(valuesPath string) error {
_, err := os.Stat(valuesPath)
if err != nil {
return fmt.Errorf("file does not exist")
return errors.New("file does not exist")
}
return nil
}
func validateValuesFile(valuesPath string, overrides map[string]interface{}, skipSchemaValidation bool) error {
func validateValuesFile(valuesPath string, overrides map[string]any, skipSchemaValidation bool) error {
values, err := common.ReadValuesFile(valuesPath)
if err != nil {
return fmt.Errorf("unable to parse YAML: %w", err)
@ -63,7 +64,7 @@ func validateValuesFile(valuesPath string, overrides map[string]interface{}, ski
// We could change that. For now, though, we retain that strategy, and thus can
// coalesce tables (like reuse-values does) instead of doing the full chart
// CoalesceValues
coalescedValues := util.CoalesceTables(make(map[string]interface{}, len(overrides)), overrides)
coalescedValues := util.CoalesceTables(make(map[string]any, len(overrides)), overrides)
coalescedValues = util.CoalesceTables(coalescedValues, values)
ext := filepath.Ext(valuesPath)

@ -22,11 +22,12 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/internal/test/ensure"
)
var nonExistingValuesFilePath = filepath.Join("/fake/dir", "values.yaml")
var nonExistingValuesFilePath = filepath.FromSlash("/fake/dir/values.yaml")
const testSchema = `
{
@ -55,10 +56,7 @@ func TestValidateValuesYamlNotDirectory(t *testing.T) {
_ = os.Mkdir(nonExistingValuesFilePath, os.ModePerm)
defer os.Remove(nonExistingValuesFilePath)
err := validateValuesFileExistence(nonExistingValuesFilePath)
if err == nil {
t.Errorf("validateValuesFileExistence to return a linter error, got no error")
}
assert.Error(t, validateValuesFileExistence(nonExistingValuesFilePath), "validateValuesFileExistence to return a linter error, got no error")
}
func TestValidateValuesFileWellFormed(t *testing.T) {
@ -67,9 +65,7 @@ func TestValidateValuesFileWellFormed(t *testing.T) {
`
tmpdir := ensure.TempFile(t, "values.yaml", []byte(badYaml))
valfile := filepath.Join(tmpdir, "values.yaml")
if err := validateValuesFile(valfile, map[string]interface{}{}, false); err == nil {
t.Fatal("expected values file to fail parsing")
}
require.Error(t, validateValuesFile(valfile, map[string]any{}, false), "expected values file to fail parsing")
}
func TestValidateValuesFileSchema(t *testing.T) {
@ -78,9 +74,7 @@ func TestValidateValuesFileSchema(t *testing.T) {
createTestingSchema(t, tmpdir)
valfile := filepath.Join(tmpdir, "values.yaml")
if err := validateValuesFile(valfile, map[string]interface{}{}, false); err != nil {
t.Fatalf("Failed validation with %s", err)
}
require.NoError(t, validateValuesFile(valfile, map[string]any{}, false), "Failed validation")
}
func TestValidateValuesFileSchemaFailure(t *testing.T) {
@ -91,12 +85,7 @@ func TestValidateValuesFileSchemaFailure(t *testing.T) {
valfile := filepath.Join(tmpdir, "values.yaml")
err := validateValuesFile(valfile, map[string]interface{}{}, false)
if err == nil {
t.Fatal("expected values file to fail parsing")
}
assert.Contains(t, err.Error(), "- at '/username': got number, want string")
assert.ErrorContains(t, validateValuesFile(valfile, map[string]any{}, false), "- at '/username': got number, want string")
}
func TestValidateValuesFileSchemaFailureButWithSkipSchemaValidation(t *testing.T) {
@ -107,48 +96,43 @@ func TestValidateValuesFileSchemaFailureButWithSkipSchemaValidation(t *testing.T
valfile := filepath.Join(tmpdir, "values.yaml")
err := validateValuesFile(valfile, map[string]interface{}{}, true)
if err != nil {
t.Fatal("expected values file to pass parsing because of skipSchemaValidation")
}
require.NoError(t, validateValuesFile(valfile, map[string]any{}, true), "expected values file to pass parsing because of skipSchemaValidation")
}
func TestValidateValuesFileSchemaOverrides(t *testing.T) {
yaml := "username: admin"
overrides := map[string]interface{}{
overrides := map[string]any{
"password": "swordfish",
}
tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml))
createTestingSchema(t, tmpdir)
valfile := filepath.Join(tmpdir, "values.yaml")
if err := validateValuesFile(valfile, overrides, false); err != nil {
t.Fatalf("Failed validation with %s", err)
}
require.NoError(t, validateValuesFile(valfile, overrides, false), "Failed validation")
}
func TestValidateValuesFile(t *testing.T) {
tests := []struct {
name string
yaml string
overrides map[string]interface{}
overrides map[string]any
errorMessage string
}{
{
name: "value added",
yaml: "username: admin",
overrides: map[string]interface{}{"password": "swordfish"},
overrides: map[string]any{"password": "swordfish"},
},
{
name: "value not overridden",
yaml: "username: admin\npassword:",
overrides: map[string]interface{}{"username": "anotherUser"},
overrides: map[string]any{"username": "anotherUser"},
errorMessage: "- at '/password': got null, want string",
},
{
name: "value overridden",
yaml: "username: admin\npassword:",
overrides: map[string]interface{}{"username": "anotherUser", "password": "swordfish"},
overrides: map[string]any{"username": "anotherUser", "password": "swordfish"},
},
}
@ -161,13 +145,11 @@ func TestValidateValuesFile(t *testing.T) {
err := validateValuesFile(valfile, tt.overrides, false)
switch {
case err != nil && tt.errorMessage == "":
t.Errorf("Failed validation with %s", err)
case err == nil && tt.errorMessage != "":
t.Error("expected values file to fail parsing")
case err != nil && tt.errorMessage != "":
assert.Contains(t, err.Error(), tt.errorMessage, "Failed with unexpected error")
if tt.errorMessage == "" {
require.NoError(t, err, "Failed validation")
} else {
require.Error(t, err, "expected values file to fail parsing")
assert.ErrorContains(t, err, tt.errorMessage)
}
})
}
@ -176,8 +158,6 @@ func TestValidateValuesFile(t *testing.T) {
func createTestingSchema(t *testing.T, dir string) string {
t.Helper()
schemafile := filepath.Join(dir, "values.schema.json")
if err := os.WriteFile(schemafile, []byte(testSchema), 0700); err != nil {
t.Fatalf("Failed to write schema to tmpdir: %s", err)
}
require.NoError(t, os.WriteFile(schemafile, []byte(testSchema), 0o700), "Failed to write schema to tmpdir")
return schemafile
}

@ -20,4 +20,4 @@ Package support contains tools for linting charts.
Linting is the process of testing charts for errors or warnings regarding
formatting, compilation, or standards compliance.
*/
package support // import "helm.sh/helm/v4/internal/chart/v3/lint/support"
package support

@ -19,12 +19,15 @@ package support
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var errLint = errors.New("lint failed")
func TestRunLinterRule(t *testing.T) {
var tests = []struct {
tests := []struct {
Severity int
LintError error
ExpectedMessages int
@ -47,33 +50,19 @@ func TestRunLinterRule(t *testing.T) {
linter := Linter{}
for _, test := range tests {
isValid := linter.RunLinterRule(test.Severity, "chart", test.LintError)
if len(linter.Messages) != test.ExpectedMessages {
t.Errorf("RunLinterRule(%d, \"chart\", %v), linter.Messages should now have %d message, we got %d", test.Severity, test.LintError, test.ExpectedMessages, len(linter.Messages))
}
if linter.HighestSeverity != test.ExpectedHighestSeverity {
t.Errorf("RunLinterRule(%d, \"chart\", %v), linter.HighestSeverity should be %d, we got %d", test.Severity, test.LintError, test.ExpectedHighestSeverity, linter.HighestSeverity)
}
if isValid != test.ExpectedReturn {
t.Errorf("RunLinterRule(%d, \"chart\", %v), should have returned %t but returned %t", test.Severity, test.LintError, test.ExpectedReturn, isValid)
}
assert.Lenf(t, linter.Messages, test.ExpectedMessages, "RunLinterRule(%d, \"chart\", %v), linter.Messages should now have %d message, we got %d", test.Severity, test.LintError, test.ExpectedMessages, len(linter.Messages))
assert.Equalf(t, test.ExpectedHighestSeverity, linter.HighestSeverity, "RunLinterRule(%d, \"chart\", %v), linter.HighestSeverity should be %d, we got %d", test.Severity, test.LintError, test.ExpectedHighestSeverity, linter.HighestSeverity)
assert.Equalf(t, test.ExpectedReturn, isValid, "RunLinterRule(%d, \"chart\", %v), should have returned %t but returned %t", test.Severity, test.LintError, test.ExpectedReturn, isValid)
}
}
func TestMessage(t *testing.T) {
m := Message{ErrorSev, "Chart.yaml", errors.New("Foo")}
if m.Error() != "[ERROR] Chart.yaml: Foo" {
t.Errorf("Unexpected output: %s", m.Error())
}
require.EqualError(t, m, "[ERROR] Chart.yaml: Foo")
m = Message{WarningSev, "templates/", errors.New("Bar")}
if m.Error() != "[WARNING] templates/: Bar" {
t.Errorf("Unexpected output: %s", m.Error())
}
require.EqualError(t, m, "[WARNING] templates/: Bar")
m = Message{InfoSev, "templates/rc.yaml", errors.New("FooBar")}
if m.Error() != "[INFO] templates/rc.yaml: FooBar" {
t.Errorf("Unexpected output: %s", m.Error())
}
assert.EqualError(t, m, "[INFO] templates/rc.yaml: FooBar")
}

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

@ -43,6 +43,10 @@ func (l DirLoader) Load() (*chart.Chart, error) {
//
// This loads charts only from directories.
func LoadDir(dir string) (*chart.Chart, error) {
return loadDir(dir, archive.MaxDecompressedChartSize)
}
func loadDir(dir string, budget int64) (*chart.Chart, error) {
topdir, err := filepath.Abs(dir)
if err != nil {
return nil, err
@ -64,6 +68,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
files := []*archive.BufferedFile{}
topdir += string(filepath.Separator)
budgetReader := archive.NewBudgetedReader(budget)
walk := func(name string, fi os.FileInfo, err error) error {
n := strings.TrimPrefix(name, topdir)
@ -100,11 +105,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", name)
}
if fi.Size() > archive.MaxDecompressedFileSize {
return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), archive.MaxDecompressedFileSize)
}
data, err := os.ReadFile(name)
data, err := budgetReader.ReadFileWithBudget(name, fi.Size())
if err != nil {
return fmt.Errorf("error reading %s: %w", n, err)
}
@ -114,7 +115,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
files = append(files, &archive.BufferedFile{Name: n, ModTime: fi.ModTime(), Data: data})
return nil
}
if err = sympath.Walk(topdir, walk); err != nil {
if err := sympath.Walk(topdir, walk); err != nil {
return c, err
}

@ -123,7 +123,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
}
fname := strings.TrimPrefix(f.Name, "charts/")
cname := strings.SplitN(fname, "/", 2)[0]
cname, _, _ := strings.Cut(fname, "/")
if slices.Index(subChartsKeys, cname) == -1 {
subChartsKeys = append(subChartsKeys, cname)
}
@ -181,15 +181,27 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
// LoadValues loads values from a reader.
//
// The reader is expected to contain one or more YAML documents, the values of which are merged.
// And the values can be either a chart's default values or a user-supplied values.
func LoadValues(data io.Reader) (map[string]interface{}, error) {
values := map[string]interface{}{}
reader := utilyaml.NewYAMLReader(bufio.NewReader(data))
// And the values can be either a chart's default values or user-supplied values.
func LoadValues(data io.Reader) (map[string]any, error) {
// Read fully first. YAMLReader/LineReader can drop a final unterminated
// line when its length is an exact multiple of bufio.Reader's default
// buffer (4096). Appending a trailing newline avoids that case.
// See https://github.com/helm/helm/issues/32506
b, err := io.ReadAll(data)
if err != nil {
return nil, err
}
if len(b) > 0 && b[len(b)-1] != '\n' {
b = append(b, '\n')
}
values := map[string]any{}
reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(b)))
for {
currentMap := map[string]interface{}{}
currentMap := map[string]any{}
raw, err := reader.Read()
if err != nil {
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("error reading yaml document: %w", err)
@ -204,13 +216,13 @@ func LoadValues(data io.Reader) (map[string]interface{}, error) {
// MergeMaps merges two maps. If a key exists in both maps, the value from b will be used.
// If the value is a map, the maps will be merged recursively.
func MergeMaps(a, b map[string]interface{}) map[string]interface{} {
out := make(map[string]interface{}, len(a))
func MergeMaps(a, b map[string]any) map[string]any {
out := make(map[string]any, len(a))
maps.Copy(out, a)
for k, v := range b {
if v, ok := v.(map[string]interface{}); ok {
if v, ok := v.(map[string]any); ok {
if bv, ok := out[k]; ok {
if bv, ok := bv.(map[string]interface{}); ok {
if bv, ok := bv.(map[string]any); ok {
out[k] = MergeMaps(bv, v)
continue
}

@ -20,16 +20,20 @@ import (
"archive/tar"
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/loader/archive"
@ -37,52 +41,49 @@ import (
func TestLoadDir(t *testing.T) {
l, err := Loader("testdata/frobnitz")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
require.NoError(t, err, "Failed to load testdata")
c, err := l.Load()
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
require.NoError(t, err, "Failed to load testdata")
verifyFrobnitz(t, c)
verifyChart(t, c)
verifyDependencies(t, c)
verifyDependenciesLock(t, c)
}
func TestLoadDirExceedsBudget(t *testing.T) {
_, err := loadDir("testdata/frobnitz", 1)
if err == nil {
t.Fatal("expected error when chart directory exceeds budget")
}
if !strings.Contains(err.Error(), "chart exceeds maximum decompressed size") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestLoadDirWithDevNull(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("test only works on unix systems with /dev/null present")
}
l, err := Loader("testdata/frobnitz_with_dev_null")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
if _, err := l.Load(); err == nil {
t.Errorf("packages with an irregular file (/dev/null) should not load")
}
require.NoError(t, err, "Failed to load testdata")
_, err = l.Load()
assert.Error(t, err, "packages with an irregular file (/dev/null) should not load")
}
func TestLoadDirWithSymlink(t *testing.T) {
sym := filepath.Join("..", "LICENSE")
link := filepath.Join("testdata", "frobnitz_with_symlink", "LICENSE")
if err := os.Symlink(sym, link); err != nil {
t.Fatal(err)
}
require.NoError(t, os.Symlink(sym, link))
defer os.Remove(link)
l, err := Loader("testdata/frobnitz_with_symlink")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
require.NoError(t, err, "Failed to load testdata")
c, err := l.Load()
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
require.NoError(t, err, "Failed to load testdata")
verifyFrobnitz(t, c)
verifyChart(t, c)
verifyDependencies(t, c)
@ -99,53 +100,37 @@ func TestBomTestData(t *testing.T) {
}
archive, err := os.ReadFile("testdata/frobnitz_with_bom.tgz")
if err != nil {
t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err)
}
require.NoErrorf(t, err, "Error reading archive frobnitz_with_bom.tgz")
unzipped, err := gzip.NewReader(bytes.NewReader(archive))
if err != nil {
t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err)
}
require.NoErrorf(t, err, "Error reading archive frobnitz_with_bom.tgz")
defer unzipped.Close()
for _, testFile := range testFiles {
data := make([]byte, 3)
err := unzipped.Reset(bytes.NewReader(archive))
if err != nil {
t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err)
}
require.NoErrorf(t, unzipped.Reset(bytes.NewReader(archive)), "Error reading archive frobnitz_with_bom.tgz")
tr := tar.NewReader(unzipped)
for {
file, err := tr.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
if err != nil {
t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err)
}
require.NoErrorf(t, err, "Error reading archive frobnitz_with_bom.tgz")
if file != nil && strings.EqualFold(file.Name, testFile) {
_, err := tr.Read(data)
if err != nil {
t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err)
} else {
if err == nil {
break
}
t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err)
}
}
if !bytes.Equal(data, utf8bom) {
t.Fatalf("Test file has no BOM or is invalid: frobnitz_with_bom.tgz/%s", testFile)
}
require.Truef(t, bytes.Equal(data, utf8bom), "Test file has no BOM or is invalid: frobnitz_with_bom.tgz/%s", testFile)
}
}
func TestLoadDirWithUTFBOM(t *testing.T) {
l, err := Loader("testdata/frobnitz_with_bom")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
require.NoError(t, err, "Failed to load testdata")
c, err := l.Load()
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
require.NoError(t, err, "Failed to load testdata")
verifyFrobnitz(t, c)
verifyChart(t, c)
verifyDependencies(t, c)
@ -155,13 +140,9 @@ func TestLoadDirWithUTFBOM(t *testing.T) {
func TestLoadArchiveWithUTFBOM(t *testing.T) {
l, err := Loader("testdata/frobnitz_with_bom.tgz")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
require.NoError(t, err, "Failed to load testdata")
c, err := l.Load()
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
require.NoError(t, err, "Failed to load testdata")
verifyFrobnitz(t, c)
verifyChart(t, c)
verifyDependencies(t, c)
@ -171,13 +152,9 @@ func TestLoadArchiveWithUTFBOM(t *testing.T) {
func TestLoadFile(t *testing.T) {
l, err := Loader("testdata/frobnitz-1.2.3.tgz")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
require.NoError(t, err, "Failed to load testdata")
c, err := l.Load()
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
require.NoError(t, err, "Failed to load testdata")
verifyFrobnitz(t, c)
verifyChart(t, c)
verifyDependencies(t, c)
@ -231,36 +208,16 @@ icon: https://example.com/64x64.png
}
c, err := LoadFiles(goodFiles)
if err != nil {
t.Errorf("Expected good files to be loaded, got %v", err)
}
if c.Name() != "frobnitz" {
t.Errorf("Expected chart name to be 'frobnitz', got %s", c.Name())
}
if c.Values["var"] != "some values" {
t.Error("Expected chart values to be populated with default values")
}
if len(c.Raw) != 5 {
t.Errorf("Expected %d files, got %d", 5, len(c.Raw))
}
if !bytes.Equal(c.Schema, []byte("type: Values")) {
t.Error("Expected chart schema to be populated with default values")
}
if len(c.Templates) != 2 {
t.Errorf("Expected number of templates == 2, got %d", len(c.Templates))
}
if _, err = LoadFiles([]*archive.BufferedFile{}); err == nil {
t.Fatal("Expected err to be non-nil")
}
if err.Error() != "Chart.yaml file is missing" {
t.Errorf("Expected chart metadata missing error, got '%s'", err.Error())
}
require.NoError(t, err, "Expected good files to be loaded")
assert.Equal(t, "frobnitz", c.Name(), "Expected chart name to be 'frobnitz', got %s", c.Name())
assert.Equal(t, "some values", c.Values["var"], "Expected chart values to be populated with default values")
assert.Len(t, c.Raw, 5, "Expected 5 files")
assert.True(t, bytes.Equal(c.Schema, []byte("type: Values")), "Expected chart schema to be populated with default values")
assert.Len(t, c.Templates, 2, "Expected 2 templates")
_, err = LoadFiles([]*archive.BufferedFile{})
require.Error(t, err, "Expected err to be non-nil")
assert.EqualError(t, err, "Chart.yaml file is missing", "Expected chart metadata missing error, got '%s'", err.Error())
}
// Test the order of file loading. The Chart.yaml file needs to come first for
@ -316,9 +273,7 @@ icon: https://example.com/64x64.png
// Capture stderr to make sure message about Chart.yaml handle dependencies
// is not present
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("Unable to create pipe: %s", err)
}
require.NoError(t, err, "Unable to create pipe")
stderr := log.Writer()
log.SetOutput(w)
defer func() {
@ -326,26 +281,19 @@ icon: https://example.com/64x64.png
}()
_, err = LoadFiles(goodFiles)
if err != nil {
t.Errorf("Expected good files to be loaded, got %v", err)
}
require.NoError(t, err, "Expected good files to be loaded")
w.Close()
var text bytes.Buffer
io.Copy(&text, r)
if text.String() != "" {
t.Errorf("Expected no message to Stderr, got %s", text.String())
}
assert.Empty(t, text.String(), "Expected no message to Stderr, got %s", text.String())
}
// Packaging the chart on a Windows machine will produce an
// archive that has \\ as delimiters. Test that we support these archives
func TestLoadFileBackslash(t *testing.T) {
c, err := Load("testdata/frobnitz_backslash-1.2.3.tgz")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
require.NoError(t, err, "Failed to load testdata")
verifyChartFileAndTemplate(t, c, "frobnitz_backslash")
verifyChart(t, c)
verifyDependencies(t, c)
@ -353,13 +301,9 @@ func TestLoadFileBackslash(t *testing.T) {
func TestLoadV3WithReqs(t *testing.T) {
l, err := Loader("testdata/frobnitz.v3.reqs")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
require.NoError(t, err, "Failed to load testdata")
c, err := l.Load()
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
require.NoError(t, err, "Failed to load testdata")
verifyDependencies(t, c)
verifyDependenciesLock(t, c)
}
@ -369,24 +313,19 @@ func TestLoadInvalidArchive(t *testing.T) {
writeTar := func(filename, internalPath string, body []byte) {
dest, err := os.Create(filename)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
zipper := gzip.NewWriter(dest)
tw := tar.NewWriter(zipper)
h := &tar.Header{
Name: internalPath,
Mode: 0755,
Mode: 0o755,
Size: int64(len(body)),
ModTime: time.Now(),
}
if err := tw.WriteHeader(h); err != nil {
t.Fatal(err)
}
if _, err := tw.Write(body); err != nil {
t.Fatal(err)
}
require.NoError(t, tw.WriteHeader(h))
_, err = tw.Write(body)
require.NoError(t, err)
tw.Close()
zipper.Close()
dest.Close()
@ -418,43 +357,33 @@ func TestLoadInvalidArchive(t *testing.T) {
illegalChart := filepath.Join(tmpdir, tt.chartname)
writeTar(illegalChart, tt.internal, []byte("hello: world"))
_, err := Load(illegalChart)
if err == nil {
t.Fatal("expected error when unpacking illegal files")
}
if !strings.Contains(err.Error(), tt.expectError) {
t.Errorf("Expected error to contain %q, got %q for %s", tt.expectError, err.Error(), tt.chartname)
}
require.Error(t, err, "expected error when unpacking illegal files")
require.ErrorContains(t, err, tt.expectError, "Expected error to contain %q, got %q for %s", tt.expectError, err.Error(), tt.chartname)
}
// Make sure that absolute path gets interpreted as relative
illegalChart := filepath.Join(tmpdir, "abs-path.tgz")
writeTar(illegalChart, "/Chart.yaml", []byte("hello: world"))
_, err := Load(illegalChart)
if err.Error() != "validation: chart.metadata.name is required" {
t.Error(err)
}
require.EqualError(t, err, "validation: chart.metadata.name is required")
// And just to validate that the above was not spurious
illegalChart = filepath.Join(tmpdir, "abs-path2.tgz")
writeTar(illegalChart, "files/whatever.yaml", []byte("hello: world"))
_, err = Load(illegalChart)
if err.Error() != "Chart.yaml file is missing" {
t.Errorf("Unexpected error message: %s", err)
}
require.EqualError(t, err, "Chart.yaml file is missing")
// Finally, test that drive letter gets stripped off on Windows
illegalChart = filepath.Join(tmpdir, "abs-winpath.tgz")
writeTar(illegalChart, "c:\\Chart.yaml", []byte("hello: world"))
_, err = Load(illegalChart)
if err.Error() != "validation: chart.metadata.name is required" {
t.Error(err)
}
assert.EqualError(t, err, "validation: chart.metadata.name is required")
}
func TestLoadValues(t *testing.T) {
testCases := map[string]struct {
data []byte
expctedValues map[string]interface{}
expctedValues map[string]any
}{
"It should load values correctly": {
data: []byte(`
@ -463,11 +392,11 @@ foo:
bar:
version: v2
`),
expctedValues: map[string]interface{}{
"foo": map[string]interface{}{
expctedValues: map[string]any{
"foo": map[string]any{
"image": "foo:v1",
},
"bar": map[string]interface{}{
"bar": map[string]any{
"version": "v2",
},
},
@ -482,11 +411,11 @@ bar:
foo:
image: foo:v2
`),
expctedValues: map[string]interface{}{
"foo": map[string]interface{}{
expctedValues: map[string]any{
"foo": map[string]any{
"image": "foo:v2",
},
"bar": map[string]interface{}{
"bar": map[string]any{
"version": "v2",
},
},
@ -495,58 +424,72 @@ foo:
for testName, testCase := range testCases {
t.Run(testName, func(tt *testing.T) {
values, err := LoadValues(bytes.NewReader(testCase.data))
if err != nil {
tt.Fatal(err)
}
if !reflect.DeepEqual(values, testCase.expctedValues) {
tt.Errorf("Expected values: %v, got %v", testCase.expctedValues, values)
require.NoError(tt, err)
assert.Equalf(tt, testCase.expctedValues, values, "Expected values: %v, got %v", testCase.expctedValues, values)
})
}
}
func TestLoadValuesEOFBoundary(t *testing.T) {
// Reproduces #32506: a single logical line whose length is a multiple of
// bufio's default buffer (4096) and has no trailing newline used to be
// dropped entirely by YAMLReader, yielding empty values.
// Also cover 8192 (2x buffer) so we do not only hit the single-buffer case.
for _, size := range []int{4096, 8192} {
t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) {
prefix := []byte(`{"foo":"`)
suffix := []byte(`"}`)
pad := size - len(prefix) - len(suffix)
data := make([]byte, 0, size)
data = append(data, prefix...)
data = append(data, bytes.Repeat([]byte("x"), pad)...)
data = append(data, suffix...)
if len(data) != size {
t.Fatalf("test setup: want data length %d, got %d", size, len(data))
}
values, err := LoadValues(bytes.NewReader(data))
require.NoError(t, err)
assert.Equal(t, map[string]any{
"foo": string(bytes.Repeat([]byte("x"), pad)),
}, values)
})
}
}
func TestMergeValuesV3(t *testing.T) {
nestedMap := map[string]interface{}{
nestedMap := map[string]any{
"foo": "bar",
"baz": map[string]string{
"cool": "stuff",
},
}
anotherNestedMap := map[string]interface{}{
anotherNestedMap := map[string]any{
"foo": "bar",
"baz": map[string]string{
"cool": "things",
"awesome": "stuff",
},
}
flatMap := map[string]interface{}{
flatMap := map[string]any{
"foo": "bar",
"baz": "stuff",
}
anotherFlatMap := map[string]interface{}{
anotherFlatMap := map[string]any{
"testing": "fun",
}
testMap := MergeMaps(flatMap, nestedMap)
equal := reflect.DeepEqual(testMap, nestedMap)
if !equal {
t.Errorf("Expected a nested map to overwrite a flat value. Expected: %v, got %v", nestedMap, testMap)
}
assert.Equal(t, testMap, nestedMap, "Expected a nested map to overwrite a flat value. Expected: %v, got %v", nestedMap, testMap)
testMap = MergeMaps(nestedMap, flatMap)
equal = reflect.DeepEqual(testMap, flatMap)
if !equal {
t.Errorf("Expected a flat value to overwrite a map. Expected: %v, got %v", flatMap, testMap)
}
assert.Equal(t, testMap, flatMap, "Expected a flat value to overwrite a map. Expected: %v, got %v", flatMap, testMap)
testMap = MergeMaps(nestedMap, anotherNestedMap)
equal = reflect.DeepEqual(testMap, anotherNestedMap)
if !equal {
t.Errorf("Expected a nested map to overwrite another nested map. Expected: %v, got %v", anotherNestedMap, testMap)
}
assert.Equal(t, testMap, anotherNestedMap, "Expected a nested map to overwrite another nested map. Expected: %v, got %v", anotherNestedMap, testMap)
testMap = MergeMaps(anotherFlatMap, anotherNestedMap)
expectedMap := map[string]interface{}{
expectedMap := map[string]any{
"testing": "fun",
"foo": "bar",
"baz": map[string]string{
@ -554,32 +497,23 @@ func TestMergeValuesV3(t *testing.T) {
"awesome": "stuff",
},
}
equal = reflect.DeepEqual(testMap, expectedMap)
if !equal {
t.Errorf("Expected a map with different keys to merge properly with another map. Expected: %v, got %v", expectedMap, testMap)
}
assert.Equal(t, expectedMap, testMap, "Expected a map with different keys to merge properly with another map. Expected: %v, got %v", expectedMap, testMap)
}
func verifyChart(t *testing.T, c *chart.Chart) {
t.Helper()
if c.Name() == "" {
t.Fatalf("No chart metadata found on %v", c)
}
require.NotEmpty(t, c.Name(), "No chart metadata found on %v", c)
t.Logf("Verifying chart %s", c.Name())
if len(c.Templates) != 1 {
t.Errorf("Expected 1 template, got %d", len(c.Templates))
}
assert.Len(t, c.Templates, 1, "Expected 1 template")
numfiles := 6
if len(c.Files) != numfiles {
t.Errorf("Expected %d extra files, got %d", numfiles, len(c.Files))
if !assert.Len(t, c.Files, numfiles, "Expected %d extra files", numfiles) {
for _, n := range c.Files {
t.Logf("\t%s", n.Name)
}
}
if len(c.Dependencies()) != 2 {
t.Errorf("Expected 2 dependencies, got %d (%v)", len(c.Dependencies()), c.Dependencies())
if !assert.Len(t, c.Dependencies(), 2, "Expected 2 dependencies") {
for _, d := range c.Dependencies() {
t.Logf("\tSubchart: %s\n", d.Name())
}
@ -595,63 +529,40 @@ func verifyChart(t *testing.T, c *chart.Chart) {
}
for _, dep := range c.Dependencies() {
if dep.Metadata == nil {
t.Fatalf("expected metadata on dependency: %v", dep)
}
require.NotNil(t, dep.Metadata, "expected metadata on dependency: %v", dep)
exp, ok := expect[dep.Name()]
if !ok {
t.Fatalf("Unknown dependency %s", dep.Name())
}
if exp["version"] != dep.Metadata.Version {
t.Errorf("Expected %s version %s, got %s", dep.Name(), exp["version"], dep.Metadata.Version)
}
require.True(t, ok, "Unknown dependency %s", dep.Name())
assert.Equal(t, exp["version"], dep.Metadata.Version, "Expected %s version %s, got %s", dep.Name(), exp["version"], dep.Metadata.Version)
}
}
func verifyDependencies(t *testing.T, c *chart.Chart) {
t.Helper()
if len(c.Metadata.Dependencies) != 2 {
t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies))
}
assert.Len(t, c.Metadata.Dependencies, 2, "Expected 2 dependencies")
tests := []*chart.Dependency{
{Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"},
{Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"},
}
for i, tt := range tests {
d := c.Metadata.Dependencies[i]
if d.Name != tt.Name {
t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name)
}
if d.Version != tt.Version {
t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version)
}
if d.Repository != tt.Repository {
t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository)
}
assert.Equal(t, tt.Name, d.Name, "Expected dependency named %q, got %q", tt.Name, d.Name)
assert.Equal(t, tt.Version, d.Version, "Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version)
assert.Equal(t, tt.Repository, d.Repository, "Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository)
}
}
func verifyDependenciesLock(t *testing.T, c *chart.Chart) {
t.Helper()
if len(c.Metadata.Dependencies) != 2 {
t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies))
}
assert.Len(t, c.Metadata.Dependencies, 2, "Expected 2 dependencies, got %d", len(c.Metadata.Dependencies))
tests := []*chart.Dependency{
{Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"},
{Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"},
}
for i, tt := range tests {
d := c.Metadata.Dependencies[i]
if d.Name != tt.Name {
t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name)
}
if d.Version != tt.Version {
t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version)
}
if d.Repository != tt.Repository {
t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository)
}
assert.Equal(t, tt.Name, d.Name, "Expected dependency named %q, got %q", tt.Name, d.Name)
assert.Equal(t, tt.Version, d.Version, "Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version)
assert.Equal(t, tt.Repository, d.Repository, "Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository)
}
}
@ -662,53 +573,25 @@ func verifyFrobnitz(t *testing.T, c *chart.Chart) {
func verifyChartFileAndTemplate(t *testing.T, c *chart.Chart, name string) {
t.Helper()
if c.Metadata == nil {
t.Fatal("Metadata is nil")
}
if c.Name() != name {
t.Errorf("Expected %s, got %s", name, c.Name())
}
if len(c.Templates) != 1 {
t.Fatalf("Expected 1 template, got %d", len(c.Templates))
}
if c.Templates[0].Name != "templates/template.tpl" {
t.Errorf("Unexpected template: %s", c.Templates[0].Name)
}
if len(c.Templates[0].Data) == 0 {
t.Error("No template data.")
}
if len(c.Files) != 6 {
t.Fatalf("Expected 6 Files, got %d", len(c.Files))
}
if len(c.Dependencies()) != 2 {
t.Fatalf("Expected 2 Dependency, got %d", len(c.Dependencies()))
}
if len(c.Metadata.Dependencies) != 2 {
t.Fatalf("Expected 2 Dependencies.Dependency, got %d", len(c.Metadata.Dependencies))
}
if len(c.Lock.Dependencies) != 2 {
t.Fatalf("Expected 2 Lock.Dependency, got %d", len(c.Lock.Dependencies))
}
require.NotNil(t, c.Metadata, "Metadata is nil")
assert.Equal(t, name, c.Name(), "Expected %s, got %s", name, c.Name())
require.Len(t, c.Templates, 1, "Expected 1 template, got %d", len(c.Templates))
assert.Equal(t, "templates/template.tpl", c.Templates[0].Name, "Unexpected template: %s", c.Templates[0].Name)
assert.NotEmpty(t, c.Templates[0].Data, "No template data.")
require.Len(t, c.Files, 6, "Expected 6 Files, got %d", len(c.Files))
require.Len(t, c.Dependencies(), 2, "Expected 2 Dependency, got %d", len(c.Dependencies()))
require.Len(t, c.Metadata.Dependencies, 2, "Expected 2 Dependencies.Dependency, got %d", len(c.Metadata.Dependencies))
require.Len(t, c.Lock.Dependencies, 2, "Expected 2 Lock.Dependency, got %d", len(c.Lock.Dependencies))
for _, dep := range c.Dependencies() {
switch dep.Name() {
case "mariner":
case "alpine":
if len(dep.Templates) != 1 {
t.Fatalf("Expected 1 template, got %d", len(dep.Templates))
}
if dep.Templates[0].Name != "templates/alpine-pod.yaml" {
t.Errorf("Unexpected template: %s", dep.Templates[0].Name)
}
if len(dep.Templates[0].Data) == 0 {
t.Error("No template data.")
}
if len(dep.Files) != 1 {
t.Fatalf("Expected 1 Files, got %d", len(dep.Files))
}
if len(dep.Dependencies()) != 2 {
t.Fatalf("Expected 2 Dependency, got %d", len(dep.Dependencies()))
}
require.Len(t, dep.Templates, 1, "Expected 1 template, got %d", len(dep.Templates))
assert.Equal(t, "templates/alpine-pod.yaml", dep.Templates[0].Name, "Unexpected template: %s", dep.Templates[0].Name)
assert.NotEmpty(t, dep.Templates[0].Data, "No template data.")
require.Len(t, dep.Files, 1, "Expected 1 Files, got %d", len(dep.Files))
require.Len(t, dep.Dependencies(), 2, "Expected 2 Dependency, got %d", len(dep.Dependencies()))
default:
t.Errorf("Unexpected dependency %s", dep.Name())
}
@ -718,8 +601,6 @@ func verifyChartFileAndTemplate(t *testing.T, c *chart.Chart, name string) {
func verifyBomStripped(t *testing.T, files []*common.File) {
t.Helper()
for _, file := range files {
if bytes.HasPrefix(file.Data, utf8bom) {
t.Errorf("Byte Order Mark still present in processed file %s", file.Name)
}
assert.Falsef(t, bytes.HasPrefix(file.Data, utf8bom), "Byte Order Mark still present in processed file %s", file.Name)
}
}

@ -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)
}

@ -17,6 +17,9 @@ package v3
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestValidate(t *testing.T) {
@ -40,6 +43,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,21 +194,13 @@ func TestValidate(t *testing.T) {
for _, tt := range tests {
result := tt.md.Validate()
if result != tt.err {
t.Errorf("expected %q, got %q in test %q", tt.err, result, tt.name)
}
assert.ErrorIsf(t, result, tt.err, "expected %q, got %q in test %q", tt.err, result, tt.name)
}
}
func TestValidate_sanitize(t *testing.T) {
md := &Metadata{APIVersion: "3", Name: "test", Version: "1.0", Description: "\adescr\u0081iption\rtest", Maintainers: []*Maintainer{{Name: "\r"}}}
if err := md.Validate(); err != nil {
t.Fatalf("unexpected error: %s", err)
}
if md.Description != "description test" {
t.Fatalf("description was not sanitized: %q", md.Description)
}
if md.Maintainers[0].Name != " " {
t.Fatal("maintainer name was not sanitized")
}
require.NoError(t, md.Validate())
require.Equalf(t, "description test", md.Description, "description was not sanitized: %q", md.Description)
require.Equal(t, " ", md.Maintainers[0].Name, "maintainer name was not sanitized")
}

@ -58,7 +58,7 @@ func SaveChartfile(filename string, cf *chart.Metadata) error {
if err != nil {
return err
}
return os.WriteFile(filename, out, 0644)
return os.WriteFile(filename, out, 0o644)
}
// IsChartDir validate a chart directory.

@ -19,6 +19,9 @@ package util
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
chart "helm.sh/helm/v4/internal/chart/v3"
)
@ -26,92 +29,40 @@ const testfile = "testdata/chartfiletest.yaml"
func TestLoadChartfile(t *testing.T) {
f, err := LoadChartfile(testfile)
if err != nil {
t.Errorf("Failed to open %s: %s", testfile, err)
return
}
require.NoErrorf(t, err, "Failed to open %s", testfile)
verifyChartfile(t, f, "frobnitz")
}
func verifyChartfile(t *testing.T, f *chart.Metadata, name string) {
t.Helper()
if f == nil { //nolint:staticcheck
t.Fatal("Failed verifyChartfile because f is nil")
}
if f.Name != name {
t.Errorf("Expected %s, got %s", name, f.Name)
}
if f.Description != "This is a frobnitz." {
t.Errorf("Unexpected description %q", f.Description)
}
if f.Version != "1.2.3" {
t.Errorf("Unexpected version %q", f.Version)
}
if len(f.Maintainers) != 2 {
t.Errorf("Expected 2 maintainers, got %d", len(f.Maintainers))
}
if f.Maintainers[0].Name != "The Helm Team" {
t.Errorf("Unexpected maintainer name.")
}
if f.Maintainers[1].Email != "nobody@example.com" {
t.Errorf("Unexpected maintainer email.")
}
if len(f.Sources) != 1 {
t.Fatalf("Unexpected number of sources")
}
if f.Sources[0] != "https://example.com/foo/bar" {
t.Errorf("Expected https://example.com/foo/bar, got %s", f.Sources)
}
if f.Home != "http://example.com" {
t.Error("Unexpected home.")
}
if f.Icon != "https://example.com/64x64.png" {
t.Errorf("Unexpected icon: %q", f.Icon)
}
if len(f.Keywords) != 3 {
t.Error("Unexpected keywords")
}
if len(f.Annotations) != 2 {
t.Fatalf("Unexpected annotations")
}
if want, got := "extravalue", f.Annotations["extrakey"]; want != got {
t.Errorf("Want %q, but got %q", want, got)
}
if want, got := "anothervalue", f.Annotations["anotherkey"]; want != got {
t.Errorf("Want %q, but got %q", want, got)
}
require.NotNil(t, f, "Failed verifyChartfile because f is nil")
assert.Equal(t, name, f.Name, "Expected %s, got %s", name, f.Name)
assert.Equal(t, "This is a frobnitz.", f.Description, "Unexpected description %q", f.Description)
assert.Equal(t, "1.2.3", f.Version, "Unexpected version %q", f.Version)
assert.Len(t, f.Maintainers, 2, "Expected 2 maintainers, got %d", len(f.Maintainers))
assert.Equal(t, "The Helm Team", f.Maintainers[0].Name, "Unexpected maintainer name.")
assert.Equal(t, "nobody@example.com", f.Maintainers[1].Email, "Unexpected maintainer email.")
require.Len(t, f.Sources, 1, "Unexpected number of sources")
assert.Equal(t, "https://example.com/foo/bar", f.Sources[0], "Expected https://example.com/foo/bar, got %s", f.Sources)
assert.Equal(t, "http://example.com", f.Home, "Unexpected home.")
assert.Equal(t, "https://example.com/64x64.png", f.Icon, "Unexpected icon: %q", f.Icon)
require.Len(t, f.Keywords, 3, "Unexpected keywords")
require.Len(t, f.Annotations, 2, "Unexpected annotations")
assert.Equal(t, "extravalue", f.Annotations["extrakey"])
assert.Equal(t, "anothervalue", f.Annotations["anotherkey"])
kk := []string{"frobnitz", "sprocket", "dodad"}
for i, k := range f.Keywords {
if kk[i] != k {
t.Errorf("Expected %q, got %q", kk[i], k)
}
assert.Equal(t, kk[i], k, "Expected %q, got %q", kk[i], k)
}
}
func TestIsChartDir(t *testing.T) {
validChartDir, err := IsChartDir("testdata/frobnitz")
if !validChartDir {
t.Errorf("unexpected error while reading chart-directory: (%v)", err)
return
}
require.NoError(t, err, "while reading chart-directory")
require.True(t, validChartDir, "expected valid chart directory")
validChartDir, err = IsChartDir("testdata")
if validChartDir || err == nil {
t.Errorf("expected error but did not get any")
return
}
require.Error(t, err)
require.False(t, validChartDir, "expected invalid chart directory")
}

@ -17,7 +17,11 @@ limitations under the License.
// Package version represents the current version of the project.
package util
import "testing"
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsCompatibleRange(t *testing.T) {
tests := []struct {
@ -36,8 +40,6 @@ func TestIsCompatibleRange(t *testing.T) {
}
for _, tt := range tests {
if IsCompatibleRange(tt.constraint, tt.ver) != tt.expected {
t.Errorf("expected constraint %s to be %v for %s", tt.constraint, tt.expected, tt.ver)
}
assert.Equalf(t, tt.expected, IsCompatibleRange(tt.constraint, tt.ver), "expected constraint %s to be %v for %s", tt.constraint, tt.expected, tt.ver)
}
}

@ -18,7 +18,7 @@ package util
import (
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"regexp"
@ -642,12 +642,6 @@ spec:
restartPolicy: Never
`
// Stderr is an io.Writer to which error messages can be written
//
// In Helm 4, this will be replaced. It is needed in Helm 3 to preserve API backward
// compatibility.
var Stderr io.Writer = os.Stderr
// CreateFrom creates a new chart, but scaffolds it from the src chart.
func CreateFrom(chartfile *chart.Metadata, dest, src string) error {
schart, err := loader.Load(src)
@ -670,7 +664,7 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error {
return fmt.Errorf("reading values file: %w", err)
}
var m map[string]interface{}
var m map[string]any
if err := yaml.Unmarshal(transform(string(b), schart.Name()), &m); err != nil {
return fmt.Errorf("transforming values file: %w", err)
}
@ -702,7 +696,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
@ -796,15 +789,14 @@ func Create(name, dir string) (string, error) {
for _, file := range files {
if _, err := os.Stat(file.path); err == nil {
// There is no handle to a preferred output stream here.
fmt.Fprintf(Stderr, "WARNING: File %q already exists. Overwriting.\n", file.path)
slog.Warn("Overwriting existing file", slog.String("file", file.path))
}
if err := writeFile(file.path, file.content); err != nil {
return cdir, err
}
}
// Need to add the ChartsDir explicitly as it does not contain any file OOTB
if err := os.MkdirAll(filepath.Join(cdir, ChartsDir), 0755); err != nil {
if err := os.MkdirAll(filepath.Join(cdir, ChartsDir), 0o755); err != nil {
return cdir, err
}
return cdir, nil
@ -817,10 +809,10 @@ func transform(src, replacement string) []byte {
}
func writeFile(name string, content []byte) error {
if err := os.MkdirAll(filepath.Dir(name), 0755); err != nil {
if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil {
return err
}
return os.WriteFile(name, content, 0644)
return os.WriteFile(name, content, 0o644)
}
func validateChartName(name string) error {

@ -18,10 +18,14 @@ package util
import (
"bytes"
"log/slog"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/internal/chart/v3/loader"
)
@ -30,20 +34,14 @@ func TestCreate(t *testing.T) {
tdir := t.TempDir()
c, err := Create("foo", tdir)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
dir := filepath.Join(tdir, "foo")
mychart, err := loader.LoadDir(c)
if err != nil {
t.Fatalf("Failed to load newly created chart %q: %s", c, err)
}
require.NoError(t, err, "Failed to load newly created chart %q", c)
if mychart.Name() != "foo" {
t.Errorf("Expected name to be 'foo', got %q", mychart.Name())
}
assert.Equal(t, "foo", mychart.Name(), "Expected name to be 'foo', got %q", mychart.Name())
for _, f := range []string{
ChartfileName,
@ -58,9 +56,8 @@ func TestCreate(t *testing.T) {
TestConnectionName,
ValuesfileName,
} {
if _, err := os.Stat(filepath.Join(dir, f)); err != nil {
t.Errorf("Expected %s file: %s", f, err)
}
_, err := os.Stat(filepath.Join(dir, f))
assert.NoErrorf(t, err, "Expected %s file", f)
}
}
@ -74,38 +71,27 @@ func TestCreateFrom(t *testing.T) {
}
srcdir := "./testdata/frobnitz/charts/mariner"
if err := CreateFrom(cf, tdir, srcdir); err != nil {
t.Fatal(err)
}
require.NoError(t, CreateFrom(cf, tdir, srcdir))
dir := filepath.Join(tdir, "foo")
c := filepath.Join(tdir, cf.Name)
mychart, err := loader.LoadDir(c)
if err != nil {
t.Fatalf("Failed to load newly created chart %q: %s", c, err)
}
require.NoError(t, err, "Failed to load newly created chart %q", c)
if mychart.Name() != "foo" {
t.Errorf("Expected name to be 'foo', got %q", mychart.Name())
}
assert.Equal(t, "foo", mychart.Name(), "Expected name to be 'foo', got %q", mychart.Name())
for _, f := range []string{
ChartfileName,
ValuesfileName,
filepath.Join(TemplatesDir, "placeholder.tpl"),
} {
if _, err := os.Stat(filepath.Join(dir, f)); err != nil {
t.Errorf("Expected %s file: %s", f, err)
}
_, err := os.Stat(filepath.Join(dir, f))
require.NoErrorf(t, err, "Expected %s file", f)
// Check each file to make sure <CHARTNAME> has been replaced
b, err := os.ReadFile(filepath.Join(dir, f))
if err != nil {
t.Errorf("Unable to read file %s: %s", f, err)
}
if bytes.Contains(b, []byte("<CHARTNAME>")) {
t.Errorf("File %s contains <CHARTNAME>", f)
}
require.NoError(t, err, "Unable to read file %s", f)
assert.Falsef(t, bytes.Contains(b, []byte("<CHARTNAME>")), "File %s contains <CHARTNAME>", f)
}
}
@ -113,35 +99,30 @@ func TestCreateFrom(t *testing.T) {
func TestCreate_Overwrite(t *testing.T) {
tdir := t.TempDir()
var errlog bytes.Buffer
prev := slog.Default()
t.Cleanup(func() { slog.SetDefault(prev) })
if _, err := Create("foo", tdir); err != nil {
t.Fatal(err)
}
var logBuf bytes.Buffer
slog.SetDefault(slog.New(slog.NewJSONHandler(&logBuf, nil)))
_, err := Create("foo", tdir)
require.NoError(t, err)
dir := filepath.Join(tdir, "foo")
tplname := filepath.Join(dir, "templates/hpa.yaml")
tplname := filepath.Join(dir, "templates", "hpa.yaml")
writeFile(tplname, []byte("FOO"))
// Now re-run the create
Stderr = &errlog
if _, err := Create("foo", tdir); err != nil {
t.Fatal(err)
}
_, err = Create("foo", tdir)
require.NoError(t, err)
data, err := os.ReadFile(tplname)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
if string(data) == "FOO" {
t.Fatal("File that should have been modified was not.")
}
require.NotEqual(t, "FOO", string(data), "File that should have been modified was not.")
if errlog.Len() == 0 {
t.Errorf("Expected warnings about overwriting files.")
}
assert.NotEqual(t, 0, logBuf.Len(), "Expected warnings about overwriting files.")
}
func TestValidateChartName(t *testing.T) {
@ -165,8 +146,11 @@ func TestValidateChartName(t *testing.T) {
"abcdefghijklmnopqrstuvwxyz-_." +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ-_.": false,
} {
if err := validateChartName(name); (err != nil) == shouldPass {
t.Errorf("test for %q failed", name)
err := validateChartName(name)
if shouldPass {
assert.NoErrorf(t, err, "Expected chart name %q to pass validation", name)
} else {
assert.Errorf(t, err, "Expected chart name %q to fail validation, but it passed", name)
}
}
}

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

@ -21,6 +21,9 @@ import (
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/internal/chart/v3/loader"
"helm.sh/helm/v4/pkg/chart/common"
@ -29,9 +32,7 @@ import (
func loadChart(t *testing.T, path string) *chart.Chart {
t.Helper()
c, err := loader.Load(path)
if err != nil {
t.Fatalf("failed to load testdata: %s", err)
}
require.NoError(t, err, "failed to load testdata")
return c
}
@ -42,19 +43,11 @@ func TestLoadDependency(t *testing.T) {
}
check := func(deps []*chart.Dependency) {
if len(deps) != 2 {
t.Errorf("expected 2 dependencies, got %d", len(deps))
}
require.Len(t, deps, 2, "expected 2 dependencies")
for i, tt := range tests {
if deps[i].Name != tt.Name {
t.Errorf("expected dependency named %q, got %q", tt.Name, deps[i].Name)
}
if deps[i].Version != tt.Version {
t.Errorf("expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, deps[i].Version)
}
if deps[i].Repository != tt.Repository {
t.Errorf("expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, deps[i].Repository)
}
assert.Equal(t, tt.Name, deps[i].Name, "expected dependency named %q, got %q", tt.Name, deps[i].Name)
assert.Equal(t, tt.Version, deps[i].Version, "expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, deps[i].Version)
assert.Equal(t, tt.Repository, deps[i].Repository, "expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, deps[i].Repository)
}
}
c := loadChart(t, "testdata/frobnitz")
@ -63,7 +56,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
@ -117,18 +110,12 @@ func TestDependencyEnabled(t *testing.T) {
for _, tc := range tests {
c := loadChart(t, "testdata/subpop")
t.Run(tc.name, func(t *testing.T) {
if err := processDependencyEnabled(c, tc.v, ""); err != nil {
t.Fatalf("error processing enabled dependencies %v", err)
}
require.NoErrorf(t, processDependencyEnabled(c, tc.v, ""), "error processing enabled dependencies")
names := extractChartNames(c)
if len(names) != len(tc.e) {
t.Fatalf("slice lengths do not match got %v, expected %v", len(names), len(tc.e))
}
require.Len(t, names, len(tc.e), "slice lengths do not match got %v, expected %v", len(names), len(tc.e))
for i := range names {
if names[i] != tc.e[i] {
t.Fatalf("slice values do not match got %v, expected %v", names, tc.e)
}
require.Equal(t, tc.e[i], names[i], "slice values do not match got %v, expected %v", names, tc.e)
}
})
}
@ -219,68 +206,44 @@ func TestProcessDependencyImportValues(t *testing.T) {
e["SCBexported2A"] = "blaster"
e["global.SC1exported2.all.SC1exported3"] = "SC1expstr"
if err := processDependencyImportValues(c, false); err != nil {
t.Fatalf("processing import values dependencies %v", err)
}
require.NoError(t, processDependencyImportValues(c, false), "processing import values dependencies")
cc := common.Values(c.Values)
for kk, vv := range e {
pv, err := cc.PathValue(kk)
if err != nil {
t.Fatalf("retrieving import values table %v %v", kk, err)
}
require.NoError(t, err, "retrieving import values table %v", kk)
switch pv := pv.(type) {
case float64:
if s := strconv.FormatFloat(pv, 'f', -1, 64); s != vv {
t.Errorf("failed to match imported float value %v with expected %v for key %q", s, vv, kk)
}
s := strconv.FormatFloat(pv, 'f', -1, 64)
assert.Equalf(t, vv, s, "failed to match imported float value %v with expected %v for key %q", s, vv, kk)
case bool:
if b := strconv.FormatBool(pv); b != vv {
t.Errorf("failed to match imported bool value %v with expected %v for key %q", b, vv, kk)
}
b := strconv.FormatBool(pv)
assert.Equalf(t, vv, b, "failed to match imported bool value %v with expected %v for key %q", b, vv, kk)
default:
if pv != vv {
t.Errorf("failed to match imported string value %q with expected %q for key %q", pv, vv, kk)
}
assert.Equalf(t, vv, pv, "failed to match imported string value %q with expected %q for key %q", pv, vv, kk)
}
}
// Since this was processed with coalescing there should be no null values.
// Here we verify that.
_, err := cc.PathValue("ensurenull")
if err == nil {
t.Error("expect nil value not found but found it")
}
switch xerr := err.(type) {
case common.ErrNoValue:
// We found what we expected
default:
t.Errorf("expected an ErrNoValue but got %q instead", xerr)
}
require.Error(t, err, "expect nil value not found but found it")
var xerr common.ErrNoValue
require.ErrorAs(t, err, &xerr, "expected an ErrNoValue")
c = loadChart(t, "testdata/subpop")
if err := processDependencyImportValues(c, true); err != nil {
t.Fatalf("processing import values dependencies %v", err)
}
require.NoError(t, processDependencyImportValues(c, true), "processing import values dependencies")
cc = common.Values(c.Values)
val, err := cc.PathValue("ensurenull")
if err != nil {
t.Error("expect value but ensurenull was not found")
}
if val != nil {
t.Errorf("expect nil value but got %q instead", val)
}
require.NoError(t, err, "expect value but ensurenull was not found")
assert.Nil(t, val, "expect nil value but got %v instead", val)
}
func TestProcessDependencyImportValuesFromSharedDependencyToAliases(t *testing.T) {
c := loadChart(t, "testdata/chart-with-import-from-aliased-dependencies")
if err := processDependencyEnabled(c, c.Values, ""); err != nil {
t.Fatalf("expected no errors but got %q", err)
}
if err := processDependencyImportValues(c, true); err != nil {
t.Fatalf("processing import values dependencies %v", err)
}
require.NoError(t, processDependencyEnabled(c, c.Values, ""), "expected no errors")
require.NoError(t, processDependencyImportValues(c, true), "processing import values dependencies")
e := make(map[string]string)
e["foo-defaults.defaultValue"] = "42"
@ -295,12 +258,8 @@ func TestProcessDependencyImportValuesFromSharedDependencyToAliases(t *testing.T
cValues := common.Values(c.Values)
for kk, vv := range e {
pv, err := cValues.PathValue(kk)
if err != nil {
t.Fatalf("retrieving import values table %v %v", kk, err)
}
if pv != vv {
t.Errorf("failed to match imported value %v with expected %v", pv, vv)
}
require.NoError(t, err, "retrieving import values table %v", kk)
assert.Equal(t, vv, pv, "failed to match imported value %v with expected %v", pv, vv)
}
}
@ -327,25 +286,18 @@ func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) {
e["app2.service.port"] = "8080"
e["app3.service.port"] = "9090"
e["app4.service.port"] = "1234"
if err := processDependencyImportValues(c, true); err != nil {
t.Fatalf("processing import values dependencies %v", err)
}
require.NoError(t, processDependencyImportValues(c, true), "processing import values dependencies")
cc := common.Values(c.Values)
for kk, vv := range e {
pv, err := cc.PathValue(kk)
if err != nil {
t.Fatalf("retrieving import values table %v %v", kk, err)
}
require.NoError(t, err, "retrieving import values table %v", kk)
switch pv := pv.(type) {
case float64:
if s := strconv.FormatFloat(pv, 'f', -1, 64); s != vv {
t.Errorf("failed to match imported float value %v with expected %v", s, vv)
}
s := strconv.FormatFloat(pv, 'f', -1, 64)
assert.Equalf(t, vv, s, "failed to match imported float value %v with expected %v", s, vv)
default:
if pv != vv {
t.Errorf("failed to match imported string value %q with expected %q", pv, vv)
}
assert.Equalf(t, vv, pv, "failed to match imported string value %q with expected %q", pv, vv)
}
}
}
@ -354,128 +306,71 @@ func TestProcessDependencyImportValuesForEnabledCharts(t *testing.T) {
c := loadChart(t, "testdata/import-values-from-enabled-subchart/parent-chart")
nameOverride := "parent-chart-prod"
if err := processDependencyImportValues(c, true); err != nil {
t.Fatalf("processing import values dependencies %v", err)
}
if len(c.Dependencies()) != 2 {
t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
}
if err := processDependencyEnabled(c, c.Values, ""); err != nil {
t.Fatalf("expected no errors but got %q", err)
}
if len(c.Dependencies()) != 1 {
t.Fatal("expected no changes in dependencies")
}
if len(c.Metadata.Dependencies) != 1 {
t.Fatalf("expected 1 dependency specified in Chart.yaml, got %d", len(c.Metadata.Dependencies))
}
require.NoError(t, processDependencyImportValues(c, true), "processing import values dependencies")
require.Len(t, c.Dependencies(), 2, "expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
require.NoError(t, processDependencyEnabled(c, c.Values, ""), "expected no errors")
require.Len(t, c.Dependencies(), 1, "expected no changes in dependencies")
require.Len(t, c.Metadata.Dependencies, 1, "expected 1 dependency specified in Chart.yaml, got %d", len(c.Metadata.Dependencies))
prodDependencyValues := c.Dependencies()[0].Values
if prodDependencyValues["nameOverride"] != nameOverride {
t.Fatalf("dependency chart name should be %s but got %s", nameOverride, prodDependencyValues["nameOverride"])
}
require.Equal(t, nameOverride, prodDependencyValues["nameOverride"], "dependency chart name should be %s but got %s", nameOverride, prodDependencyValues["nameOverride"])
}
func TestGetAliasDependency(t *testing.T) {
c := loadChart(t, "testdata/frobnitz")
req := c.Metadata.Dependencies
if len(req) == 0 {
t.Fatalf("there are no dependencies to test")
}
require.NotEmpty(t, req, "there are no dependencies to test")
// Success case
aliasChart := getAliasDependency(c.Dependencies(), req[0])
if aliasChart == nil {
t.Fatalf("failed to get dependency chart for alias %s", req[0].Name)
}
require.NotNil(t, aliasChart, "failed to get dependency chart for alias %s", req[0].Name)
if req[0].Alias != "" {
if aliasChart.Name() != req[0].Alias {
t.Fatalf("dependency chart name should be %s but got %s", req[0].Alias, aliasChart.Name())
}
} else if aliasChart.Name() != req[0].Name {
t.Fatalf("dependency chart name should be %s but got %s", req[0].Name, aliasChart.Name())
require.Equal(t, req[0].Alias, aliasChart.Name(), "dependency chart name should be %s but got %s", req[0].Alias, aliasChart.Name())
} else {
require.Equal(t, req[0].Name, aliasChart.Name(), "dependency chart name should be %s but got %s", req[0].Name, aliasChart.Name())
}
if req[0].Version != "" {
if !IsCompatibleRange(req[0].Version, aliasChart.Metadata.Version) {
t.Fatalf("dependency chart version is not in the compatible range")
}
require.True(t, IsCompatibleRange(req[0].Version, aliasChart.Metadata.Version), "dependency chart version is not in the compatible range")
}
// Failure case
req[0].Name = "something-else"
if aliasChart := getAliasDependency(c.Dependencies(), req[0]); aliasChart != nil {
t.Fatalf("expected no chart but got %s", aliasChart.Name())
}
require.Nil(t, getAliasDependency(c.Dependencies(), req[0]), "expected no chart")
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 ")
}
require.False(t, IsCompatibleRange(req[0].Version, aliasChart.Metadata.Version), "dependency chart version outside the compatible range should not be considered compatible")
}
func TestDependentChartAliases(t *testing.T) {
c := loadChart(t, "testdata/dependent-chart-alias")
req := c.Metadata.Dependencies
if len(c.Dependencies()) != 2 {
t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
}
if err := processDependencyEnabled(c, c.Values, ""); err != nil {
t.Fatalf("expected no errors but got %q", err)
}
if len(c.Dependencies()) != 3 {
t.Fatal("expected alias dependencies to be added")
}
if len(c.Dependencies()) != len(c.Metadata.Dependencies) {
t.Fatalf("expected number of chart dependencies %d, but got %d", len(c.Metadata.Dependencies), len(c.Dependencies()))
}
require.Len(t, c.Dependencies(), 2, "expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
require.NoError(t, processDependencyEnabled(c, c.Values, ""), "expected no errors")
require.Len(t, c.Dependencies(), 3, "expected alias dependencies to be added")
require.Len(t, c.Dependencies(), len(c.Metadata.Dependencies), "expected number of chart dependencies %d, but got %d", len(c.Metadata.Dependencies), len(c.Dependencies()))
aliasChart := getAliasDependency(c.Dependencies(), req[2])
if aliasChart == nil {
t.Fatalf("failed to get dependency chart for alias %s", req[2].Name)
}
if aliasChart.Parent() != c {
t.Fatalf("dependency chart has wrong parent, expected %s but got %s", c.Name(), aliasChart.Parent().Name())
}
require.NotNil(t, aliasChart, "failed to get dependency chart for alias %s", req[2].Name)
require.Equal(t, c, aliasChart.Parent(), "dependency chart has wrong parent, expected %s but got %s", c.Name(), aliasChart.Parent().Name())
if req[2].Alias != "" {
if aliasChart.Name() != req[2].Alias {
t.Fatalf("dependency chart name should be %s but got %s", req[2].Alias, aliasChart.Name())
}
} else if aliasChart.Name() != req[2].Name {
t.Fatalf("dependency chart name should be %s but got %s", req[2].Name, aliasChart.Name())
require.Equal(t, req[2].Alias, aliasChart.Name(), "dependency chart name should be %s but got %s", req[2].Alias, aliasChart.Name())
} else {
require.Equal(t, req[2].Name, aliasChart.Name(), "dependency chart name should be %s but got %s", req[2].Name, aliasChart.Name())
}
req[2].Name = "dummy-name"
if aliasChart := getAliasDependency(c.Dependencies(), req[2]); aliasChart != nil {
t.Fatalf("expected no chart but got %s", aliasChart.Name())
}
require.Nil(t, getAliasDependency(c.Dependencies(), req[2]), "expected no chart")
}
func TestDependentChartWithSubChartsAbsentInDependency(t *testing.T) {
c := loadChart(t, "testdata/dependent-chart-no-requirements-yaml")
if len(c.Dependencies()) != 2 {
t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
}
if err := processDependencyEnabled(c, c.Values, ""); err != nil {
t.Fatalf("expected no errors but got %q", err)
}
require.Len(t, c.Dependencies(), 2, "expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
if len(c.Dependencies()) != 2 {
t.Fatal("expected no changes in dependencies")
}
require.NoError(t, processDependencyEnabled(c, c.Values, ""), "expected no errors")
require.Len(t, c.Dependencies(), 2, "expected no changes in dependencies")
}
func TestDependentChartWithSubChartsHelmignore(t *testing.T) {
@ -485,86 +380,48 @@ func TestDependentChartWithSubChartsHelmignore(t *testing.T) {
func TestDependentChartsWithSubChartsSymlink(t *testing.T) {
joonix := filepath.Join("testdata", "joonix")
if err := os.Symlink(filepath.Join("..", "..", "frobnitz"), filepath.Join(joonix, "charts", "frobnitz")); err != nil {
t.Fatal(err)
}
require.NoError(t, os.Symlink(filepath.Join("..", "..", "frobnitz"), filepath.Join(joonix, "charts", "frobnitz")), "failed to create symlink")
defer os.RemoveAll(filepath.Join(joonix, "charts", "frobnitz"))
c := loadChart(t, joonix)
if c.Name() != "joonix" {
t.Fatalf("unexpected chart name: %s", c.Name())
}
if n := len(c.Dependencies()); n != 1 {
t.Fatalf("expected 1 dependency for this chart, but got %d", n)
}
require.Equal(t, "joonix", c.Name(), "unexpected chart name: %s", c.Name())
require.Len(t, c.Dependencies(), 1, "expected 1 dependency for this chart")
}
func TestDependentChartsWithSubchartsAllSpecifiedInDependency(t *testing.T) {
c := loadChart(t, "testdata/dependent-chart-with-all-in-requirements-yaml")
if len(c.Dependencies()) != 2 {
t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
}
if err := processDependencyEnabled(c, c.Values, ""); err != nil {
t.Fatalf("expected no errors but got %q", err)
}
if len(c.Dependencies()) != 2 {
t.Fatal("expected no changes in dependencies")
}
if len(c.Dependencies()) != len(c.Metadata.Dependencies) {
t.Fatalf("expected number of chart dependencies %d, but got %d", len(c.Metadata.Dependencies), len(c.Dependencies()))
}
require.Len(t, c.Dependencies(), 2, "expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
require.NoError(t, processDependencyEnabled(c, c.Values, ""), "expected no errors")
require.Len(t, c.Dependencies(), 2, "expected no changes in dependencies")
require.Len(t, c.Dependencies(), len(c.Metadata.Dependencies), "expected number of chart dependencies %d, but got %d", len(c.Metadata.Dependencies), len(c.Dependencies()))
}
func TestDependentChartsWithSomeSubchartsSpecifiedInDependency(t *testing.T) {
c := loadChart(t, "testdata/dependent-chart-with-mixed-requirements-yaml")
if len(c.Dependencies()) != 2 {
t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
}
if err := processDependencyEnabled(c, c.Values, ""); err != nil {
t.Fatalf("expected no errors but got %q", err)
}
if len(c.Dependencies()) != 2 {
t.Fatal("expected no changes in dependencies")
}
if len(c.Metadata.Dependencies) != 1 {
t.Fatalf("expected 1 dependency specified in Chart.yaml, got %d", len(c.Metadata.Dependencies))
}
require.Len(t, c.Dependencies(), 2, "expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
require.NoError(t, processDependencyEnabled(c, c.Values, ""), "expected no errors")
require.Len(t, c.Dependencies(), 2, "expected no changes in dependencies")
require.Len(t, c.Metadata.Dependencies, 1, "expected 1 dependency specified in Chart.yaml, got %d", len(c.Metadata.Dependencies))
}
func validateDependencyTree(t *testing.T, c *chart.Chart) {
t.Helper()
for _, dependency := range c.Dependencies() {
if dependency.Parent() != c {
if dependency.Parent() != c {
t.Fatalf("dependency chart %s has wrong parent, expected %s but got %s", dependency.Name(), c.Name(), dependency.Parent().Name())
}
}
// recurse entire tree
validateDependencyTree(t, dependency)
t.Run(dependency.Name(), func(t *testing.T) {
require.Equal(t, c, dependency.Parent(), "dependency chart %s has wrong parent, expected %s but got %s", dependency.Name(), c.Name(), dependency.Parent().Name())
// recurse entire tree
validateDependencyTree(t, dependency)
})
}
}
func TestChartWithDependencyAliasedTwiceAndDoublyReferencedSubDependency(t *testing.T) {
c := loadChart(t, "testdata/chart-with-dependency-aliased-twice")
if len(c.Dependencies()) != 1 {
t.Fatalf("expected one dependency for this chart, but got %d", len(c.Dependencies()))
}
if err := processDependencyEnabled(c, c.Values, ""); err != nil {
t.Fatalf("expected no errors but got %q", err)
}
if len(c.Dependencies()) != 2 {
t.Fatal("expected two dependencies after processing aliases")
}
require.Len(t, c.Dependencies(), 1, "expected one dependency for this chart, but got %d", len(c.Dependencies()))
require.NoError(t, processDependencyEnabled(c, c.Values, ""), "expected no errors")
require.Len(t, c.Dependencies(), 2, "expected two dependencies after processing aliases")
validateDependencyTree(t, c)
}

@ -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

@ -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 {
@ -71,11 +88,11 @@ func Expand(dir string, r io.Reader) error {
// Make sure the necessary subdirs get created.
basedir := filepath.Dir(outpath)
if err := os.MkdirAll(basedir, 0755); err != nil {
if err := os.MkdirAll(basedir, 0o755); err != nil {
return err
}
if err := os.WriteFile(outpath, file.Data, 0644); err != nil {
if err := os.WriteFile(outpath, file.Data, 0o644); err != nil {
return err
}
}

@ -17,108 +17,154 @@ 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)
require.NoError(t, tw.WriteHeader(&tar.Header{
Name: filepath.Join(chartName, relPath),
Mode: int64(fStat.Mode()),
Size: fStat.Size(),
}))
data, err := fs.ReadFile(dir, relPath)
require.NoError(t, err)
_, err = tw.Write(data)
require.NoError(t, err)
}
require.NoError(t, 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
}))
require.NoError(t, tw.Close())
require.NoError(t, gw.Close())
return &result
}
func TestExpand(t *testing.T) {
dest := t.TempDir()
reader, err := os.Open("testdata/frobnitz-1.2.3.tgz")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
t.Cleanup(func() { assert.NoError(t, reader.Close()) })
if err := Expand(dest, reader); err != nil {
t.Fatal(err)
}
require.NoError(t, Expand(dest, reader))
expectedChartPath := filepath.Join(dest, "frobnitz")
fi, err := os.Stat(expectedChartPath)
if err != nil {
t.Fatal(err)
}
if !fi.IsDir() {
t.Fatalf("expected a chart directory at %s", expectedChartPath)
}
require.NoError(t, err)
require.Truef(t, fi.IsDir(), "expected a chart directory at %s", expectedChartPath)
dir, err := os.Open(expectedChartPath)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
t.Cleanup(func() { assert.NoError(t, dir.Close()) })
fis, err := dir.Readdir(0)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
expectLen := 11
if len(fis) != expectLen {
t.Errorf("Expected %d files, but got %d", expectLen, len(fis))
}
assert.Len(t, fis, expectLen, "Expected %d files", expectLen)
for _, fi := range fis {
expect, err := os.Stat(filepath.Join("testdata", "frobnitz", fi.Name()))
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
// os.Stat can return different values for directories, based on the OS
// for Linux, for example, os.Stat always returns the size of the directory
// (value-4096) regardless of the size of the contents of the directory
mode := expect.Mode()
if !mode.IsDir() {
if fi.Size() != expect.Size() {
t.Errorf("Expected %s to have size %d, got %d", fi.Name(), expect.Size(), fi.Size())
}
assert.Equal(t, expect.Size(), fi.Size(), "Expected %s to have size %d, got %d", fi.Name(), expect.Size(), fi.Size())
}
}
}
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()
assert.ErrorContains(t, Expand(dest, archive), tt.wantErr)
})
}
}
func TestExpandFile(t *testing.T) {
dest := t.TempDir()
if err := ExpandFile(dest, "testdata/frobnitz-1.2.3.tgz"); err != nil {
t.Fatal(err)
}
require.NoError(t, ExpandFile(dest, "testdata/frobnitz-1.2.3.tgz"))
expectedChartPath := filepath.Join(dest, "frobnitz")
fi, err := os.Stat(expectedChartPath)
if err != nil {
t.Fatal(err)
}
if !fi.IsDir() {
t.Fatalf("expected a chart directory at %s", expectedChartPath)
}
require.NoError(t, err)
require.Truef(t, fi.IsDir(), "expected a chart directory at %s", expectedChartPath)
dir, err := os.Open(expectedChartPath)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
fis, err := dir.Readdir(0)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
expectLen := 11
if len(fis) != expectLen {
t.Errorf("Expected %d files, but got %d", expectLen, len(fis))
}
assert.Len(t, fis, expectLen, "Expected %d files", expectLen)
for _, fi := range fis {
expect, err := os.Stat(filepath.Join("testdata", "frobnitz", fi.Name()))
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
// os.Stat can return different values for directories, based on the OS
// for Linux, for example, os.Stat always returns the size of the directory
// (value-4096) regardless of the size of the contents of the directory
mode := expect.Mode()
if !mode.IsDir() {
if fi.Size() != expect.Size() {
t.Errorf("Expected %s to have size %d, got %d", fi.Name(), expect.Size(), fi.Size())
}
assert.Equal(t, expect.Size(), fi.Size(), "Expected %s to have size %d, got %d", fi.Name(), expect.Size(), fi.Size())
}
}
}

@ -49,7 +49,7 @@ func SaveDir(c *chart.Chart, dest string) error {
if fi, err := os.Stat(outdir); err == nil && !fi.IsDir() {
return fmt.Errorf("file %s already exists and is not a directory", outdir)
}
if err := os.MkdirAll(outdir, 0755); err != nil {
if err := os.MkdirAll(outdir, 0o755); err != nil {
return err
}
@ -114,13 +114,12 @@ func Save(c *chart.Chart, outDir string) (string, error) {
filename = filepath.Join(outDir, filename)
dir := filepath.Dir(filename)
if stat, err := os.Stat(dir); err != nil {
if errors.Is(err, fs.ErrNotExist) {
if err2 := os.MkdirAll(dir, 0755); err2 != nil {
return "", err2
}
} else {
if !errors.Is(err, fs.ErrNotExist) {
return "", fmt.Errorf("stat %s: %w", dir, err)
}
if err2 := os.MkdirAll(dir, 0o755); err2 != nil {
return "", err2
}
} else if !stat.IsDir() {
return "", fmt.Errorf("is not a directory: %s", dir)
}
@ -230,7 +229,7 @@ func writeToTar(out *tar.Writer, name string, body []byte, modTime time.Time) er
// TODO: Do we need to create dummy parent directory names if none exist?
h := &tar.Header{
Name: filepath.ToSlash(name),
Mode: 0644,
Mode: 0o644,
Size: int64(len(body)),
ModTime: modTime,
}

@ -21,7 +21,8 @@ import (
"bytes"
"compress/gzip"
"crypto/sha256"
"fmt"
"encoding/hex"
"errors"
"io"
"os"
"path"
@ -31,6 +32,9 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/internal/chart/v3/loader"
"helm.sh/helm/v4/pkg/chart/common"
@ -58,26 +62,15 @@ func TestSave(t *testing.T) {
chartWithInvalidJSON := withSchema(*c, []byte("{"))
where, err := Save(c, dest)
if err != nil {
t.Fatalf("Failed to save: %s", err)
}
if !strings.HasPrefix(where, dest) {
t.Fatalf("Expected %q to start with %q", where, dest)
}
if !strings.HasSuffix(where, ".tgz") {
t.Fatalf("Expected %q to end with .tgz", where)
}
require.NoError(t, err, "Failed to save")
require.Truef(t, strings.HasPrefix(where, dest), "Expected %q to start with %q", where, dest)
require.Truef(t, strings.HasSuffix(where, ".tgz"), "Expected %q to end with .tgz", where)
c2, err := loader.LoadFile(where)
if err != nil {
t.Fatal(err)
}
if c2.Name() != c.Name() {
t.Fatalf("Expected chart archive to have %q, got %q", c.Name(), c2.Name())
}
if len(c2.Files) != 1 || c2.Files[0].Name != "scheherazade/shahryar.txt" {
t.Fatal("Files data did not match")
}
require.NoError(t, err)
require.Equal(t, c.Name(), c2.Name(), "Expected chart archive to have %q, got %q", c.Name(), c2.Name())
require.Len(t, c2.Files, 1, "Files data did not match")
require.Equal(t, "scheherazade/shahryar.txt", c2.Files[0].Name, "Files data did not match")
if !bytes.Equal(c.Schema, c2.Schema) {
indentation := 4
@ -85,25 +78,16 @@ func TestSave(t *testing.T) {
formattedActual := Indent(indentation, string(c2.Schema))
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")
}
_, err = Save(&chartWithInvalidJSON, dest)
require.Error(t, err, "Invalid JSON was not caught while saving chart")
c.Metadata.APIVersion = chart.APIVersionV3
where, err = Save(c, dest)
if err != nil {
t.Fatalf("Failed to save: %s", err)
}
require.NoError(t, err, "Failed to save")
c2, err = loader.LoadFile(where)
if err != nil {
t.Fatal(err)
}
if c2.Lock == nil {
t.Fatal("Expected v3 chart archive to contain a Chart.lock file")
}
if c2.Lock.Digest != c.Lock.Digest {
t.Fatal("Chart.lock data did not match")
}
require.NoError(t, err)
require.NotNil(t, c2.Lock, "Expected v3 chart archive to contain a Chart.lock file")
require.Equal(t, c.Lock.Digest, c2.Lock.Digest, "Chart.lock data did not match")
})
}
@ -121,9 +105,7 @@ func TestSave(t *testing.T) {
},
}
_, err := Save(c, tmp)
if err == nil {
t.Fatal("Expected error saving chart with invalid name")
}
require.Error(t, err, "Expected error saving chart with invalid name")
}
// Creates a copy with a different schema; does not modify anything.
@ -152,7 +134,7 @@ func TestSavePreservesTimestamps(t *testing.T) {
Version: "1.2.3",
},
ModTime: initialCreateTime,
Values: map[string]interface{}{
Values: map[string]any{
"imageName": "testimage",
"imageId": 42,
},
@ -164,20 +146,14 @@ func TestSavePreservesTimestamps(t *testing.T) {
}
where, err := Save(c, tmp)
if err != nil {
t.Fatalf("Failed to save: %s", err)
}
require.NoError(t, err, "Failed to save")
allHeaders, err := retrieveAllHeadersFromTar(where)
if err != nil {
t.Fatalf("Failed to parse tar: %v", err)
}
require.NoError(t, err, "Failed to parse tar")
roundedTime := initialCreateTime.Round(time.Second)
for _, header := range allHeaders {
if !header.ModTime.Equal(roundedTime) {
t.Fatalf("File timestamp not preserved: %v", header.ModTime)
}
require.Truef(t, header.ModTime.Equal(roundedTime), "File timestamp not preserved: %v", header.ModTime)
}
}
@ -201,7 +177,7 @@ func retrieveAllHeadersFromTar(path string) ([]*tar.Header, error) {
headers := []*tar.Header{}
for {
hd, err := tr.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
@ -233,47 +209,32 @@ func TestSaveDir(t *testing.T) {
},
}
if err := SaveDir(c, tmp); err != nil {
t.Fatalf("Failed to save: %s", err)
}
require.NoError(t, SaveDir(c, tmp), "Failed to save")
c2, err := loader.LoadDir(tmp + "/ahab")
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
if c2.Name() != c.Name() {
t.Fatalf("Expected chart archive to have %q, got %q", c.Name(), c2.Name())
}
require.Equal(t, c.Name(), c2.Name(), "Expected chart archive to have %q, got %q", c.Name(), c2.Name())
if len(c2.Templates) != 1 || c2.Templates[0].Name != c.Templates[0].Name {
t.Fatal("Templates data did not match")
}
require.Len(t, c2.Templates, 1)
require.Equal(t, c.Templates[0].Name, c2.Templates[0].Name, "Templates data did not match")
if len(c2.Files) != 1 || c2.Files[0].Name != c.Files[0].Name {
t.Fatal("Files data did not match")
}
require.Len(t, c2.Files, 1)
require.Equal(t, c.Files[0].Name, c2.Files[0].Name, "Files data did not match")
tmp2 := t.TempDir()
c.Metadata.Name = "../ahab"
pth := filepath.Join(tmp2, "tmpcharts")
if err := os.MkdirAll(filepath.Join(pth), 0755); err != nil {
t.Fatal(err)
}
require.NoError(t, os.MkdirAll(filepath.Join(pth), 0o755), "Failed to create directory")
if err := SaveDir(c, pth); err.Error() != "\"../ahab\" is not a valid chart name" {
t.Fatalf("Did not get expected error for chart named %q", c.Name())
}
assert.EqualError(t, SaveDir(c, pth), "\"../ahab\" is not a valid chart name", "Did not get expected error for chart named %q", c.Name())
}
func TestRepeatableSave(t *testing.T) {
tmp := t.TempDir()
defer os.RemoveAll(tmp)
modTime := time.Date(2021, 9, 1, 20, 34, 58, 651387237, time.UTC)
tests := []struct {
name string
chart *chart.Chart
want string
}{
{
name: "Package 1 file",
@ -294,7 +255,6 @@ func TestRepeatableSave(t *testing.T) {
Schema: []byte("{\n \"title\": \"Values\"\n}"),
SchemaModTime: modTime,
},
want: "5bfea18cc3c8cbc265744bc32bffa9489a4dbe87d6b51b90f4255e4839d35e03",
},
{
name: "Package 2 files",
@ -316,26 +276,26 @@ func TestRepeatableSave(t *testing.T) {
Schema: []byte("{\n \"title\": \"Values\"\n}"),
SchemaModTime: modTime,
},
want: "a240365c21e0a2f4a57873132a9b686566a612d08bcb3f20c9446bfff005ccce",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// create package
dest := path.Join(tmp, "newdir")
where, err := Save(test.chart, dest)
if err != nil {
t.Fatalf("Failed to save: %s", err)
}
// get shasum for package
result, err := sha256Sum(where)
if err != nil {
t.Fatalf("Failed to check shasum: %s", err)
}
// assert that the package SHA is what we wanted.
if result != test.want {
t.Errorf("FormatName() result = %v, want %v", result, test.want)
}
// Package the same chart twice, into separate directories so the
// second Save does not simply overwrite the first.
first, err := Save(test.chart, t.TempDir())
require.NoError(t, err, "Failed to save")
second, err := Save(test.chart, t.TempDir())
require.NoError(t, err, "Failed to save")
firstSum, err := sha256Sum(first)
require.NoError(t, err, "Failed to check shasum")
secondSum, err := sha256Sum(second)
require.NoError(t, err, "Failed to check shasum")
// Packaging a chart must be reproducible. The digest itself is not
// pinned here because the bytes depend on the compression
// implementation of the Go release Helm is built with.
assert.Equal(t, firstSum, secondSum, "Save() is not repeatable")
})
}
}
@ -352,5 +312,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

@ -1,111 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package util
import (
"errors"
"fmt"
"regexp"
)
// validName is a regular expression for resource names.
//
// According to the Kubernetes help text, the regular expression it uses is:
//
// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
//
// This follows the above regular expression (but requires a full string match, not partial).
//
// The Kubernetes documentation is here, though it is not entirely correct:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
var validName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
var (
// errMissingName indicates that a release (name) was not provided.
errMissingName = errors.New("no name provided")
// errInvalidName indicates that an invalid release name was provided
errInvalidName = fmt.Errorf(
"invalid release name, must match regex %s and the length must not be longer than 53",
validName.String())
// errInvalidKubernetesName indicates that the name does not meet the Kubernetes
// restrictions on metadata names.
errInvalidKubernetesName = fmt.Errorf(
"invalid metadata name, must match regex %s and the length must not be longer than 253",
validName.String())
)
const (
// According to the Kubernetes docs (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#rfc-1035-label-names)
// some resource names have a max length of 63 characters while others have a max
// length of 253 characters. As we cannot be sure the resources used in a chart, we
// therefore need to limit it to 63 chars and reserve 10 chars for additional part to name
// of the resource. The reason is that chart maintainers can use release name as part of
// the resource name (and some additional chars).
maxReleaseNameLen = 53
// maxMetadataNameLen is the maximum length Kubernetes allows for any name.
maxMetadataNameLen = 253
)
// ValidateReleaseName performs checks for an entry for a Helm release name
//
// For Helm to allow a name, it must be below a certain character count (53) and also match
// a regular expression.
//
// According to the Kubernetes help text, the regular expression it uses is:
//
// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
//
// This follows the above regular expression (but requires a full string match, not partial).
//
// The Kubernetes documentation is here, though it is not entirely correct:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
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
}
return nil
}
// ValidateMetadataName validates the name field of a Kubernetes metadata object.
//
// Empty strings, strings longer than 253 chars, or strings that don't match the regexp
// will fail.
//
// According to the Kubernetes help text, the regular expression it uses is:
//
// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
//
// This follows the above regular expression (but requires a full string match, not partial).
//
// The Kubernetes documentation is here, though it is not entirely correct:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
//
// Deprecated: remove in Helm 4. Name validation now uses rules defined in
// pkg/lint/rules.validateMetadataNameFunc()
func ValidateMetadataName(name string) error {
if name == "" || len(name) > maxMetadataNameLen || !validName.MatchString(name) {
return errInvalidKubernetesName
}
return nil
}

@ -1,91 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package util
import "testing"
// TestValidateReleaseName is a regression test for ValidateName
//
// Kubernetes has strict naming conventions for resource names. This test represents
// those conventions.
//
// See https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
//
// NOTE: At the time of this writing, the docs above say that names cannot begin with
// digits. However, `kubectl`'s regular expression explicit allows this, and
// Kubernetes (at least as of 1.18) also accepts resources whose names begin with digits.
func TestValidateReleaseName(t *testing.T) {
names := map[string]bool{
"": false,
"foo": true,
"foo.bar1234baz.seventyone": true,
"FOO": false,
"123baz": true,
"foo.BAR.baz": false,
"one-two": true,
"-two": false,
"one_two": false,
"a..b": false,
"%^&#$%*@^*@&#^": false,
"example:com": false,
"example%%com": false,
"a1111111111111111111111111111111111111111111111111111111111z": false,
}
for input, expectPass := range names {
if err := ValidateReleaseName(input); (err == nil) != expectPass {
st := "fail"
if expectPass {
st = "succeed"
}
t.Errorf("Expected %q to %s", input, st)
}
}
}
func TestValidateMetadataName(t *testing.T) {
names := map[string]bool{
"": false,
"foo": true,
"foo.bar1234baz.seventyone": true,
"FOO": false,
"123baz": true,
"foo.BAR.baz": false,
"one-two": true,
"-two": false,
"one_two": false,
"a..b": false,
"%^&#$%*@^*@&#^": false,
"example:com": false,
"example%%com": false,
"a1111111111111111111111111111111111111111111111111111111111z": true,
"a1111111111111111111111111111111111111111111111111111111111z" +
"a1111111111111111111111111111111111111111111111111111111111z" +
"a1111111111111111111111111111111111111111111111111111111111z" +
"a1111111111111111111111111111111111111111111111111111111111z" +
"a1111111111111111111111111111111111111111111111111111111111z" +
"a1111111111111111111111111111111111111111111111111111111111z": false,
}
for input, expectPass := range names {
if err := ValidateMetadataName(input); (err == nil) != expectPass {
st := "fail"
if expectPass {
st = "succeed"
}
t.Errorf("Expected %q to %s", input, st)
}
}
}

@ -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)
})
}
}

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

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

@ -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.
@ -32,31 +35,19 @@ func TestAtomicWriteFile(t *testing.T) {
testpath := filepath.Join(dir, "test")
stringContent := "Test content"
reader := bytes.NewReader([]byte(stringContent))
mode := os.FileMode(0644)
mode := os.FileMode(0o644)
err := AtomicWriteFile(testpath, reader, mode)
if err != nil {
t.Errorf("AtomicWriteFile error: %s", err)
}
require.NoError(t, AtomicWriteFile(testpath, reader, mode))
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
@ -64,12 +55,9 @@ func TestAtomicWriteFile_CreateTempError(t *testing.T) {
invalidPath := "/invalid/path/that/does/not/exist/testfile"
reader := bytes.NewReader([]byte("test content"))
mode := os.FileMode(0644)
mode := os.FileMode(0o644)
err := AtomicWriteFile(invalidPath, reader, mode)
if err == nil {
t.Error("Expected error when CreateTemp fails, but got nil")
}
assert.Error(t, AtomicWriteFile(invalidPath, reader, mode), "Expected error when CreateTemp fails")
}
// TestAtomicWriteFile_EmptyContent tests with empty content
@ -78,21 +66,14 @@ func TestAtomicWriteFile_EmptyContent(t *testing.T) {
testpath := filepath.Join(dir, "empty_helm")
reader := bytes.NewReader([]byte(""))
mode := os.FileMode(0644)
mode := os.FileMode(0o644)
err := AtomicWriteFile(testpath, reader, mode)
if err != nil {
t.Errorf("AtomicWriteFile error with empty content: %s", err)
}
require.NoError(t, AtomicWriteFile(testpath, reader, mode), "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
@ -103,19 +84,30 @@ func TestAtomicWriteFile_LargeContent(t *testing.T) {
// Create a large content string
largeContent := strings.Repeat("HELM", 1024*1024)
reader := bytes.NewReader([]byte(largeContent))
mode := os.FileMode(0644)
mode := os.FileMode(0o644)
err := AtomicWriteFile(testpath, reader, mode)
if err != nil {
t.Errorf("AtomicWriteFile error with large content: %s", err)
}
require.NoError(t, AtomicWriteFile(testpath, reader, mode), "AtomicWriteFile error with large content")
got, err := os.ReadFile(testpath)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
require.Equal(t, largeContent, string(got))
}
// TestPlatformAtomicWriteFile_OverwritesExisting verifies that the platform
// helper replaces existing files instead of silently skipping them.
func TestPlatformAtomicWriteFile_OverwritesExisting(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "overwrite_test")
first := bytes.NewReader([]byte("first"))
require.NoError(t, PlatformAtomicWriteFile(path, first, 0o644), "first write failed")
second := bytes.NewReader([]byte("second"))
require.NoError(t, PlatformAtomicWriteFile(path, second, 0o644), "second write failed")
contents, err := os.ReadFile(path)
require.NoError(t, err, "failed reading result")
if largeContent != string(got) {
t.Fatalf("expected large content to match, got different length: %d vs %d", len(largeContent), len(got))
}
require.Equal(t, "second", string(contents))
}

@ -0,0 +1,32 @@
//go:build !windows
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package fileutil
import (
"io"
"os"
)
// PlatformAtomicWriteFile atomically writes a file to disk.
//
// On non-Windows platforms we don't need extra coordination, so this simply
// delegates to AtomicWriteFile to preserve the existing overwrite behaviour.
func PlatformAtomicWriteFile(filename string, reader io.Reader, mode os.FileMode) error {
return AtomicWriteFile(filename, reader, mode)
}

@ -0,0 +1,54 @@
//go:build windows
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package fileutil
import (
"io"
"os"
"github.com/gofrs/flock"
)
// PlatformAtomicWriteFile atomically writes a file to disk with file locking to
// prevent concurrent writes. This is particularly useful on Windows where
// concurrent writes to the same file can cause "Access Denied" errors.
//
// The function acquires a lock on the target file and performs an atomic write,
// preserving the existing behaviour of overwriting any previous content once
// the lock is obtained.
func PlatformAtomicWriteFile(filename string, reader io.Reader, mode os.FileMode) error {
// Use a separate lock file to coordinate access between processes
// We cannot lock the target file directly as it would prevent the atomic rename
lockFileName := filename + ".lock"
fileLock := flock.New(lockFileName)
// Lock() ensures serialized access - if another process is writing, this will wait
if err := fileLock.Lock(); err != nil {
return err
}
defer func() {
fileLock.Unlock()
// Clean up the lock file
// Ignore errors as the file might not exist or be in use by another process
os.Remove(lockFileName)
}()
// Perform the atomic write while holding the lock
return AtomicWriteFile(filename, reader, mode)
}

@ -0,0 +1,19 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package gates contains internal feature gates that can be used to enable or disable experimental features.
// This is a separate internal package instead of using the pkg/gates package to avoid circular dependencies.
package gates

@ -0,0 +1,21 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package gates
import "helm.sh/helm/v4/pkg/gates"
// ChartV3 is the feature gate for chart API version v3.
const ChartV3 gates.Gate = "HELM_EXPERIMENTAL_CHART_V3"

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

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

@ -26,14 +26,12 @@ var ErrHostnameNotProvided = errors.New("no hostname provided")
// Client represents a client capable of communicating with the Monocular API.
type Client struct {
// The base URL for requests
BaseURL string
}
// 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 +44,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,18 @@ type ChartVersion struct {
}
// Search performs a search against the monocular search API
//
// Deprecated: Use SearchWithContext instead.
//
//go:fix inline
func (c *Client) Search(term string) ([]SearchResult, error) {
return c.SearchWithContext(context.Background(), term)
}
// SearchWithContext performs a search against the monocular search API
//
// TODO Helm v5: Rename this to Search (remove the current deprecated Search method) and 'go:fix inline' SearchWithContext method to call Search.
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 +125,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)
}

@ -14,7 +14,7 @@ limitations under the License.
*/
// Package cache provides a key generator for vcs urls.
package cache // import "helm.sh/helm/v4/internal/plugin/cache"
package cache
import (
"net/url"

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

@ -27,7 +27,7 @@ import (
func TestUnmarshaConfig(t *testing.T) {
// Test unmarshalling a CLI plugin config
{
config, err := unmarshaConfig("cli/v1", map[string]any{
config, err := unmarshalConfig("cli/v1", map[string]any{
"usage": "usage string",
"shortHelp": "short help string",
"longHelp": "long help string",
@ -41,16 +41,15 @@ func TestUnmarshaConfig(t *testing.T) {
ShortHelp: "short help string",
LongHelp: "long help string",
IgnoreFlags: true,
}, *(config.(*schema.ConfigCLIV1)))
}, *config.(*schema.ConfigCLIV1))
}
// Test unmarshalling invalid config data
{
config, err := unmarshaConfig("cli/v1", map[string]any{
config, err := unmarshalConfig("cli/v1", map[string]any{
"invalid field": "foo",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "field not found")
require.ErrorContains(t, err, "field not found")
assert.Nil(t, config)
}
}

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package installer // import "helm.sh/helm/v4/internal/plugin/installer"
package installer
import (
"path/filepath"

@ -11,10 +11,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package installer // import "helm.sh/helm/v4/internal/plugin/installer"
package installer
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestPath(t *testing.T) {
@ -35,12 +37,9 @@ func TestPath(t *testing.T) {
}
for _, tt := range tests {
t.Setenv("HELM_PLUGINS", tt.helmPluginsDir)
baseIns := newBase(tt.source)
baseInsPath := baseIns.Path()
if baseInsPath != tt.expectPath {
t.Errorf("expected name %s, got %s", tt.expectPath, baseInsPath)
}
assert.Equal(t, tt.expectPath, baseInsPath, "expected name %s, got %s", tt.expectPath, baseInsPath)
}
}

@ -14,4 +14,4 @@ limitations under the License.
*/
// Package installer provides an interface for installing Helm plugins.
package installer // import "helm.sh/helm/v4/internal/plugin/installer"
package installer

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package installer // import "helm.sh/helm/v4/internal/plugin/installer"
package installer
import (
"archive/tar"
@ -48,7 +48,7 @@ var Extractors = map[string]Extractor{
// Convert a media type to an extractor extension.
//
// This should be refactored in Helm 4, combined with the extension-based mechanism.
// TODO Helm v5: refactor this, combined with the extension-based mechanism.
func mediaTypeToExtension(mt string) (string, bool) {
switch strings.ToLower(mt) {
case "application/gzip", "application/x-gzip", "application/x-tgz", "application/x-gtar":
@ -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, ":") {
@ -133,14 +132,14 @@ func (g *TarGzExtractor) Extract(buffer *bytes.Buffer, targetDir string) error {
return err
}
if err := os.MkdirAll(targetDir, 0755); err != nil {
if err := os.MkdirAll(targetDir, 0o755); err != nil {
return err
}
tarReader := tar.NewReader(uncompressedStream)
for {
header, err := tarReader.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
if err != nil {
@ -154,12 +153,12 @@ func (g *TarGzExtractor) Extract(buffer *bytes.Buffer, targetDir string) error {
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(path, 0755); err != nil {
if err := os.MkdirAll(path, 0o755); err != nil {
return err
}
case tar.TypeReg:
// Ensure parent directory exists
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
outFile, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
@ -190,6 +189,6 @@ func stripPluginName(name string) string {
break
}
}
re := regexp.MustCompile(`(.*)-[0-9]+\..*`)
re := regexp.MustCompile(`(.*)-\d+\..*`)
return re.ReplaceAllString(strippedName, `$1`)
}

@ -13,10 +13,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package installer // import "helm.sh/helm/v4/internal/plugin/installer"
package installer
import (
"bytes"
"errors"
"fmt"
"log/slog"
"os"
@ -92,10 +93,10 @@ func (i *HTTPInstaller) Install() error {
}
filename := fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version)
tarballPath := helmpath.DataPath("plugins", filename)
if err := os.MkdirAll(filepath.Dir(tarballPath), 0755); err != nil {
if err := os.MkdirAll(filepath.Dir(tarballPath), 0o755); err != nil {
return fmt.Errorf("failed to create plugins directory: %w", err)
}
if err := os.WriteFile(tarballPath, i.pluginData, 0644); err != nil {
if err := os.WriteFile(tarballPath, i.pluginData, 0o644); err != nil {
return fmt.Errorf("failed to save tarball: %w", err)
}
@ -111,7 +112,7 @@ func (i *HTTPInstaller) Install() error {
// Save prov file if we have the data
if i.provData != nil {
provPath := tarballPath + ".prov"
if err := os.WriteFile(provPath, i.provData, 0644); err != nil {
if err := os.WriteFile(provPath, i.provData, 0o644); err != nil {
slog.Debug("failed to save provenance file", "error", err)
}
}
@ -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

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package installer // import "helm.sh/helm/v4/internal/plugin/installer"
package installer
import (
"archive/tar"
@ -27,10 +27,14 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/internal/test/ensure"
"helm.sh/helm/v4/pkg/getter"
"helm.sh/helm/v4/pkg/helmpath"
@ -52,18 +56,10 @@ func (t *TestHTTPGetter) Get(_ string, _ ...getter.Option) (*bytes.Buffer, error
var fakePluginB64 = "H4sIAAAAAAAAA+3SQUvDMBgG4Jz7K0LwapdvSxrwJig6mCKC5xHabBaXdDSt4L+3cQ56mV42ZPg+lw+SF5LwZmXf3OV206/rMGEnIgdG6zTJaDmee4y01FOlZpqGHJGZSsb1qS401sfOtpyz0FTup9xv+2dqNep/N/IP6zdHPSMVXCh1sH8yhtGMDBUFFTL1r4iIcXnUWxzwz/sP1rsrLkbfQGTvro11E4ZlmcucRNZHu04py1OO73OVi2Vbb7td9vp7nXevtvsKRpGVjfc2VMP2xf3t4mH5tHi5mz8ub+bPk9JXIvvr5wMAAAAAAAAAAAAAAAAAAAAAnLVPqwHcXQAoAAA="
func TestStripName(t *testing.T) {
if stripPluginName("fake-plugin-0.0.1.tar.gz") != "fake-plugin" {
t.Errorf("name does not match expected value")
}
if stripPluginName("fake-plugin-0.0.1.tgz") != "fake-plugin" {
t.Errorf("name does not match expected value")
}
if stripPluginName("fake-plugin.tgz") != "fake-plugin" {
t.Errorf("name does not match expected value")
}
if stripPluginName("fake-plugin.tar.gz") != "fake-plugin" {
t.Errorf("name does not match expected value")
}
assert.Equal(t, "fake-plugin", stripPluginName("fake-plugin-0.0.1.tar.gz"), "name does not match expected value")
assert.Equal(t, "fake-plugin", stripPluginName("fake-plugin-0.0.1.tgz"), "name does not match expected value")
assert.Equal(t, "fake-plugin", stripPluginName("fake-plugin.tgz"), "name does not match expected value")
assert.Equal(t, "fake-plugin", stripPluginName("fake-plugin.tar.gz"), "name does not match expected value")
}
func mockArchiveServer() *httptest.Server {
@ -71,10 +67,10 @@ func mockArchiveServer() *httptest.Server {
if !strings.HasSuffix(r.URL.Path, ".tar.gz") {
w.Header().Add("Content-Type", "text/html")
fmt.Fprintln(w, "broken")
return
} else {
w.Header().Add("Content-Type", "application/gzip")
fmt.Fprintln(w, "test")
}
w.Header().Add("Content-Type", "application/gzip")
fmt.Fprintln(w, "test")
}))
}
@ -85,46 +81,29 @@ func TestHTTPInstaller(t *testing.T) {
defer srv.Close()
source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz"
if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil {
t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err)
}
require.NoErrorf(t, os.MkdirAll(helmpath.DataPath("plugins"), 0o755), "Could not create %s", helmpath.DataPath("plugins"))
i, err := NewForSource(source, "0.0.1")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
require.NoError(t, err)
// ensure a HTTPInstaller was returned
httpInstaller, ok := i.(*HTTPInstaller)
if !ok {
t.Fatal("expected a HTTPInstaller")
}
require.True(t, ok, "expected a HTTPInstaller")
// inject fake http client responding with minimal plugin tarball
mockTgz, err := base64.StdEncoding.DecodeString(fakePluginB64)
if err != nil {
t.Fatalf("Could not decode fake tgz plugin: %s", err)
}
require.NoError(t, err, "Could not decode fake tgz plugin")
httpInstaller.getter = &TestHTTPGetter{
MockResponse: bytes.NewBuffer(mockTgz),
}
// install the plugin
if err := Install(i); err != nil {
t.Fatal(err)
}
if i.Path() != helmpath.DataPath("plugins", "fake-plugin") {
t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q", i.Path())
}
require.NoError(t, Install(i))
require.Equal(t, helmpath.DataPath("plugins", "fake-plugin"), i.Path(), "expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q", i.Path())
// Install again to test plugin exists error
if err := Install(i); err == nil {
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)
}
require.EqualErrorf(t, Install(i), "plugin already exists", "expected error for plugin exists")
}
func TestHTTPInstallerNonExistentVersion(t *testing.T) {
@ -133,31 +112,22 @@ func TestHTTPInstallerNonExistentVersion(t *testing.T) {
defer srv.Close()
source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz"
if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil {
t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err)
}
require.NoErrorf(t, os.MkdirAll(helmpath.DataPath("plugins"), 0o755), "Could not create %s", helmpath.DataPath("plugins"))
i, err := NewForSource(source, "0.0.2")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
require.NoError(t, err)
// ensure a HTTPInstaller was returned
httpInstaller, ok := i.(*HTTPInstaller)
if !ok {
t.Fatal("expected a HTTPInstaller")
}
require.True(t, ok, "expected a HTTPInstaller")
// 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")
}
require.Error(t, Install(i), "expected error from http client")
}
func TestHTTPInstallerUpdate(t *testing.T) {
@ -166,43 +136,29 @@ func TestHTTPInstallerUpdate(t *testing.T) {
source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz"
ensure.HelmHome(t)
if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil {
t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err)
}
require.NoErrorf(t, os.MkdirAll(helmpath.DataPath("plugins"), 0o755), "Could not create %s", helmpath.DataPath("plugins"))
i, err := NewForSource(source, "0.0.1")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
require.NoError(t, err)
// ensure a HTTPInstaller was returned
httpInstaller, ok := i.(*HTTPInstaller)
if !ok {
t.Fatal("expected a HTTPInstaller")
}
require.True(t, ok, "expected a HTTPInstaller")
// inject fake http client responding with minimal plugin tarball
mockTgz, err := base64.StdEncoding.DecodeString(fakePluginB64)
if err != nil {
t.Fatalf("Could not decode fake tgz plugin: %s", err)
}
require.NoError(t, err, "Could not decode fake tgz plugin")
httpInstaller.getter = &TestHTTPGetter{
MockResponse: bytes.NewBuffer(mockTgz),
}
// install the plugin before updating
if err := Install(i); err != nil {
t.Fatal(err)
}
if i.Path() != helmpath.DataPath("plugins", "fake-plugin") {
t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q", i.Path())
}
require.NoError(t, Install(i))
require.Equal(t, helmpath.DataPath("plugins", "fake-plugin"), i.Path(), "expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q", i.Path())
// Update plugin, should fail because it is not implemented
if err := Update(i); err == nil {
t.Fatal("update method not implemented for http installer")
}
require.Error(t, Update(i), "update method not implemented for http installer")
}
func TestExtract(t *testing.T) {
@ -217,12 +173,12 @@ func TestExtract(t *testing.T) {
// Write a tarball to a buffer for us to extract
var tarbuf bytes.Buffer
tw := tar.NewWriter(&tarbuf)
var files = []struct {
files := []struct {
Name, Body string
Mode int64
}{
{"plugin.yaml", "plugin metadata", 0600},
{"README.md", "some text", 0777},
{"plugin.yaml", "plugin metadata", 0o600},
{"README.md", "some text", 0o777},
}
for _, file := range files {
hdr := &tar.Header{
@ -231,12 +187,9 @@ func TestExtract(t *testing.T) {
Mode: file.Mode,
Size: int64(len(file.Body)),
}
if err := tw.WriteHeader(hdr); err != nil {
t.Fatal(err)
}
if _, err := tw.Write([]byte(file.Body)); err != nil {
t.Fatal(err)
}
require.NoError(t, tw.WriteHeader(hdr))
_, err := tw.Write([]byte(file.Body))
require.NoError(t, err)
}
// Add pax global headers. This should be ignored.
@ -244,60 +197,46 @@ func TestExtract(t *testing.T) {
// Details are in the internal Go function for the tar packaged named
// allowedFormats. For a TypeXHeader it will return a message stating
// "cannot manually encode TypeXHeader, TypeGNULongName, or TypeGNULongLink headers"
if err := tw.WriteHeader(&tar.Header{
require.NoError(t, tw.WriteHeader(&tar.Header{
Name: "pax_global_header",
Typeflag: tar.TypeXGlobalHeader,
}); err != nil {
t.Fatal(err)
}
}))
if err := tw.Close(); err != nil {
t.Fatal(err)
}
require.NoError(t, tw.Close())
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
if _, err := gz.Write(tarbuf.Bytes()); err != nil {
t.Fatal(err)
}
_, err := gz.Write(tarbuf.Bytes())
require.NoError(t, err)
gz.Close()
// END tarball creation
extractor, err := NewExtractor(source)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
if err = extractor.Extract(&buf, tempDir); err != nil {
t.Fatalf("Did not expect error but got error: %v", err)
}
require.NoErrorf(t, extractor.Extract(&buf, tempDir), "Did not expect error")
// Calculate expected permissions after umask is applied
expectedPluginYAMLPerm := os.FileMode(0600 &^ currentUmask)
expectedReadmePerm := os.FileMode(0777 &^ currentUmask)
expectedPluginYAMLPerm := os.FileMode(0o600 &^ currentUmask)
expectedReadmePerm := os.FileMode(0o777 &^ currentUmask)
pluginYAMLFullPath := filepath.Join(tempDir, "plugin.yaml")
if info, err := os.Stat(pluginYAMLFullPath); err != nil {
if errors.Is(err, fs.ErrNotExist) {
t.Fatalf("Expected %s to exist but doesn't", pluginYAMLFullPath)
}
t.Fatal(err)
} else if info.Mode().Perm() != expectedPluginYAMLPerm {
t.Fatalf("Expected %s to have %o mode but has %o (umask: %o)",
pluginYAMLFullPath, expectedPluginYAMLPerm, info.Mode().Perm(), currentUmask)
info, err := os.Stat(pluginYAMLFullPath)
if err != nil {
require.NotErrorIs(t, err, fs.ErrNotExist, "Expected %s to exist but doesn't", pluginYAMLFullPath)
}
require.NoError(t, err)
require.Equalf(t, expectedPluginYAMLPerm, info.Mode().Perm(), "Expected %s to have %o mode but has %o (umask: %o)",
pluginYAMLFullPath, expectedPluginYAMLPerm, info.Mode().Perm(), currentUmask)
readmeFullPath := filepath.Join(tempDir, "README.md")
if info, err := os.Stat(readmeFullPath); err != nil {
if errors.Is(err, fs.ErrNotExist) {
t.Fatalf("Expected %s to exist but doesn't", readmeFullPath)
}
t.Fatal(err)
} else if info.Mode().Perm() != expectedReadmePerm {
t.Fatalf("Expected %s to have %o mode but has %o (umask: %o)",
readmeFullPath, expectedReadmePerm, info.Mode().Perm(), currentUmask)
info, err = os.Stat(readmeFullPath)
if err != nil {
require.NotErrorIs(t, err, fs.ErrNotExist, "Expected %s to exist but doesn't", readmeFullPath)
}
require.NoError(t, err)
require.Equalf(t, expectedReadmePerm, info.Mode().Perm(), "Expected %s to have %o mode but has %o (umask: %o)",
readmeFullPath, expectedReadmePerm, info.Mode().Perm(), currentUmask)
}
func TestCleanJoin(t *testing.T) {
@ -316,22 +255,19 @@ func TestCleanJoin(t *testing.T) {
{"foo\\bar.txt", "/tmp/foo/bar.txt", false},
{"c:\\foo\\bar.txt", "", true},
} {
out, err := cleanJoin("/tmp", fixture.path)
if err != nil {
if !fixture.expectError {
t.Errorf("Test %d: Path was not cleaned: %s", i, err)
t.Run(strconv.Itoa(i), func(t *testing.T) {
out, err := cleanJoin("/tmp", fixture.path)
if fixture.expectError {
require.Error(t, err, "Test %d: Path was not cleaned", i)
} else {
require.NoError(t, err)
assert.Equal(t, fixture.expect, out, "Test %d: Expected %q but got %q", i, fixture.expect, out)
}
continue
}
if fixture.expect != out {
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,
@ -341,14 +277,11 @@ func TestMediaTypeToExtension(t *testing.T) {
"application/json": false,
} {
ext, ok := mediaTypeToExtension(mt)
if ok != shouldPass {
t.Errorf("Media type %q failed test", mt)
}
if shouldPass && ext == "" {
t.Errorf("Expected an extension but got empty string")
}
if !shouldPass && len(ext) != 0 {
t.Error("Expected extension to be empty for unrecognized type")
assert.Equal(t, shouldPass, ok, "Media type %q failed test", mt)
if shouldPass {
assert.NotEmpty(t, ext, "Expected an extension but got empty string for media type %q", mt)
} else {
assert.Empty(t, ext, "Expected extension to be empty for unrecognized media type %q", mt)
}
}
}
@ -360,19 +293,19 @@ func TestExtractWithNestedDirectories(t *testing.T) {
// Write a tarball with nested directory structure
var tarbuf bytes.Buffer
tw := tar.NewWriter(&tarbuf)
var files = []struct {
files := []struct {
Name string
Body string
Mode int64
TypeFlag byte
}{
{"plugin.yaml", "plugin metadata", 0600, tar.TypeReg},
{"bin/", "", 0755, tar.TypeDir},
{"bin/plugin", "#!/bin/bash\necho plugin", 0755, tar.TypeReg},
{"docs/", "", 0755, tar.TypeDir},
{"docs/README.md", "readme content", 0644, tar.TypeReg},
{"docs/examples/", "", 0755, tar.TypeDir},
{"docs/examples/example1.yaml", "example content", 0644, tar.TypeReg},
{"plugin.yaml", "plugin metadata", 0o600, tar.TypeReg},
{"bin/", "", 0o755, tar.TypeDir},
{"bin/plugin", "#!/usr/bin/env sh\necho plugin", 0o755, tar.TypeReg},
{"docs/", "", 0o755, tar.TypeDir},
{"docs/README.md", "readme content", 0o644, tar.TypeReg},
{"docs/examples/", "", 0o755, tar.TypeDir},
{"docs/examples/example1.yaml", "example content", 0o644, tar.TypeReg},
}
for _, file := range files {
@ -382,55 +315,41 @@ func TestExtractWithNestedDirectories(t *testing.T) {
Mode: file.Mode,
Size: int64(len(file.Body)),
}
if err := tw.WriteHeader(hdr); err != nil {
t.Fatal(err)
}
require.NoError(t, tw.WriteHeader(hdr))
if file.TypeFlag == tar.TypeReg {
if _, err := tw.Write([]byte(file.Body)); err != nil {
t.Fatal(err)
}
_, err := tw.Write([]byte(file.Body))
require.NoError(t, err)
}
}
if err := tw.Close(); err != nil {
t.Fatal(err)
}
require.NoError(t, tw.Close())
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
if _, err := gz.Write(tarbuf.Bytes()); err != nil {
t.Fatal(err)
}
_, err := gz.Write(tarbuf.Bytes())
require.NoError(t, err)
gz.Close()
extractor, err := NewExtractor(source)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
// First extraction
if err = extractor.Extract(&buf, tempDir); err != nil {
t.Fatalf("First extraction failed: %v", err)
}
require.NoError(t, extractor.Extract(&buf, tempDir), "First extraction failed")
// Verify nested structure was created
nestedFile := filepath.Join(tempDir, "docs", "examples", "example1.yaml")
if _, err := os.Stat(nestedFile); err != nil {
t.Fatalf("Expected nested file %s to exist but got error: %v", nestedFile, err)
}
_, err = os.Stat(nestedFile)
require.NoErrorf(t, err, "Expected nested file %s to exist", nestedFile)
// Reset buffer for second extraction
buf.Reset()
gz = gzip.NewWriter(&buf)
if _, err := gz.Write(tarbuf.Bytes()); err != nil {
t.Fatal(err)
}
_, err = gz.Write(tarbuf.Bytes())
require.NoError(t, err)
gz.Close()
// Second extraction to same directory (should not fail)
if err = extractor.Extract(&buf, tempDir); err != nil {
t.Fatalf("Second extraction to existing directory failed: %v", err)
}
require.NoErrorf(t, extractor.Extract(&buf, tempDir), "Second extraction to existing directory failed")
}
func TestExtractWithExistingDirectory(t *testing.T) {
@ -439,15 +358,11 @@ func TestExtractWithExistingDirectory(t *testing.T) {
// Pre-create the cache directory structure
cacheDir := filepath.Join(tempDir, "cache")
if err := os.MkdirAll(filepath.Join(cacheDir, "existing", "dir"), 0755); err != nil {
t.Fatal(err)
}
require.NoError(t, os.MkdirAll(filepath.Join(cacheDir, "existing", "dir"), 0o755))
// Create a file in the existing directory
existingFile := filepath.Join(cacheDir, "existing", "file.txt")
if err := os.WriteFile(existingFile, []byte("existing content"), 0644); err != nil {
t.Fatal(err)
}
require.NoError(t, os.WriteFile(existingFile, []byte("existing content"), 0o644))
// Write a tarball
var tarbuf bytes.Buffer
@ -458,10 +373,10 @@ func TestExtractWithExistingDirectory(t *testing.T) {
Mode int64
TypeFlag byte
}{
{"plugin.yaml", "plugin metadata", 0600, tar.TypeReg},
{"existing/", "", 0755, tar.TypeDir},
{"existing/dir/", "", 0755, tar.TypeDir},
{"existing/dir/newfile.txt", "new content", 0644, tar.TypeReg},
{"plugin.yaml", "plugin metadata", 0o600, tar.TypeReg},
{"existing/", "", 0o755, tar.TypeDir},
{"existing/dir/", "", 0o755, tar.TypeDir},
{"existing/dir/newfile.txt", "new content", 0o644, tar.TypeReg},
}
for _, file := range files {
@ -471,47 +386,35 @@ func TestExtractWithExistingDirectory(t *testing.T) {
Mode: file.Mode,
Size: int64(len(file.Body)),
}
if err := tw.WriteHeader(hdr); err != nil {
t.Fatal(err)
}
require.NoError(t, tw.WriteHeader(hdr))
if file.TypeFlag == tar.TypeReg {
if _, err := tw.Write([]byte(file.Body)); err != nil {
t.Fatal(err)
}
_, err := tw.Write([]byte(file.Body))
require.NoError(t, err)
}
}
if err := tw.Close(); err != nil {
t.Fatal(err)
}
require.NoError(t, tw.Close())
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
if _, err := gz.Write(tarbuf.Bytes()); err != nil {
t.Fatal(err)
}
_, err := gz.Write(tarbuf.Bytes())
require.NoError(t, err)
gz.Close()
extractor, err := NewExtractor(source)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
// Extract to directory with existing content
if err = extractor.Extract(&buf, cacheDir); err != nil {
t.Fatalf("Extraction to directory with existing content failed: %v", err)
}
require.NoErrorf(t, extractor.Extract(&buf, cacheDir), "Extraction to directory with existing content failed")
// Verify new file was created
newFile := filepath.Join(cacheDir, "existing", "dir", "newfile.txt")
if _, err := os.Stat(newFile); err != nil {
t.Fatalf("Expected new file %s to exist but got error: %v", newFile, err)
}
_, err = os.Stat(newFile)
require.NoErrorf(t, err, "Expected new file %s to exist but got error", newFile)
// Verify existing file is still there
if _, err := os.Stat(existingFile); err != nil {
t.Fatalf("Expected existing file %s to still exist but got error: %v", existingFile, err)
}
_, err = os.Stat(existingFile)
require.NoErrorf(t, err, "Expected existing file %s to still exist", existingFile)
}
func TestExtractPluginInSubdirectory(t *testing.T) {
@ -528,10 +431,10 @@ func TestExtractPluginInSubdirectory(t *testing.T) {
Mode int64
TypeFlag byte
}{
{"my-plugin/", "", 0755, tar.TypeDir},
{"my-plugin/plugin.yaml", "name: my-plugin\nversion: 1.0.0\nusage: test\ndescription: test plugin\ncommand: $HELM_PLUGIN_DIR/bin/my-plugin", 0644, tar.TypeReg},
{"my-plugin/bin/", "", 0755, tar.TypeDir},
{"my-plugin/bin/my-plugin", "#!/bin/bash\necho test", 0755, tar.TypeReg},
{"my-plugin/", "", 0o755, tar.TypeDir},
{"my-plugin/plugin.yaml", "name: my-plugin\nversion: 1.0.0\nusage: test\ndescription: test plugin\ncommand: $HELM_PLUGIN_DIR/bin/my-plugin", 0o644, tar.TypeReg},
{"my-plugin/bin/", "", 0o755, tar.TypeDir},
{"my-plugin/bin/my-plugin", "#!/usr/bin/env sh\necho test", 0o755, tar.TypeReg},
}
for _, file := range files {
@ -541,25 +444,19 @@ func TestExtractPluginInSubdirectory(t *testing.T) {
Mode: file.Mode,
Size: int64(len(file.Body)),
}
if err := tw.WriteHeader(hdr); err != nil {
t.Fatal(err)
}
require.NoError(t, tw.WriteHeader(hdr))
if file.TypeFlag == tar.TypeReg {
if _, err := tw.Write([]byte(file.Body)); err != nil {
t.Fatal(err)
}
_, err := tw.Write([]byte(file.Body))
require.NoError(t, err)
}
}
if err := tw.Close(); err != nil {
t.Fatal(err)
}
require.NoError(t, tw.Close())
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
if _, err := gz.Write(tarbuf.Bytes()); err != nil {
t.Fatal(err)
}
_, err := gz.Write(tarbuf.Bytes())
require.NoError(t, err)
gz.Close()
// Test the installer
@ -578,24 +475,16 @@ func TestExtractPluginInSubdirectory(t *testing.T) {
// Ensure the destination directory doesn't exist
// (In a real scenario, this is handled by installer.Install() wrapper)
destPath := installer.Path()
if err := os.RemoveAll(destPath); err != nil {
t.Fatalf("Failed to clean destination path: %v", err)
}
require.NoErrorf(t, os.RemoveAll(destPath), "Failed to clean destination path")
// Install should handle the subdirectory correctly
if err := installer.Install(); err != nil {
t.Fatalf("Failed to install plugin with subdirectory: %v", err)
}
require.NoErrorf(t, installer.Install(), "Failed to install plugin with subdirectory")
// The plugin should be installed from the subdirectory
// Check that detectPluginRoot found the correct location
pluginRoot, err := detectPluginRoot(tempDir)
if err != nil {
t.Fatalf("Failed to detect plugin root: %v", err)
}
require.NoError(t, err, "Failed to detect plugin root")
expectedRoot := filepath.Join(tempDir, "my-plugin")
if pluginRoot != expectedRoot {
t.Errorf("Expected plugin root to be %s but got %s", expectedRoot, pluginRoot)
}
assert.Equal(t, expectedRoot, pluginRoot, "Expected plugin root to be %s but got %s", expectedRoot, pluginRoot)
}

@ -16,6 +16,7 @@ limitations under the License.
package installer
import (
"context"
"errors"
"fmt"
"log/slog"
@ -31,9 +32,6 @@ import (
// ErrMissingMetadata indicates that plugin.yaml is missing.
var ErrMissingMetadata = errors.New("plugin metadata (plugin.yaml) missing")
// Debug enables verbose output.
var Debug bool
// Options contains options for plugin installation.
type Options struct {
// Verify enables signature verification before installation
@ -75,12 +73,11 @@ 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 {
if err := os.MkdirAll(filepath.Dir(i.Path()), 0o755); err != nil {
return nil, err
}
if _, pathErr := os.Stat(i.Path()); !os.IsNotExist(pathErr) {
slog.Warn("plugin already exists", "path", i.Path(), slog.Any("error", pathErr))
slog.Warn("plugin already exists", slog.String("path", i.Path()), slog.Any("error", pathErr))
return nil, errors.New("plugin already exists")
}
@ -90,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)
@ -101,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)
}
}
@ -132,7 +128,7 @@ func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error)
// Update updates a plugin.
func Update(i Installer) error {
if _, pathErr := os.Stat(i.Path()); os.IsNotExist(pathErr) {
slog.Warn("plugin does not exist", "path", i.Path(), slog.Any("error", pathErr))
slog.Warn("plugin does not exist", slog.String("path", i.Path()), slog.Any("error", pathErr))
return errors.New("plugin does not exist")
}
return i.Update()
@ -140,15 +136,16 @@ 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)) {
switch {
case strings.HasPrefix(source, registry.OCIScheme+"://"):
// Source is an OCI registry reference
installer, err = NewOCIInstaller(source)
} else if isLocalReference(source) {
case isLocalReference(source):
// Source is a local directory
installer, err = NewLocalInstaller(source)
} else if isRemoteHTTPArchive(source) {
case isRemoteHTTPArchive(source):
installer, err = NewHTTPInstaller(source)
} else {
default:
installer, err = NewVCSInstaller(source, version)
}
@ -156,14 +153,18 @@ func NewForSource(source, version string) (installer Installer, err error) {
return installer, fmt.Errorf("cannot get information about plugin source %q (if it's a local directory, does it exist?), last error was: %w", source, err)
}
return
return installer, err
}
// FindSource determines the correct Installer for the given source.
func FindSource(location string) (Installer, error) {
installer, err := existingVCSRepo(location)
if err != nil && err.Error() == "Cannot detect VCS" {
slog.Warn("cannot get information about plugin source", "location", location, slog.Any("error", err))
slog.Warn(
"cannot get information about plugin source",
slog.String("location", location),
slog.Any("error", err),
)
return installer, errors.New("cannot get information about plugin source")
}
return installer, err
@ -190,12 +191,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.

@ -15,33 +15,25 @@ limitations under the License.
package installer
import "testing"
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsRemoteHTTPArchive(t *testing.T) {
srv := mockArchiveServer()
defer srv.Close()
source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz"
if isRemoteHTTPArchive("/not/a/URL") {
t.Errorf("Expected non-URL to return false")
}
assert.False(t, isRemoteHTTPArchive("/not/a/URL"), "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")
}
assert.True(t, isRemoteHTTPArchive("https://127.0.0.1:123/fake/plugin-1.2.3.tgz"), "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")
}
if !isRemoteHTTPArchive(source) {
t.Errorf("Expected %q to be a valid archive URL", source)
}
if isRemoteHTTPArchive(source + "-not-an-extension") {
t.Error("Expected media type match to fail")
}
assert.False(t, isRemoteHTTPArchive("https://127.0.0.1:123/fake/plugin-1.2.3.notanarchive"), "Bad URL without valid extension should not succeed")
assert.True(t, isRemoteHTTPArchive(source), "Expected %q to be a valid archive URL", source)
assert.False(t, isRemoteHTTPArchive(source+"-not-an-extension"), "Expected media type match to fail")
}

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package installer // import "helm.sh/helm/v4/internal/plugin/installer"
package installer
import (
"bytes"
@ -117,10 +117,10 @@ func (i *LocalInstaller) installFromArchive() error {
}
filename := fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version)
tarballPath := helmpath.DataPath("plugins", filename)
if err := os.MkdirAll(filepath.Dir(tarballPath), 0755); err != nil {
if err := os.MkdirAll(filepath.Dir(tarballPath), 0o755); err != nil {
return fmt.Errorf("failed to create plugins directory: %w", err)
}
if err := os.WriteFile(tarballPath, data, 0644); err != nil {
if err := os.WriteFile(tarballPath, data, 0o644); err != nil {
return fmt.Errorf("failed to save tarball: %w", err)
}
@ -128,7 +128,7 @@ func (i *LocalInstaller) installFromArchive() error {
provSource := i.Source + ".prov"
if provData, err := os.ReadFile(provSource); err == nil {
provPath := tarballPath + ".prov"
if err := os.WriteFile(provPath, provData, 0644); err != nil {
if err := os.WriteFile(provPath, provData, 0o644); err != nil {
slog.Debug("failed to save provenance file", "error", err)
}
}
@ -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
@ -204,14 +204,13 @@ func (i *LocalInstaller) GetVerificationData() (archiveData, provData []byte, fi
provFile := i.Source + ".prov"
i.provData, err = os.ReadFile(provFile)
if err != nil {
if os.IsNotExist(err) {
// If provenance file doesn't exist, set provData to nil
// The verification logic will handle this gracefully
i.provData = nil
} else {
if !os.IsNotExist(err) {
// If file exists but can't be read (permissions, etc), return error
return nil, nil, "", fmt.Errorf("failed to access provenance file %s: %w", provFile, err)
}
// If provenance file doesn't exist, set provData to nil
// The verification logic will handle this gracefully
i.provData = nil
}
}

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package installer // import "helm.sh/helm/v4/internal/plugin/installer"
package installer
import (
"archive/tar"
@ -23,6 +23,8 @@ import (
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/internal/test/ensure"
"helm.sh/helm/v4/pkg/helmpath"
)
@ -33,40 +35,23 @@ func TestLocalInstaller(t *testing.T) {
ensure.HelmHome(t)
// Make a temp dir
tdir := t.TempDir()
if err := os.WriteFile(filepath.Join(tdir, "plugin.yaml"), []byte{}, 0644); err != nil {
t.Fatal(err)
}
require.NoError(t, os.WriteFile(filepath.Join(tdir, "plugin.yaml"), []byte{}, 0o644))
source := "../testdata/plugdir/good/echo-v1"
i, err := NewForSource(source, "")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
require.NoError(t, err)
if err := Install(i); err != nil {
t.Fatal(err)
}
require.NoError(t, Install(i))
if i.Path() != helmpath.DataPath("plugins", "echo-v1") {
t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/helm-env', got %q", i.Path())
}
defer os.RemoveAll(filepath.Dir(helmpath.DataPath())) // helmpath.DataPath is like /tmp/helm013130971/helm
require.Equal(t, helmpath.DataPath("plugins", "echo-v1"), i.Path(), "expected path '$XDG_CONFIG_HOME/helm/plugins/helm-env', got %q", i.Path())
os.RemoveAll(filepath.Dir(helmpath.DataPath())) // helmpath.DataPath is like /tmp/helm013130971/helm
}
func TestLocalInstallerNotAFolder(t *testing.T) {
source := "../testdata/plugdir/good/echo-v1/plugin.yaml"
i, err := NewForSource(source, "")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
err = Install(i)
if err == nil {
t.Fatal("expected error")
}
if err != ErrPluginNotADirectory {
t.Fatalf("expected error to equal: %q", err)
}
require.NoError(t, err)
require.ErrorIs(t, Install(i), ErrPluginNotADirectory)
}
func TestLocalInstallerTarball(t *testing.T) {
@ -86,8 +71,8 @@ func TestLocalInstallerTarball(t *testing.T) {
Body string
Mode int64
}{
{"test-plugin/plugin.yaml", "name: test-plugin\napiVersion: v1\ntype: cli/v1\nruntime: subprocess\nversion: 1.0.0\nconfig:\n shortHelp: test\n longHelp: test\nruntimeConfig:\n platformCommand:\n - command: echo", 0644},
{"test-plugin/bin/test-plugin", "#!/bin/bash\necho test", 0755},
{"test-plugin/plugin.yaml", "name: test-plugin\napiVersion: v1\ntype: cli/v1\nruntime: subprocess\nversion: 1.0.0\nconfig:\n shortHelp: test\n longHelp: test\nruntimeConfig:\n platformCommand:\n - command: echo", 0o644},
{"test-plugin/bin/test-plugin", "#!/usr/bin/env sh\necho test", 0o755},
}
for _, file := range files {
@ -96,53 +81,31 @@ func TestLocalInstallerTarball(t *testing.T) {
Mode: file.Mode,
Size: int64(len(file.Body)),
}
if err := tw.WriteHeader(hdr); err != nil {
t.Fatal(err)
}
if _, err := tw.Write([]byte(file.Body)); err != nil {
t.Fatal(err)
}
require.NoError(t, tw.WriteHeader(hdr))
_, err := tw.Write([]byte(file.Body))
require.NoError(t, err)
}
if err := tw.Close(); err != nil {
t.Fatal(err)
}
if err := gw.Close(); err != nil {
t.Fatal(err)
}
require.NoError(t, tw.Close())
require.NoError(t, gw.Close())
// Write tarball to file
if err := os.WriteFile(tarballPath, buf.Bytes(), 0644); err != nil {
t.Fatal(err)
}
require.NoError(t, os.WriteFile(tarballPath, buf.Bytes(), 0o644))
// Test installation
i, err := NewForSource(tarballPath, "")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
require.NoError(t, err)
// Verify it's detected as LocalInstaller
localInstaller, ok := i.(*LocalInstaller)
if !ok {
t.Fatal("expected LocalInstaller")
}
if !localInstaller.isArchive {
t.Fatal("expected isArchive to be true")
}
if err := Install(i); err != nil {
t.Fatal(err)
}
require.True(t, ok, "expected LocalInstaller")
require.True(t, localInstaller.isArchive, "expected isArchive to be true")
require.NoError(t, Install(i))
expectedPath := helmpath.DataPath("plugins", "test-plugin")
if i.Path() != expectedPath {
t.Fatalf("expected path %q, got %q", expectedPath, i.Path())
}
require.Equal(t, expectedPath, i.Path(), "expected path %q, got %q", expectedPath, i.Path())
// Verify plugin was installed
if _, err := os.Stat(i.Path()); err != nil {
t.Fatalf("plugin not found at %s: %v", i.Path(), err)
}
_, err = os.Stat(i.Path())
require.NoErrorf(t, err, "plugin not found at %s", i.Path())
}

@ -19,6 +19,7 @@ import (
"archive/tar"
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"log/slog"
@ -103,10 +104,10 @@ func (i *OCIInstaller) Install() error {
filename := fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version)
tarballPath := helmpath.DataPath("plugins", filename)
if err := os.MkdirAll(filepath.Dir(tarballPath), 0755); err != nil {
if err := os.MkdirAll(filepath.Dir(tarballPath), 0o755); err != nil {
return fmt.Errorf("failed to create plugins directory: %w", err)
}
if err := os.WriteFile(tarballPath, i.pluginData, 0644); err != nil {
if err := os.WriteFile(tarballPath, i.pluginData, 0o644); err != nil {
return fmt.Errorf("failed to save tarball: %w", err)
}
@ -122,18 +123,18 @@ func (i *OCIInstaller) Install() error {
// Save prov file if we have the data
if i.provData != nil {
provPath := tarballPath + ".prov"
if err := os.WriteFile(provPath, i.provData, 0644); err != nil {
if err := os.WriteFile(provPath, i.provData, 0o644); err != nil {
slog.Debug("failed to save provenance file", "error", err)
}
}
// 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
if err := os.MkdirAll(i.CacheDir, 0755); err != nil {
if err := os.MkdirAll(i.CacheDir, 0o755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
@ -214,7 +215,7 @@ func extractTar(r io.Reader, targetDir string) error {
for {
header, err := tarReader.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
if err != nil {
@ -228,12 +229,12 @@ func extractTar(r io.Reader, targetDir string) error {
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(path, 0755); err != nil {
if err := os.MkdirAll(path, 0o755); err != nil {
return err
}
case tar.TypeReg:
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package installer // import "helm.sh/helm/v4/internal/plugin/installer"
package installer
import (
"archive/tar"
@ -33,6 +33,8 @@ import (
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/internal/test/ensure"
"helm.sh/helm/v4/pkg/cli"
@ -58,41 +60,33 @@ command: "$HELM_PLUGIN_DIR/bin/%s"
`, pluginName, pluginName)
header := &tar.Header{
Name: "plugin.yaml",
Mode: 0644,
Mode: 0o644,
Size: int64(len(pluginYAML)),
Typeflag: tar.TypeReg,
}
if err := tarWriter.WriteHeader(header); err != nil {
t.Fatal(err)
}
if _, err := tarWriter.Write([]byte(pluginYAML)); err != nil {
t.Fatal(err)
}
require.NoError(t, tarWriter.WriteHeader(header))
_, err := tarWriter.Write([]byte(pluginYAML))
require.NoError(t, err)
// Add bin directory
dirHeader := &tar.Header{
Name: "bin/",
Mode: 0755,
Mode: 0o755,
Typeflag: tar.TypeDir,
}
if err := tarWriter.WriteHeader(dirHeader); err != nil {
t.Fatal(err)
}
require.NoError(t, tarWriter.WriteHeader(dirHeader))
// Add executable
execContent := fmt.Sprintf("#!/bin/sh\necho '%s test plugin'", pluginName)
execHeader := &tar.Header{
Name: fmt.Sprintf("bin/%s", pluginName),
Mode: 0755,
Name: "bin/" + pluginName,
Mode: 0o755,
Size: int64(len(execContent)),
Typeflag: tar.TypeReg,
}
if err := tarWriter.WriteHeader(execHeader); err != nil {
t.Fatal(err)
}
if _, err := tarWriter.Write([]byte(execContent)); err != nil {
t.Fatal(err)
}
require.NoError(t, tarWriter.WriteHeader(execHeader))
_, err = tarWriter.Write([]byte(execContent))
require.NoError(t, err)
tarWriter.Close()
gzWriter.Close()
@ -133,9 +127,7 @@ func mockOCIRegistryWithArtifactType(t *testing.T, pluginName string) (*httptest
}
manifestData, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
manifestDigest := fmt.Sprintf("sha256:%x", sha256Sum(manifestData))
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -173,9 +165,7 @@ func mockOCIRegistryWithArtifactType(t *testing.T, pluginName string) (*httptest
// Parse server URL to get host:port format for OCI reference
serverURL, err := url.Parse(server.URL)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
registryHost := serverURL.Host
return server, registryHost
@ -238,42 +228,20 @@ func TestNewOCIInstaller(t *testing.T) {
installer, err := NewOCIInstaller(tt.source)
if tt.expectError {
if err == nil {
t.Errorf("expected error but got none")
}
return
}
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
// Check all fields thoroughly
if installer.PluginName != tt.expectName {
t.Errorf("expected plugin name %s, got %s", tt.expectName, installer.PluginName)
}
if installer.Source != tt.source {
t.Errorf("expected source %s, got %s", tt.source, installer.Source)
}
if installer.CacheDir == "" {
t.Error("expected non-empty cache directory")
}
if !strings.Contains(installer.CacheDir, "plugins") {
t.Errorf("expected cache directory to contain 'plugins', got %s", installer.CacheDir)
}
if installer.settings == nil {
t.Error("expected settings to be initialized")
}
// Check that Path() method works
expectedPath := helmpath.DataPath("plugins", tt.expectName)
if installer.Path() != expectedPath {
t.Errorf("expected path %s, got %s", expectedPath, installer.Path())
require.Error(t, err)
} else {
require.NoError(t, err)
// Check all fields thoroughly
assert.Equal(t, tt.expectName, installer.PluginName, "expected plugin name %s, got %s", tt.expectName, installer.PluginName)
assert.Equal(t, tt.source, installer.Source, "expected source %s, got %s", tt.source, installer.Source)
assert.NotEmpty(t, installer.CacheDir, "expected non-empty cache directory")
assert.Contains(t, installer.CacheDir, "plugins", "expected cache directory to contain 'plugins', got %s", installer.CacheDir)
assert.NotNil(t, installer.settings, "expected settings to be initialized")
// Check that Path() method works
expectedPath := helmpath.DataPath("plugins", tt.expectName)
assert.Equal(t, expectedPath, installer.Path(), "expected path %s, got %s", expectedPath, installer.Path())
}
})
}
@ -309,9 +277,7 @@ func TestOCIInstaller_Path(t *testing.T) {
}
path := installer.Path()
if path != tt.expectPath {
t.Errorf("expected path %s, got %s", tt.expectPath, path)
}
assert.Equal(t, tt.expectPath, path, "expected path %s, got %s", tt.expectPath, path)
})
}
}
@ -329,39 +295,30 @@ func TestOCIInstaller_Install(t *testing.T) {
// Test with plain HTTP (since test server uses HTTP)
installer, err := NewOCIInstaller(source, getter.WithPlainHTTP(true))
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
require.NoError(t, err)
// The OCI installer uses helmpath.DataPath, which is isolated by ensure.HelmHome(t)
actualPath := installer.Path()
t.Logf("Installer will use path: %s", actualPath)
// Install the plugin
if err := Install(installer); err != nil {
t.Fatalf("Expected installation to succeed, got error: %v", err)
}
require.NoErrorf(t, Install(installer), "Expected installation to succeed")
// Verify plugin was installed to the correct location
if !isPlugin(actualPath) {
t.Errorf("Expected plugin directory %s to contain plugin.yaml", actualPath)
}
assert.Truef(t, isPlugin(actualPath), "Expected plugin directory %s to contain plugin.yaml", actualPath)
// Debug: list what was actually created
if entries, err := os.ReadDir(actualPath); err != nil {
t.Fatalf("Could not read plugin directory %s: %v", actualPath, err)
} else {
t.Logf("Plugin directory %s contains:", actualPath)
for _, entry := range entries {
t.Logf(" - %s", entry.Name())
}
entries, err := os.ReadDir(actualPath)
require.NoError(t, err, "Could not read plugin directory %s", actualPath)
t.Logf("Plugin directory %s contains:", actualPath)
for _, entry := range entries {
t.Logf(" - %s", entry.Name())
}
// Verify the plugin.yaml file exists and is valid
pluginFile := filepath.Join(actualPath, "plugin.yaml")
if _, err := os.Stat(pluginFile); err != nil {
t.Errorf("Expected plugin.yaml to exist, got error: %v", err)
}
_, err = os.Stat(pluginFile)
assert.NoErrorf(t, err, "Expected plugin.yaml to exist")
}
func TestOCIInstaller_Install_WithGetterOptions(t *testing.T) {
@ -402,11 +359,9 @@ func TestOCIInstaller_Install_WithGetterOptions(t *testing.T) {
source := fmt.Sprintf("oci://%s/%s:latest", registryHost, tc.pluginName)
installer, err := NewOCIInstaller(source, tc.options...)
if err != nil {
if !tc.wantErr {
t.Fatalf("Expected no error creating installer, got %v", err)
}
return
if !tc.wantErr {
require.NoError(t, err, "Expected no error creating installer")
}
// The installer now uses our isolated test directory
@ -415,18 +370,11 @@ func TestOCIInstaller_Install_WithGetterOptions(t *testing.T) {
// Install the plugin
err = Install(installer)
if tc.wantErr {
if err == nil {
t.Errorf("Expected installation to fail, but it succeeded")
}
require.Error(t, err, "Expected installation to fail, but it succeeded")
} else {
if err != nil {
t.Errorf("Expected installation to succeed, got error: %v", err)
} else {
// Verify plugin was installed to the actual path
if !isPlugin(actualPath) {
t.Errorf("Expected plugin directory %s to contain plugin.yaml", actualPath)
}
}
require.NoError(t, err, "Expected installation to succeed")
// Verify plugin was installed to the actual path
assert.True(t, isPlugin(actualPath), "Expected plugin directory %s to contain plugin.yaml", actualPath)
}
})
}
@ -442,27 +390,16 @@ func TestOCIInstaller_Install_AlreadyExists(t *testing.T) {
source := fmt.Sprintf("oci://%s/%s:latest", registryHost, pluginName)
installer, err := NewOCIInstaller(source, getter.WithPlainHTTP(true))
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
require.NoError(t, err)
// First install should succeed
if err := Install(installer); err != nil {
t.Fatalf("Expected first installation to succeed, got error: %v", err)
}
require.NoErrorf(t, Install(installer), "Expected first installation to succeed")
// Verify plugin was installed
if !isPlugin(installer.Path()) {
t.Errorf("Expected plugin directory %s to contain plugin.yaml", installer.Path())
}
assert.Truef(t, isPlugin(installer.Path()), "Expected plugin directory %s to contain plugin.yaml", installer.Path())
// Second install should fail with "plugin already exists"
err = Install(installer)
if err == nil {
t.Error("Expected error when installing plugin that already exists")
} else if !strings.Contains(err.Error(), "plugin already exists") {
t.Errorf("Expected 'plugin already exists' error, got: %v", err)
}
assert.ErrorContains(t, Install(installer), "plugin already exists")
}
func TestOCIInstaller_Update(t *testing.T) {
@ -475,38 +412,23 @@ func TestOCIInstaller_Update(t *testing.T) {
source := fmt.Sprintf("oci://%s/%s:latest", registryHost, pluginName)
installer, err := NewOCIInstaller(source, getter.WithPlainHTTP(true))
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
require.NoError(t, err)
// Test update when plugin does not exist - should fail
err = Update(installer)
if err == nil {
t.Error("Expected error when updating plugin that does not exist")
} else if !strings.Contains(err.Error(), "plugin does not exist") {
t.Errorf("Expected 'plugin does not exist' error, got: %v", err)
}
require.ErrorContains(t, Update(installer), "plugin does not exist")
// Install plugin first
if err := Install(installer); err != nil {
t.Fatalf("Expected installation to succeed, got error: %v", err)
}
require.NoErrorf(t, Install(installer), "Expected installation to succeed")
// Verify plugin was installed
if !isPlugin(installer.Path()) {
t.Errorf("Expected plugin directory %s to contain plugin.yaml", installer.Path())
}
assert.Truef(t, isPlugin(installer.Path()), "Expected plugin directory %s to contain plugin.yaml", installer.Path())
// Test update when plugin exists - should succeed
// For OCI, Update() removes old version and reinstalls
if err := Update(installer); err != nil {
t.Errorf("Expected update to succeed, got error: %v", err)
}
require.NoErrorf(t, Update(installer), "Expected update to succeed")
// Verify plugin is still installed after update
if !isPlugin(installer.Path()) {
t.Errorf("Expected plugin directory %s to contain plugin.yaml after update", installer.Path())
}
assert.Truef(t, isPlugin(installer.Path()), "Expected plugin directory %s to contain plugin.yaml after update", installer.Path())
}
func TestOCIInstaller_Install_ComponentExtraction(t *testing.T) {
@ -518,35 +440,26 @@ func TestOCIInstaller_Install_ComponentExtraction(t *testing.T) {
pluginData := createTestPluginTarGz(t, pluginName)
// Test extraction
err := extractTarGz(bytes.NewReader(pluginData), tempDir)
if err != nil {
t.Fatalf("Failed to extract plugin: %v", err)
}
require.NoError(t, extractTarGz(bytes.NewReader(pluginData), tempDir), "Failed to extract plugin")
// 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")
}
_, err := os.Stat(pluginYAMLPath)
assert.False(t, os.IsNotExist(err), "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")
}
_, err = os.Stat(binPath)
assert.False(t, os.IsNotExist(err), "bin directory not found after extraction")
// Verify executable exists and has correct permissions
execPath := filepath.Join(tempDir, "bin", pluginName)
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")
}
info, err := os.Stat(execPath)
require.NoError(t, err, "executable not found")
assert.NotEqual(t, 0, info.Mode()&0o111, "file is not executable")
// Verify this would be recognized as a plugin
if !isPlugin(tempDir) {
t.Errorf("extracted directory is not a valid plugin")
}
assert.True(t, isPlugin(tempDir), "extracted directory is not a valid plugin")
}
func TestExtractTarGz(t *testing.T) {
@ -561,55 +474,42 @@ func TestExtractTarGz(t *testing.T) {
testContent := "test content"
header := &tar.Header{
Name: "test-file.txt",
Mode: 0644,
Mode: 0o644,
Size: int64(len(testContent)),
Typeflag: tar.TypeReg,
}
if err := tarWriter.WriteHeader(header); err != nil {
t.Fatal(err)
}
require.NoError(t, tarWriter.WriteHeader(header))
if _, err := tarWriter.Write([]byte(testContent)); err != nil {
t.Fatal(err)
}
_, err := tarWriter.Write([]byte(testContent))
require.NoError(t, err)
// Add a test directory
dirHeader := &tar.Header{
Name: "test-dir/",
Mode: 0755,
Mode: 0o755,
Typeflag: tar.TypeDir,
}
if err := tarWriter.WriteHeader(dirHeader); err != nil {
t.Fatal(err)
}
require.NoError(t, tarWriter.WriteHeader(dirHeader))
tarWriter.Close()
gzWriter.Close()
// Test extraction
err := extractTarGz(bytes.NewReader(buf.Bytes()), tempDir)
if err != nil {
t.Errorf("extractTarGz failed: %v", err)
}
require.NoError(t, extractTarGz(bytes.NewReader(buf.Bytes()), tempDir), "extractTarGz failed")
// Verify extracted file
extractedFile := filepath.Join(tempDir, "test-file.txt")
content, err := os.ReadFile(extractedFile)
if err != nil {
t.Errorf("failed to read extracted file: %v", err)
}
require.NoError(t, err, "failed to read extracted file")
if string(content) != testContent {
t.Errorf("expected content %s, got %s", testContent, string(content))
}
assert.Equal(t, testContent, string(content), "expected content %s, got %s", testContent, string(content))
// Verify extracted directory
extractedDir := filepath.Join(tempDir, "test-dir")
if _, err := os.Stat(extractedDir); os.IsNotExist(err) {
t.Errorf("extracted directory does not exist: %s", extractedDir)
}
_, err = os.Stat(extractedDir)
assert.Falsef(t, os.IsNotExist(err), "extracted directory does not exist: %s", extractedDir)
}
func TestExtractTarGz_InvalidGzip(t *testing.T) {
@ -617,10 +517,7 @@ func TestExtractTarGz_InvalidGzip(t *testing.T) {
// Test with invalid gzip data
invalidGzipData := []byte("not gzip data")
err := extractTarGz(bytes.NewReader(invalidGzipData), tempDir)
if err == nil {
t.Error("expected error for invalid gzip data")
}
assert.Error(t, extractTarGz(bytes.NewReader(invalidGzipData), tempDir), "expected error for invalid gzip data")
}
func TestExtractTar_UnknownFileType(t *testing.T) {
@ -634,41 +531,29 @@ func TestExtractTar_UnknownFileType(t *testing.T) {
testContent := "test content"
header := &tar.Header{
Name: "test-file.txt",
Mode: 0644,
Mode: 0o644,
Size: int64(len(testContent)),
Typeflag: tar.TypeReg,
}
if err := tarWriter.WriteHeader(header); err != nil {
t.Fatal(err)
}
require.NoError(t, tarWriter.WriteHeader(header))
if _, err := tarWriter.Write([]byte(testContent)); err != nil {
t.Fatal(err)
}
_, err := tarWriter.Write([]byte(testContent))
require.NoError(t, err)
// Test unknown file type
unknownHeader := &tar.Header{
Name: "unknown-type",
Mode: 0644,
Mode: 0o644,
Typeflag: tar.TypeSymlink, // Use a type that's not handled
}
if err := tarWriter.WriteHeader(unknownHeader); err != nil {
t.Fatal(err)
}
require.NoError(t, tarWriter.WriteHeader(unknownHeader))
tarWriter.Close()
// Test extraction - should fail due to unknown type
err := extractTar(bytes.NewReader(buf.Bytes()), tempDir)
if err == nil {
t.Error("expected error for unknown tar file type")
}
if !strings.Contains(err.Error(), "unknown type") {
t.Errorf("expected 'unknown type' error, got: %v", err)
}
assert.ErrorContains(t, extractTar(bytes.NewReader(buf.Bytes()), tempDir), "unknown type")
}
func TestExtractTar_SuccessfulExtraction(t *testing.T) {
@ -686,37 +571,27 @@ func TestExtractTar_SuccessfulExtraction(t *testing.T) {
testContent := "test content"
header := &tar.Header{
Name: "test-file.txt",
Mode: 0644,
Mode: 0o644,
Size: int64(len(testContent)),
Typeflag: tar.TypeReg,
}
if err := tarWriter.WriteHeader(header); err != nil {
t.Fatal(err)
}
require.NoError(t, tarWriter.WriteHeader(header))
if _, err := tarWriter.Write([]byte(testContent)); err != nil {
t.Fatal(err)
}
_, err := tarWriter.Write([]byte(testContent))
require.NoError(t, err)
tarWriter.Close()
// Test extraction
err := extractTar(bytes.NewReader(buf.Bytes()), tempDir)
if err != nil {
t.Errorf("extractTar failed: %v", err)
}
require.NoError(t, extractTar(bytes.NewReader(buf.Bytes()), tempDir), "extractTar failed")
// Verify the regular file was extracted
extractedFile := filepath.Join(tempDir, "test-file.txt")
content, err := os.ReadFile(extractedFile)
if err != nil {
t.Errorf("failed to read extracted file: %v", err)
}
require.NoError(t, err, "failed to read extracted file")
if string(content) != testContent {
t.Errorf("expected content %s, got %s", testContent, string(content))
}
assert.Equal(t, testContent, string(content), "expected content %s, got %s", testContent, string(content))
}
func TestOCIInstaller_Install_PlainHTTPOption(t *testing.T) {
@ -725,41 +600,25 @@ func TestOCIInstaller_Install_PlainHTTPOption(t *testing.T) {
// Test with PlainHTTP=false (default)
installer1, err := NewOCIInstaller(source)
if err != nil {
t.Fatalf("failed to create installer: %v", err)
}
if installer1.getter == nil {
t.Error("getter should be initialized")
}
require.NoError(t, err, "failed to create installer")
assert.NotNil(t, installer1.getter, "getter should be initialized")
// Test with PlainHTTP=true
installer2, err := NewOCIInstaller(source, getter.WithPlainHTTP(true))
if err != nil {
t.Fatalf("failed to create installer with PlainHTTP=true: %v", err)
}
if installer2.getter == nil {
t.Error("getter should be initialized with PlainHTTP=true")
}
require.NoError(t, err, "failed to create installer with PlainHTTP=true")
assert.NotNil(t, installer2.getter, "getter should be initialized with PlainHTTP=true")
// Both installers should have the same basic properties
if installer1.PluginName != installer2.PluginName {
t.Error("plugin names should match")
}
if installer1.Source != installer2.Source {
t.Error("sources should match")
}
assert.Equal(t, installer2.PluginName, installer1.PluginName, "plugin names should match")
assert.Equal(t, installer2.Source, installer1.Source, "sources should match")
// Test with multiple options
installer3, err := NewOCIInstaller(source,
getter.WithPlainHTTP(true),
getter.WithBasicAuth("user", "pass"),
)
if err != nil {
t.Fatalf("failed to create installer with multiple options: %v", err)
}
if installer3.getter == nil {
t.Error("getter should be initialized with multiple options")
}
require.NoError(t, err, "failed to create installer with multiple options")
assert.NotNil(t, installer3.getter, "getter should be initialized with multiple options")
}
func TestOCIInstaller_Install_ValidationErrors(t *testing.T) {
@ -794,12 +653,8 @@ func TestOCIInstaller_Install_ValidationErrors(t *testing.T) {
// Test the gzip validation logic that's used in the Install method
if len(tt.layerData) < 2 || tt.layerData[0] != 0x1f || tt.layerData[1] != 0x8b {
// This matches the validation in the Install method
if !tt.expectError {
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'")
}
assert.True(t, tt.expectError, "expected valid gzip data")
assert.Contains(t, tt.errorMsg, "is not a gzip compressed archive")
}
})
}

@ -53,7 +53,7 @@ func detectPluginRoot(extractDir string) (string, error) {
// validatePluginName checks if the plugin directory name matches the plugin name
// from plugin.yaml when the plugin is in a subdirectory.
func validatePluginName(pluginRoot string, expectedName string) error {
func validatePluginName(pluginRoot, expectedName string) error {
// Only validate if plugin is in a subdirectory
dirName := filepath.Base(pluginRoot)
if dirName == expectedName {

@ -19,6 +19,9 @@ import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDetectPluginRoot(t *testing.T) {
@ -31,7 +34,7 @@ func TestDetectPluginRoot(t *testing.T) {
{
name: "plugin.yaml at root",
setup: func(dir string) error {
return os.WriteFile(filepath.Join(dir, "plugin.yaml"), []byte("name: test"), 0644)
return os.WriteFile(filepath.Join(dir, "plugin.yaml"), []byte("name: test"), 0o644)
},
expectRoot: ".",
expectError: false,
@ -40,10 +43,10 @@ func TestDetectPluginRoot(t *testing.T) {
name: "plugin.yaml in subdirectory",
setup: func(dir string) error {
subdir := filepath.Join(dir, "my-plugin")
if err := os.MkdirAll(subdir, 0755); err != nil {
if err := os.MkdirAll(subdir, 0o755); err != nil {
return err
}
return os.WriteFile(filepath.Join(subdir, "plugin.yaml"), []byte("name: test"), 0644)
return os.WriteFile(filepath.Join(subdir, "plugin.yaml"), []byte("name: test"), 0o644)
},
expectRoot: "my-plugin",
expectError: false,
@ -51,7 +54,7 @@ func TestDetectPluginRoot(t *testing.T) {
{
name: "no plugin.yaml",
setup: func(dir string) error {
return os.WriteFile(filepath.Join(dir, "README.md"), []byte("test"), 0644)
return os.WriteFile(filepath.Join(dir, "README.md"), []byte("test"), 0o644)
},
expectRoot: "",
expectError: true,
@ -60,10 +63,10 @@ func TestDetectPluginRoot(t *testing.T) {
name: "plugin.yaml in nested subdirectory (should not find)",
setup: func(dir string) error {
subdir := filepath.Join(dir, "outer", "inner")
if err := os.MkdirAll(subdir, 0755); err != nil {
if err := os.MkdirAll(subdir, 0o755); err != nil {
return err
}
return os.WriteFile(filepath.Join(subdir, "plugin.yaml"), []byte("name: test"), 0644)
return os.WriteFile(filepath.Join(subdir, "plugin.yaml"), []byte("name: test"), 0o644)
},
expectRoot: "",
expectError: true,
@ -73,26 +76,18 @@ func TestDetectPluginRoot(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dir := t.TempDir()
if err := tt.setup(dir); err != nil {
t.Fatalf("Setup failed: %v", err)
}
require.NoErrorf(t, tt.setup(dir), "Setup failed")
root, err := detectPluginRoot(dir)
if tt.expectError {
if err == nil {
t.Error("Expected error but got none")
}
assert.Error(t, err)
} else {
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
require.NoError(t, err)
expectedPath := dir
if tt.expectRoot != "." {
expectedPath = filepath.Join(dir, tt.expectRoot)
}
if root != expectedPath {
t.Errorf("Expected root %s but got %s", expectedPath, root)
}
assert.Equal(t, expectedPath, root, "Expected root %s but got %s", expectedPath, root)
}
})
}
@ -110,14 +105,14 @@ func TestValidatePluginName(t *testing.T) {
name: "matching directory and plugin name",
setup: func(dir string) error {
subdir := filepath.Join(dir, "my-plugin")
if err := os.MkdirAll(subdir, 0755); err != nil {
if err := os.MkdirAll(subdir, 0o755); err != nil {
return err
}
yaml := `name: my-plugin
version: 1.0.0
usage: test
description: test`
return os.WriteFile(filepath.Join(subdir, "plugin.yaml"), []byte(yaml), 0644)
return os.WriteFile(filepath.Join(subdir, "plugin.yaml"), []byte(yaml), 0o644)
},
pluginRoot: "my-plugin",
expectedName: "my-plugin",
@ -127,14 +122,14 @@ description: test`
name: "different directory and plugin name",
setup: func(dir string) error {
subdir := filepath.Join(dir, "wrong-name")
if err := os.MkdirAll(subdir, 0755); err != nil {
if err := os.MkdirAll(subdir, 0o755); err != nil {
return err
}
yaml := `name: my-plugin
version: 1.0.0
usage: test
description: test`
return os.WriteFile(filepath.Join(subdir, "plugin.yaml"), []byte(yaml), 0644)
return os.WriteFile(filepath.Join(subdir, "plugin.yaml"), []byte(yaml), 0o644)
},
pluginRoot: "wrong-name",
expectedName: "wrong-name",
@ -145,20 +140,14 @@ description: test`
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dir := t.TempDir()
if err := tt.setup(dir); err != nil {
t.Fatalf("Setup failed: %v", err)
}
require.NoErrorf(t, tt.setup(dir), "Setup failed")
pluginRoot := filepath.Join(dir, tt.pluginRoot)
err := validatePluginName(pluginRoot, tt.expectedName)
if tt.expectError {
if err == nil {
t.Error("Expected error but got none")
}
assert.Error(t, err)
} else {
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
assert.NoError(t, err)
}
})
}

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

Loading…
Cancel
Save