Merge upstream/main into fix-issue-4256

Resolves a conflict in pkg/cmd/list_test.go: upstream migrated the
release-count assertion to assert.Len while this branch added the
showSource parameter to newReleaseListWriter. Kept both.
pull/32250/head
Shaan Satsangi 4 weeks ago
commit c5e0c8e8c8

@ -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:
- "*"

2
.github/env vendored

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

@ -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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # pin@v7.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@4a3601121dd01d1626a1e23e37211e3254c1c06c # pin@6.4.0
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # pin@7.0.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true

@ -44,11 +44,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # pin@v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # pin@v7.0.1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # pinv4.36.2
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # pinv4.37.6
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -59,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@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # pinv4.36.2
uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # pinv4.37.6
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -73,4 +73,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # pinv4.36.2
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # pinv4.37.6

@ -13,15 +13,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # pin@v7.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@4a3601121dd01d1626a1e23e37211e3254c1c06c # pin@6.4.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@82606bf257cbaff209d206a39f5134f0cfbfd2ee #pin@9.2.1
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a #pin@9.3.0
with:
version: ${{ env.GOLANGCI_LINT_VERSION }}

@ -19,17 +19,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # pin@v7.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@4a3601121dd01d1626a1e23e37211e3254c1c06c # pin@6.4.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # pin@v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # pin@v7.0.1
with:
fetch-depth: 0
@ -28,7 +28,7 @@ jobs:
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # pin@6.4.0
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # pin@7.0.0
with:
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true
@ -85,7 +85,7 @@ jobs:
if: github.ref == 'refs/heads/main' && github.repository == 'helm/helm'
steps:
- name: Checkout source code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # pin@v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # pin@v7.0.1
with:
fetch-depth: 0
@ -93,7 +93,7 @@ jobs:
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # pin@6.4.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.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
@ -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@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
with:
sarif_file: results.sarif

@ -12,7 +12,7 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.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.'

10
.gitignore vendored

@ -3,7 +3,6 @@
.DS_Store
.coverage/
.idea
.claude
.vimrc
.vscode/
.devcontainer/
@ -11,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,10 @@ formatters:
gofmt:
simplify: true
gofumpt:
module-path: helm.sh/helm/v4
extra-rules: false
goimports:
local-prefixes:
- helm.sh/helm/v4
@ -27,7 +32,7 @@ linters:
- exhaustive
- fatcontext
- gocritic
- gomodguard
- gomodguard_v2
- govet
- ineffassign
- misspell
@ -47,7 +52,7 @@ linters:
- whitespace
exclusions:
generated: lax
presets:
@ -74,9 +79,9 @@ linters:
errorlint:
# Check for plain type assertions and type switches.
asserts: false
asserts: true
# Check for plain error comparisons.
comparison: false
comparison: true
exhaustive:
default-signifies-exhaustive: true
@ -84,51 +89,27 @@ linters:
gocritic:
disabled-checks:
- appendAssign
- appendCombine
- assignOp
- badCall
- commentedOutCode
- commentFormatting
- deferInLoop
- elseif
- emptyStringTest
- exposedSyncMutex
- filepathJoin
- httpNoBody
- hugeParam
- ifElseChain
- importShadow
- nestingReduce
- nilValReturn
- octalLiteral
- paramTypeCombine
- preferStringWriter
- ptrToRefParam
- rangeValCopy
- regexpSimplify
- singleCaseSwitch
- sloppyReassign
- sloppyLen
- sprintfQuotedString
- stringConcatSimplify
- stringsCompare
- stringXbytes
- todoCommentWithoutDetail
- tooManyResultsChecker
- typeAssertChain
- uncheckedInlineErr
- unnamedResult
- unnecessaryDefer
- unslice
- whyNoLint
enable-all: true
gomodguard:
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
@ -138,6 +119,16 @@ linters:
enable-default-rules: true
max-open-files: 2048
rules:
- 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:
@ -152,16 +143,6 @@ linters:
- models
testifylint:
disable:
- encoded-compare
- equal-values
- expected-actual
- float-compare
- go-require
- nil-compare
- require-error
- suite-dont-use-pkg
- suite-extra-assert-call
# Intentionally enable all testifylint rules so new checks are adopted automatically.
enable-all: true

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

@ -200,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 \

@ -14,7 +14,7 @@ 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"

@ -18,13 +18,13 @@ package main
import (
"bytes"
"errors"
"os"
"os/exec"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCliPluginExitCode(t *testing.T) {
@ -62,20 +62,13 @@ func TestCliPluginExitCode(t *testing.T) {
err := cmd.Run()
exiterr := &exec.ExitError{}
ok := errors.As(err, &exiterr)
if !ok {
t.Fatalf("Unexpected error type returned by os.Exit: %T", err)
}
require.ErrorAs(t, err, &exiterr)
assert.Empty(t, stdout.String())
expectedStderr := "level=WARN msg=\"failed to load plugin (ignoring)\" plugin_yaml=../../pkg/cmd/testdata/helmhome/helm/plugins/noversion/plugin.yaml error=\"failed to load plugin \\\"../../pkg/cmd/testdata/helmhome/helm/plugins/noversion\\\": plugin `version` is required\"\nError: plugin \"exitwith\" exited with error\n"
if stderr.String() != expectedStderr {
t.Errorf("Expected %q written to stderr: Got %q", expectedStderr, stderr.String())
}
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())
}
}

@ -17,37 +17,37 @@ require (
github.com/evanphx/json-patch/v5 v5.9.11
github.com/extism/go-sdk v1.7.1
github.com/fatih/color v1.19.0
github.com/fluxcd/cli-utils v1.2.1
github.com/fluxcd/cli-utils v1.2.2
github.com/foxcpp/go-mockdns v1.2.0
github.com/gobwas/glob v0.2.3
github.com/gofrs/flock v0.13.0
github.com/gosuri/uitable v0.0.4
github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.12.3
github.com/mattn/go-shellwords v1.0.13
github.com/mattn/go-shellwords v1.0.14
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.1
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
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.12.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.53.0
golang.org/x/term v0.44.0
golang.org/x/text v0.38.0
go.yaml.in/yaml/v3 v3.0.5
golang.org/x/crypto v0.55.0
golang.org/x/term v0.45.0
golang.org/x/text v0.41.0
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.36.2
k8s.io/apiextensions-apiserver v0.36.2
k8s.io/apimachinery v0.36.2
k8s.io/apiserver v0.36.2
k8s.io/cli-runtime v0.36.2
k8s.io/client-go v0.36.2
k8s.io/api v0.36.3
k8s.io/apiextensions-apiserver v0.36.3
k8s.io/apimachinery v0.36.3
k8s.io/apiserver v0.36.3
k8s.io/cli-runtime v0.36.3
k8s.io/client-go v0.36.3
k8s.io/klog/v2 v2.140.0
k8s.io/kubectl v0.36.2
oras.land/oras-go/v2 v2.6.1
k8s.io/kubectl v0.36.3
oras.land/oras-go/v2 v2.6.2
sigs.k8s.io/controller-runtime v0.24.1
sigs.k8s.io/kustomize/kyaml v0.21.1
sigs.k8s.io/yaml v1.6.0
@ -80,6 +80,7 @@ require (
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect
@ -115,7 +116,7 @@ require (
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/gomega v1.39.1 // indirect
github.com/onsi/gomega v1.42.1 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
@ -137,7 +138,7 @@ require (
go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 // indirect
go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 // indirect
@ -150,32 +151,32 @@ require (
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 // indirect
go.opentelemetry.io/otel/log v0.19.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/mod v0.38.0 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.45.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/grpc v1.80.0 // indirect
golang.org/x/tools v0.48.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
google.golang.org/grpc v1.82.1 // indirect
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/component-base v0.36.2 // indirect
k8s.io/component-base v0.36.3 // indirect
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/kustomize/api v0.21.1 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.3 // indirect
)

131
go.sum

@ -93,8 +93,8 @@ 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 v1.2.1 h1:ug9CicKW7H9QXnvNDapTSKuryZvWcu4Nw7pRvQa6jDY=
github.com/fluxcd/cli-utils v1.2.1/go.mod h1:cky6M6eHvTQkoPtsuFYLIgAMYdpTCSLoor4IA6vueSw=
github.com/fluxcd/cli-utils v1.2.2 h1:adDOmwE+LSwTzmYUaoEFPblruOuaQEKAg1ZNTmPJObE=
github.com/fluxcd/cli-utils v1.2.2/go.mod h1:FsghNGY+3Sr70c0FOB7I5So0kzoYVdvQ8GTid3XXVWM=
github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0=
github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@ -105,6 +105,8 @@ github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8b
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=
@ -203,8 +205,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
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.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXNtDcf4=
github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-shellwords v1.0.14 h1:yUKzIgsCnosndOASY6/enly1EAuaXeFSQ7cdyA3OuYg=
github.com/mattn/go-shellwords v1.0.14/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@ -233,8 +235,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
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=
@ -280,8 +282,8 @@ github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy
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=
@ -326,8 +328,8 @@ go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 h1:4fnRcNpc6YFtG3zsFw9a
go.opentelemetry.io/contrib/exporters/autoexport v0.67.0/go.mod h1:qTvIHMFKoxW7HXg02gm6/Wofhq5p3Ib/A/NNt1EoBSQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 h1:deI9UQMoGFgrg5iLPgzueqFPHevDl+28YKfSpPTI6rY=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0/go.mod h1:PFx9NgpNUKXdf7J4Q3agRxMs3Y07QhTCVipKmLsMKnU=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag=
@ -352,18 +354,20 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1J
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs=
go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4=
go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko=
go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg=
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk=
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@ -374,22 +378,23 @@ go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
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-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.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
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.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -400,8 +405,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
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=
@ -412,8 +417,8 @@ 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.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -430,8 +435,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -439,8 +444,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.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
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=
@ -448,8 +453,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
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=
@ -458,17 +463,17 @@ 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.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec=
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
@ -485,30 +490,30 @@ 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.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY=
k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg=
k8s.io/apiextensions-apiserver v0.36.2 h1:3O5gqOj/dt2XWWbpMe+TXWpE9yU6pjM/tXxtHHJT/K4=
k8s.io/apiextensions-apiserver v0.36.2/go.mod h1:cL1tBWe8XSaP1H30iWKGo7hf6iAUUUJPEU70dskmAnA=
k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ=
k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4=
k8s.io/apiserver v0.36.2 h1:6vMnkmHZPeBloNkHUhmZYq7Ylv8WIB8xjyEl+eSt26E=
k8s.io/apiserver v0.36.2/go.mod h1:9PoQ2ikCytrZyZg11mGhLEF5m8Rgsb5FJmYJ4Wvnl1k=
k8s.io/cli-runtime v0.36.2 h1:CconTvEeV4DJs4ZX3HQKCFbFRGsm6OtuBM9yjmMP2VM=
k8s.io/cli-runtime v0.36.2/go.mod h1:LddcjiMf4YlnHO7c1Y7rEtDqL84FyiYVLco7V679GUU=
k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI=
k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0=
k8s.io/component-base v0.36.2 h1:Z0VH80O7Ng0HDZnZj3WRR3urEGa0kTwmO8CwEwjVK1w=
k8s.io/component-base v0.36.2/go.mod h1:mGfFOA7Gwpdm1VW2cwSQYbiDIlz8GD2WGwH88QSeCyA=
k8s.io/api v0.36.3 h1:NxB+05W2UGqXWFXcLO0RB5cnqnUPP5v5sVlaOH0Iz4w=
k8s.io/api v0.36.3/go.mod h1:JzLQKqRHC5+I8RVj/lS3lCg0mg6nWI9Fo/Sk3ElxHzg=
k8s.io/apiextensions-apiserver v0.36.3 h1:dPmOAPhwTtqb1bTxbFPsy18KHPhktQeO3WUPXunZIB0=
k8s.io/apiextensions-apiserver v0.36.3/go.mod h1:KTXFqgXiuw2pRoL+Wpmttqc+up9Xt/GohadPWeLLOa4=
k8s.io/apimachinery v0.36.3 h1:PkzMRBRG8joFD8EhCuQAtNPvJlxb82FwplP26HIzvAM=
k8s.io/apimachinery v0.36.3/go.mod h1:cTSjBWgPe/6CQyBKzY/hDIRWCQQQeK0mfLbml0UYFHE=
k8s.io/apiserver v0.36.3 h1:MGSg2SkdfuytiDEcRylT5mQFmmSsbx90XFUO67Y4bsQ=
k8s.io/apiserver v0.36.3/go.mod h1:fVH7zv9EUNUA7Fl7LtDKh8aB9W7u1VQPSGtWV5SjUxg=
k8s.io/cli-runtime v0.36.3 h1:g+eJ+M1sYpnNYp/q5fzaw2KejIL0Q7DH+xFl6YVoL4U=
k8s.io/cli-runtime v0.36.3/go.mod h1:hZpAqK8nSFXvvLaVCbzUPVp8e9TRLSTCfpNzMt7s3tE=
k8s.io/client-go v0.36.3 h1:M4JdVzXxYcZk4fGpfDdYnxSwhLKWCFoQsHW6t+z8Hfg=
k8s.io/client-go v0.36.3/go.mod h1:gcPwr0c87vjjG6HB6pWEqOeuYVoXSsREjzux2j6GF30=
k8s.io/component-base v0.36.3 h1:vc/UFvPCkW0irPz84LAodAL1j3f4xktPM6dDJIEheAY=
k8s.io/component-base v0.36.3/go.mod h1:hZbNFG+gCMl9EbykDGEu73feKP9/Cq6JsV4pTo9GTO8=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
k8s.io/kubectl v0.36.2 h1:rpUGGpeL09XVOLep2yle5jrtk//JA1L6ZHfkQQtVEwk=
k8s.io/kubectl v0.36.2/go.mod h1:gVbQ3B/yb4bSR2ggQ7rd0W6icUSWs7sduH4e16Vii+0=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
oras.land/oras-go/v2 v2.6.1 h1:bonOEkjLfp8tt6qXWRRWP6p1F+9octchOf2EqnWB4Zs=
oras.land/oras-go/v2 v2.6.1/go.mod h1:dhtFrFOuZuDtAVeZ9FUnaa5zfzplG3ZnFX9/uH1J/Yk=
k8s.io/kubectl v0.36.3 h1:TesKp+XYQEjPYoFvuobcVnuvira2+/xAVlq//+kksaI=
k8s.io/kubectl v0.36.3/go.mod h1:W+NEb1CzBGmoaI1Nrpn2ETo9omNBl0AsyxnnMT40N6E=
k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 h1:wU4tMEhLGgIbLvXQb1cfN+EcM0wf7zC6CPF+C79jroc=
k8s.io/utils v0.0.0-20260507154919-ff6756f316d2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
oras.land/oras-go/v2 v2.6.2 h1:N04RXngAp1LJKTG6ifz3xHPipasEkWr+hFmInja5YKo=
oras.land/oras-go/v2 v2.6.2/go.mod h1:PlTtg4JTDJkDe8yVHpM2wz7/YDc00GVas+i4jAW2TZ4=
sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4=
sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
@ -519,7 +524,7 @@ sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7
sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8=
sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/structured-merge-diff/v6 v6.3.3 h1:u08YRbVUi59ri4YD6cg0UqNM4Dimn0sIl+wldcx5PYw=
sigs.k8s.io/structured-merge-diff/v6 v6.3.3/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=

@ -21,6 +21,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/pkg/chart/common"
)
@ -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)
}

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

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

@ -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.Error("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.Error("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")
}
}
@ -177,10 +162,7 @@ func TestHelmCreateChart(t *testing.T) {
// of the `--set` flag.
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:
@ -210,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)
}
@ -233,11 +214,7 @@ func TestMalformedTemplate(t *testing.T) {
case <-c:
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.Error("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"
@ -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

@ -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.Error("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.Error("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.Error("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.Error("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.Error("validateChartMaintainer did not return error for nil maintainer as expected")
}
if err.Error() != "a maintainer entry is empty" {
t.Errorf("validateChartMaintainer returned unexpected error for nil maintainer: %s", err.Error())
}
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.Error("validateChartIconPresence to return a linter error, got no error")
} else if !strings.Contains(err.Error(), "icon is recommended") {
t.Errorf("expected %q, got %q", "icon is recommended", err.Error())
}
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")
})
}

@ -22,6 +22,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/internal/chart/v3/lint/support"
)
@ -33,7 +34,7 @@ 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")
}
@ -45,11 +46,11 @@ func TestCrdWithEmptyDocument(t *testing.T) {
`apiVersion: v1
name: test
version: 0.1.0
`), 0644)
`), 0o644)
// CRD with comments before --- (creates empty document)
crdsDir := filepath.Join(chartDir, "crds")
os.Mkdir(crdsDir, 0755)
os.Mkdir(crdsDir, 0o755)
os.WriteFile(filepath.Join(crdsDir, "test.yaml"), []byte(
`# Comments create empty document
---
@ -57,7 +58,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: test.example.io
`), 0644)
`), 0o644)
linter := support.Linter{ChartDir: chartDir}
Crds(&linter)

@ -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.Error("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.Error("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"

@ -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.Error("Expected a v1 Pod to not be deprecated")
}
}, nil), "Expected a v1 Pod to not be deprecated")
}

@ -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]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
@ -262,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)
}
@ -306,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
@ -330,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
@ -351,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) {
@ -366,8 +346,11 @@ 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)
}
}
}
@ -392,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",
@ -424,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
@ -439,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) {
@ -457,8 +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)
}
}

@ -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.Error("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]any{}, 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]any{}, 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]any{}, 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,10 +96,7 @@ func TestValidateValuesFileSchemaFailureButWithSkipSchemaValidation(t *testing.T
valfile := filepath.Join(tmpdir, "values.yaml")
err := validateValuesFile(valfile, map[string]any{}, 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) {
@ -122,9 +108,7 @@ func TestValidateValuesFileSchemaOverrides(t *testing.T) {
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) {
@ -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")
}

@ -114,7 +114,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
}

@ -25,12 +25,14 @@ import (
"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"
@ -38,13 +40,9 @@ 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)
@ -57,33 +55,24 @@ func TestLoadDirWithDevNull(t *testing.T) {
}
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.Error("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)
@ -100,53 +89,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 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)
@ -156,13 +129,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)
@ -172,13 +141,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)
@ -232,36 +197,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
@ -317,9 +262,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() {
@ -327,25 +270,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 +290,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 +302,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,37 +346,27 @@ 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) {
@ -495,12 +413,8 @@ 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)
})
}
}
@ -528,22 +442,13 @@ func TestMergeValuesV3(t *testing.T) {
}
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]any{
@ -554,32 +459,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,62 +491,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)
}
}
@ -661,53 +535,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())
}
@ -717,8 +563,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)
}
}

@ -16,8 +16,10 @@ limitations under the License.
package v3
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestValidate(t *testing.T) {
@ -192,21 +194,13 @@ func TestValidate(t *testing.T) {
for _, tt := range tests {
result := tt.md.Validate()
if !errors.Is(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 {
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.Error("Unexpected maintainer name.")
}
if f.Maintainers[1].Email != "nobody@example.com" {
t.Error("Unexpected maintainer email.")
}
if len(f.Sources) != 1 {
t.Fatal("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.Fatal("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.Error("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)
}
}

@ -803,7 +803,7 @@ func Create(name, dir string) (string, error) {
}
}
// 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
@ -816,10 +816,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 {

@ -22,6 +22,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/loader"
)
@ -30,20 +33,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 +55,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 +70,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)
}
}
@ -115,33 +100,25 @@ func TestCreate_Overwrite(t *testing.T) {
var errlog bytes.Buffer
if _, err := Create("foo", tdir); err != nil {
t.Fatal(err)
}
_, 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.Error("Expected warnings about overwriting files.")
}
assert.NotEqual(t, 0, errlog.Len(), "Expected warnings about overwriting files.")
}
func TestValidateChartName(t *testing.T) {
@ -165,8 +142,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)
}
}
}

@ -43,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)
var errNoValue common.ErrNoValue

@ -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")
@ -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,127 +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.Fatal("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.Fatal("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.Fatal("dependency chart version outside the compatible range should not be considered compatible")
}
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) {
@ -484,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)
}

@ -88,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
}
}

@ -47,19 +47,19 @@ func makeTestChartArchive(t *testing.T, chartName, sourceDir string) *bytes.Buff
fStat, err := f.Stat()
require.NoError(t, err)
err = tw.WriteHeader(&tar.Header{
require.NoError(t, tw.WriteHeader(&tar.Header{
Name: filepath.Join(chartName, relPath),
Mode: int64(fStat.Mode()),
Size: fStat.Size(),
})
require.NoError(t, err)
}))
data, err := fs.ReadFile(dir, relPath)
require.NoError(t, err)
tw.Write(data)
_, err = tw.Write(data)
require.NoError(t, err)
}
err := fs.WalkDir(dir, ".", func(path string, d os.DirEntry, walkErr error) error {
require.NoError(t, fs.WalkDir(dir, ".", func(path string, d os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
@ -71,15 +71,10 @@ func makeTestChartArchive(t *testing.T, chartName, sourceDir string) *bytes.Buff
writeFile(path)
return nil
})
if err != nil {
t.Fatal(err)
}
}))
err = tw.Close()
require.NoError(t, err)
err = gw.Close()
require.NoError(t, err)
require.NoError(t, tw.Close())
require.NoError(t, gw.Close())
return &result
}
@ -88,51 +83,35 @@ 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())
}
}
}
@ -153,8 +132,7 @@ func TestExpandError(t *testing.T) {
archive := makeTestChartArchive(t, tt.chartName, tt.chartDir)
dest := t.TempDir()
err := Expand(dest, archive)
assert.ErrorContains(t, err, tt.wantErr)
assert.ErrorContains(t, Expand(dest, archive), tt.wantErr)
})
}
}
@ -162,47 +140,31 @@ func TestExpandError(t *testing.T) {
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,
}

@ -32,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"
@ -59,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
@ -86,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.Fatal("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")
})
}
@ -122,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.
@ -165,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)
}
}
@ -234,37 +209,25 @@ 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) {
@ -325,18 +288,12 @@ func TestRepeatableSave(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)
}
require.NoError(t, err, "Failed to save")
// get shasum for package
result, err := sha256Sum(where)
if err != nil {
t.Fatalf("Failed to check shasum: %s", err)
}
require.NoError(t, err, "Failed to check shasum")
// assert that the package SHA is what we wanted.
if result != test.want {
t.Errorf("FormatName() result = %v, want %v", result, test.want)
}
assert.Equal(t, test.want, result, "FormatName() result = %v, want %v", result, test.want)
})
}
}

@ -16,7 +16,11 @@ limitations under the License.
package util
import "testing"
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestValidateReleaseName is a regression test for ValidateName
//
@ -46,12 +50,11 @@ func TestValidateReleaseName(t *testing.T) {
"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)
err := ValidateReleaseName(input)
if expectPass {
assert.NoErrorf(t, err, "Expected release name %q to pass validation", input)
} else {
assert.Errorf(t, err, "Expected release name %q to fail validation, but it passed", input)
}
}
}
@ -80,12 +83,11 @@ func TestValidateMetadataName(t *testing.T) {
"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)
err := ValidateMetadataName(input)
if expectPass {
assert.NoError(t, err, "Expected %q to succeed", input)
} else {
assert.Error(t, err, "Expected %q to fail", input)
}
}
}

@ -20,6 +20,8 @@ import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"helm.sh/helm/v4/pkg/release/common"
)
@ -93,14 +95,12 @@ 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())
})
}
}
@ -139,9 +139,7 @@ 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)
})
}
}
@ -180,9 +178,7 @@ func TestColorizeNamespace(t *testing.T) {
result := ColorizeNamespace(tt.namespace, tt.noColor)
// Always check the namespace text is present
if !strings.Contains(result, tt.namespace) {
t.Errorf("ColorizeNamespace() = %q, want to contain %q", result, tt.namespace)
}
assert.Contains(t, result, tt.namespace)
})
}
}

@ -22,6 +22,9 @@ import (
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestAtomicWriteFile tests the happy path of AtomicWriteFile function.
@ -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,21 +84,14 @@ 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)
if largeContent != string(got) {
t.Fatalf("expected large content to match, got different length: %d vs %d", len(largeContent), len(got))
}
require.Equal(t, largeContent, string(got))
}
// TestPlatformAtomicWriteFile_OverwritesExisting verifies that the platform
@ -127,21 +101,13 @@ func TestPlatformAtomicWriteFile_OverwritesExisting(t *testing.T) {
path := filepath.Join(dir, "overwrite_test")
first := bytes.NewReader([]byte("first"))
if err := PlatformAtomicWriteFile(path, first, 0644); err != nil {
t.Fatalf("first write failed: %v", err)
}
require.NoError(t, PlatformAtomicWriteFile(path, first, 0o644), "first write failed")
second := bytes.NewReader([]byte("second"))
if err := PlatformAtomicWriteFile(path, second, 0644); err != nil {
t.Fatalf("second write failed: %v", err)
}
require.NoError(t, PlatformAtomicWriteFile(path, second, 0o644), "second write failed")
contents, err := os.ReadFile(path)
if err != nil {
t.Fatalf("failed reading result: %v", err)
}
require.NoError(t, err, "failed reading result")
if string(contents) != "second" {
t.Fatalf("expected file to be overwritten, got %q", string(contents))
}
require.Equal(t, "second", string(contents))
}

@ -24,6 +24,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLogHolder_Logger(t *testing.T) {
@ -196,9 +197,8 @@ func TestDebugCheckHandler_Handle(t *testing.T) {
}
record := slog.NewRecord(time.Now(), slog.LevelInfo, "test message", 0)
err := handler.Handle(t.Context(), record)
assert.NoError(t, err)
require.NoError(t, handler.Handle(t.Context(), record))
assert.Contains(t, buf.String(), "test message")
})
@ -213,9 +213,8 @@ func TestDebugCheckHandler_Handle(t *testing.T) {
type testKey string
ctx := context.WithValue(t.Context(), testKey("test"), "value")
record := slog.NewRecord(time.Now(), slog.LevelInfo, "context test", 0)
err := handler.Handle(ctx, record)
assert.NoError(t, err)
require.NoError(t, handler.Handle(ctx, record))
assert.Contains(t, buf.String(), "context test")
})
}

@ -26,7 +26,6 @@ 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
}

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

@ -21,6 +21,9 @@ 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
@ -33,16 +36,10 @@ func TestSearch(t *testing.T) {
defer ts.Close()
c, err := New(ts.URL)
if err != nil {
t.Errorf("unable to create monocular client: %s", err)
}
require.NoError(t, err, "unable to create monocular client")
results, err := c.SearchWithContext(t.Context(), "phpmyadmin")
if err != nil {
t.Errorf("unable to search monocular: %s", err)
}
require.NoError(t, err, "unable to search monocular")
if len(results) != 2 {
t.Error("Did not receive the expected number of results")
}
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"

@ -49,8 +49,7 @@ func TestUnmarshaConfig(t *testing.T) {
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) {
@ -38,8 +40,6 @@ func TestPath(t *testing.T) {
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"
@ -132,7 +132,7 @@ 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
}
@ -153,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))
@ -189,6 +189,6 @@ func stripPluginName(name string) string {
break
}
}
re := regexp.MustCompile(`(.*)-[0-9]+\..*`)
re := regexp.MustCompile(`(.*)-\d+\..*`)
return re.ReplaceAllString(strippedName, `$1`)
}

@ -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"
@ -93,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)
}
@ -112,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)
}
}

@ -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.Error("name does not match expected value")
}
if stripPluginName("fake-plugin-0.0.1.tgz") != "fake-plugin" {
t.Error("name does not match expected value")
}
if stripPluginName("fake-plugin.tgz") != "fake-plugin" {
t.Error("name does not match expected value")
}
if stripPluginName("fake-plugin.tar.gz") != "fake-plugin" {
t.Error("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,45 +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) {
@ -132,20 +112,14 @@ 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{
@ -153,9 +127,7 @@ func TestHTTPInstallerNonExistentVersion(t *testing.T) {
}
// 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) {
@ -164,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) {
@ -215,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{
@ -229,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.
@ -242,59 +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) {
@ -313,16 +255,15 @@ 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)
}
})
}
}
@ -336,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.Error("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)
}
}
}
@ -355,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", "#!/usr/bin/env sh\necho plugin", 0755, tar.TypeReg},
{"docs/", "", 0755, tar.TypeDir},
{"docs/README.md", "readme content", 0644, tar.TypeReg},
{"docs/examples/", "", 0755, tar.TypeDir},
{"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 {
@ -377,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) {
@ -434,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
@ -453,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 {
@ -466,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) {
@ -523,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", "#!/usr/bin/env sh\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 {
@ -536,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
@ -573,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)
}

@ -73,7 +73,7 @@ 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) {
@ -136,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, 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)
}

@ -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.Error("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.Error("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.Error("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)
}
}
@ -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,17 +13,18 @@ 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"
"bytes"
"compress/gzip"
"errors"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/internal/test/ensure"
"helm.sh/helm/v4/pkg/helmpath"
)
@ -34,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 !errors.Is(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) {
@ -87,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", "#!/usr/bin/env sh\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 {
@ -97,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())
}

@ -104,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)
}
@ -123,7 +123,7 @@ 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)
}
}
@ -134,7 +134,7 @@ func (i *OCIInstaller) Install() error {
}
// 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)
}
@ -229,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: "bin/" + pluginName,
Mode: 0755,
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.Error("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.Error("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.Error("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.Error("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.Error("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.Error("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.Error("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")
}
})
}

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

@ -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 (
"errors"

@ -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 (
"fmt"
@ -23,6 +23,7 @@ import (
"testing"
"github.com/Masterminds/vcs"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/internal/test/ensure"
"helm.sh/helm/v4/pkg/helmpath"
@ -52,9 +53,7 @@ func (r *testRepo) UpdateVersion(version string) error {
func TestVCSInstaller(t *testing.T) {
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"))
source := "https://github.com/adamreese/helm-env"
testRepoPath, _ := filepath.Abs("../testdata/plugdir/good/echo-v1")
@ -64,43 +63,27 @@ func TestVCSInstaller(t *testing.T) {
}
i, err := NewForSource(source, "~0.1.0")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
require.NoError(t, err)
// ensure a VCSInstaller was returned
vcsInstaller, ok := i.(*VCSInstaller)
if !ok {
t.Fatal("expected a VCSInstaller")
}
require.True(t, ok, "expected a VCSInstaller")
// set the testRepo in the VCSInstaller
vcsInstaller.Repo = repo
if err := Install(i); err != nil {
t.Fatal(err)
}
if repo.current != "0.1.1" {
t.Fatalf("expected version '0.1.1', got %q", repo.current)
}
require.NoError(t, Install(i))
require.Equal(t, "0.1.1", repo.current, "expected version '0.1.1', got %q", repo.current)
expectedPath := helmpath.DataPath("plugins", "helm-env")
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())
// 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")
// Testing FindSource method, expect error because plugin code is not a cloned repository
if _, err := FindSource(i.Path()); err == nil {
t.Fatal("expected error for inability to find plugin source, got none")
} else if err.Error() != "cannot get information about plugin source" {
t.Fatalf("expected error for inability to find plugin source, got (%v)", err)
}
_, err = FindSource(i.Path())
require.Error(t, err, "expected error for inability to find plugin source, got none")
require.EqualErrorf(t, err, "cannot get information about plugin source", "expected error for inability to find plugin source")
}
func TestVCSInstallerNonExistentVersion(t *testing.T) {
@ -110,79 +93,53 @@ func TestVCSInstallerNonExistentVersion(t *testing.T) {
version := "0.2.0"
i, err := NewForSource(source, version)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
require.NoError(t, err)
// ensure a VCSInstaller was returned
if _, ok := i.(*VCSInstaller); !ok {
t.Fatal("expected a VCSInstaller")
}
require.IsType(t, &VCSInstaller{}, i, "expected a VCSInstaller")
if err := Install(i); err == nil {
t.Fatal("expected error for version does not exists, got none")
} else if strings.Contains(err.Error(), "Could not resolve host: github.com") {
err = Install(i)
require.Error(t, err, "expected error for version does not exists, got none")
if strings.Contains(err.Error(), "Could not resolve host: github.com") {
t.Skip("Unable to run test without Internet access")
} else if err.Error() != fmt.Sprintf("requested version %q does not exist for plugin %q", version, source) {
t.Fatalf("expected error for version does not exists, got (%v)", err)
}
require.EqualErrorf(t, err, fmt.Sprintf("requested version %q does not exist for plugin %q", version, source), "expected error for version does not exists")
}
func TestVCSInstallerUpdate(t *testing.T) {
ensure.HelmHome(t)
source := "https://github.com/adamreese/helm-env"
i, err := NewForSource(source, "")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
require.NoError(t, err)
// ensure a VCSInstaller was returned
if _, ok := i.(*VCSInstaller); !ok {
t.Fatal("expected a VCSInstaller")
}
require.IsType(t, &VCSInstaller{}, i, "expected a VCSInstaller")
if err := Update(i); err == nil {
t.Fatal("expected error for plugin does not exist, got none")
} else if err.Error() != "plugin does not exist" {
t.Fatalf("expected error for plugin does not exist, got (%v)", err)
}
require.EqualErrorf(t, Update(i), "plugin does not exist", "expected error for plugin does not exist")
// Install plugin before update
if err := Install(i); err != nil {
if strings.Contains(err.Error(), "Could not resolve host: github.com") {
t.Skip("Unable to run test without Internet access")
} else {
t.Fatal(err)
}
err = Install(i)
if err != nil && strings.Contains(err.Error(), "Could not resolve host: github.com") {
t.Skip("Unable to run test without Internet access")
}
require.NoError(t, err)
// Test FindSource method for positive result
pluginInfo, err := FindSource(i.Path())
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
vcsInstaller := pluginInfo.(*VCSInstaller)
repoRemote := vcsInstaller.Repo.Remote()
if repoRemote != source {
t.Fatalf("invalid source found, expected %q got %q", source, repoRemote)
}
require.Equal(t, source, repoRemote, "invalid source found, expected %q got %q", source, repoRemote)
// Update plugin
if err := Update(i); err != nil {
t.Fatal(err)
}
require.NoError(t, Update(i))
// Test update failure
if err := os.Remove(filepath.Join(vcsInstaller.Repo.LocalPath(), "plugin.yaml")); err != nil {
t.Fatal(err)
}
require.NoError(t, os.Remove(filepath.Join(vcsInstaller.Repo.LocalPath(), "plugin.yaml")))
// Testing update for error
if err := Update(vcsInstaller); err == nil {
t.Fatal("expected error for plugin modified, got none")
} else if err.Error() != "plugin repo was modified" {
t.Fatalf("expected error for plugin modified, got (%v)", err)
}
require.EqualErrorf(t, Update(vcsInstaller), "plugin repo was modified", "expected error for plugin modified")
}

@ -23,6 +23,9 @@ import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/internal/plugin"
"helm.sh/helm/v4/internal/test/ensure"
)
@ -37,29 +40,20 @@ func TestInstallWithOptions_VerifyMissingProvenance(t *testing.T) {
// Create local installer
installer, err := NewLocalInstaller(pluginTgz)
if err != nil {
t.Fatalf("Failed to create installer: %v", err)
}
require.NoError(t, err, "Failed to create installer")
defer os.RemoveAll(installer.Path())
// Install with verification enabled should fail when .prov is missing
result, err := InstallWithOptions(installer, Options{Verify: true, Keyring: "dummy"})
// Should fail with a missing provenance error
if err == nil {
t.Fatal("Expected installation to fail when .prov file is missing and verification is enabled")
}
if !strings.Contains(err.Error(), "no provenance file") {
t.Errorf("Expected 'no provenance file' in error message, got: %v", err)
}
if result != nil {
t.Errorf("Expected nil verification result when .prov file is missing, got: %+v", result)
}
require.Error(t, err, "Expected installation to fail when .prov file is missing and verification is enabled")
require.ErrorContains(t, err, "no provenance file")
assert.Nil(t, result, "Expected nil verification result when .prov file is missing, got: %+v", result)
// Plugin should NOT be installed
if _, err := os.Stat(installer.Path()); !os.IsNotExist(err) {
t.Error("Plugin should not be installed when verification fails due to missing .prov")
}
_, err = os.Stat(installer.Path())
assert.True(t, os.IsNotExist(err), "Plugin should not be installed when verification fails due to missing .prov")
}
func TestInstallWithOptions_NoVerifyMissingProvenance(t *testing.T) {
@ -72,25 +66,18 @@ func TestInstallWithOptions_NoVerifyMissingProvenance(t *testing.T) {
// Create local installer
installer, err := NewLocalInstaller(pluginTgz)
if err != nil {
t.Fatalf("Failed to create installer: %v", err)
}
require.NoError(t, err, "Failed to create installer")
defer os.RemoveAll(installer.Path())
// Install with verification explicitly disabled should succeed without .prov
result, err := InstallWithOptions(installer, Options{Verify: false})
if err != nil {
t.Fatalf("Expected installation to succeed with --verify=false, got error: %v", err)
}
if result != nil {
t.Errorf("Expected nil verification result when verification is disabled, got: %+v", result)
}
require.NoError(t, err, "Expected installation to succeed with --verify=false")
assert.Nil(t, result, "Expected nil verification result when verification is disabled, got: %+v", result)
// Plugin should be installed
if _, err := os.Stat(installer.Path()); os.IsNotExist(err) {
t.Errorf("Plugin should be installed at %s", installer.Path())
}
_, err = os.Stat(installer.Path())
assert.Falsef(t, os.IsNotExist(err), "Plugin should be installed at %s", installer.Path())
}
func TestInstallWithOptions_VerifyWithValidProvenance(t *testing.T) {
@ -110,9 +97,7 @@ func TestInstallWithOptions_VerifyWithValidProvenance(t *testing.T) {
// Create local installer
installer, err := NewLocalInstaller(pluginTgz)
if err != nil {
t.Fatalf("Failed to create installer: %v", err)
}
require.NoError(t, err, "Failed to create installer")
defer os.RemoveAll(installer.Path())
// Install with verification enabled
@ -120,20 +105,13 @@ func TestInstallWithOptions_VerifyWithValidProvenance(t *testing.T) {
result, err := InstallWithOptions(installer, Options{Verify: true, Keyring: keyring})
// Should fail due to invalid signature (empty keyring) but we test that it gets past the hash check
if err == nil {
t.Fatal("Expected installation to fail with empty keyring")
}
if !strings.Contains(err.Error(), "plugin verification failed") {
t.Errorf("Expected plugin verification failed error, got: %v", err)
}
if result != nil {
t.Errorf("Expected nil verification result when verification fails, got: %+v", result)
}
require.Error(t, err, "Expected installation to fail with empty keyring")
require.ErrorContains(t, err, "plugin verification failed")
assert.Nil(t, result, "Expected nil verification result when verification fails, got: %+v", result)
// Plugin should not be installed due to verification failure
if _, err := os.Stat(installer.Path()); !os.IsNotExist(err) {
t.Error("Plugin should not be installed when verification fails")
}
_, err = os.Stat(installer.Path())
assert.True(t, os.IsNotExist(err), "Plugin should not be installed when verification fails")
}
func TestInstallWithOptions_VerifyWithInvalidProvenance(t *testing.T) {
@ -154,32 +132,23 @@ func TestInstallWithOptions_VerifyWithInvalidProvenance(t *testing.T) {
// Create local installer
installer, err := NewLocalInstaller(pluginTgz)
if err != nil {
t.Fatalf("Failed to create installer: %v", err)
}
require.NoError(t, err, "Failed to create installer")
defer os.RemoveAll(installer.Path())
// Install with verification enabled (should fail)
result, err := InstallWithOptions(installer, Options{Verify: true, Keyring: keyring})
// Should fail with verification error
if err == nil {
t.Fatal("Expected installation with invalid .prov file to fail")
}
if result != nil {
t.Errorf("Expected nil verification result when verification fails, got: %+v", result)
}
require.Error(t, err, "Expected installation with invalid .prov file to fail")
assert.Nil(t, result, "Expected nil verification result when verification fails, got: %+v", result)
// Should contain verification failure message
expectedError := "plugin verification failed"
if !strings.Contains(err.Error(), expectedError) {
t.Errorf("Expected error message '%s', got: %s", expectedError, err.Error())
}
require.ErrorContains(t, err, expectedError)
// Plugin should not be installed
if _, err := os.Stat(installer.Path()); !os.IsNotExist(err) {
t.Error("Plugin should not be installed when verification fails")
}
_, err = os.Stat(installer.Path())
assert.True(t, os.IsNotExist(err), "Plugin should not be installed when verification fails")
}
func TestInstallWithOptions_NoVerifyRequested(t *testing.T) {
@ -192,26 +161,19 @@ func TestInstallWithOptions_NoVerifyRequested(t *testing.T) {
// Create local installer
installer, err := NewLocalInstaller(pluginTgz)
if err != nil {
t.Fatalf("Failed to create installer: %v", err)
}
require.NoError(t, err, "Failed to create installer")
defer os.RemoveAll(installer.Path())
// Install without verification (should succeed without any verification)
result, err := InstallWithOptions(installer, Options{Verify: false})
// Should succeed with no verification
if err != nil {
t.Fatalf("Expected installation without verification to succeed, got error: %v", err)
}
if result != nil {
t.Errorf("Expected nil verification result when verification is disabled, got: %+v", result)
}
require.NoError(t, err, "Expected installation without verification to succeed")
assert.Nil(t, result, "Expected nil verification result when verification is disabled, got: %+v", result)
// Plugin should be installed
if _, err := os.Stat(installer.Path()); os.IsNotExist(err) {
t.Errorf("Plugin should be installed at %s", installer.Path())
}
_, err = os.Stat(installer.Path())
assert.Falsef(t, os.IsNotExist(err), "Plugin should be installed at %s", installer.Path())
}
func TestInstallWithOptions_VerifyDirectoryNotSupported(t *testing.T) {
@ -222,24 +184,16 @@ func TestInstallWithOptions_VerifyDirectoryNotSupported(t *testing.T) {
// Create local installer for directory
installer, err := NewLocalInstaller(pluginDir)
if err != nil {
t.Fatalf("Failed to create installer: %v", err)
}
require.NoError(t, err, "Failed to create installer")
defer os.RemoveAll(installer.Path())
// Install with verification should fail (directories don't support verification)
result, err := InstallWithOptions(installer, Options{Verify: true, Keyring: "dummy"})
// Should fail with verification not supported error
if err == nil {
t.Fatal("Expected installation to fail with verification not supported error")
}
if !strings.Contains(err.Error(), "--verify is only supported for plugin tarballs") {
t.Errorf("Expected verification not supported error, got: %v", err)
}
if result != nil {
t.Errorf("Expected nil verification result when verification fails, got: %+v", result)
}
require.Error(t, err, "Expected installation to fail with verification not supported error")
require.ErrorContains(t, err, "--verify is only supported for plugin tarballs")
assert.Nil(t, result, "Expected nil verification result when verification fails, got: %+v", result)
}
func TestInstallWithOptions_VerifyMismatchedProvenance(t *testing.T) {
@ -261,24 +215,16 @@ func TestInstallWithOptions_VerifyMismatchedProvenance(t *testing.T) {
// Create local installer
installer, err := NewLocalInstaller(pluginTgz)
if err != nil {
t.Fatalf("Failed to create installer: %v", err)
}
require.NoError(t, err, "Failed to create installer")
defer os.RemoveAll(installer.Path())
// Install with verification should fail due to hash mismatch
result, err := InstallWithOptions(installer, Options{Verify: true, Keyring: keyring})
// Should fail with verification error
if err == nil {
t.Fatal("Expected installation to fail with hash mismatch")
}
if !strings.Contains(err.Error(), "plugin verification failed") {
t.Errorf("Expected plugin verification failed error, got: %v", err)
}
if result != nil {
t.Errorf("Expected nil verification result when verification fails, got: %+v", result)
}
require.Error(t, err, "Expected installation to fail with hash mismatch")
require.ErrorContains(t, err, "plugin verification failed")
assert.Nil(t, result, "Expected nil verification result when verification fails, got: %+v", result)
}
func TestInstallWithOptions_VerifyProvenanceAccessError(t *testing.T) {
@ -291,9 +237,7 @@ func TestInstallWithOptions_VerifyProvenanceAccessError(t *testing.T) {
// Create a .prov file but make it inaccessible (simulate permission error)
provFile := pluginTgz + ".prov"
if err := os.WriteFile(provFile, []byte("test"), 0000); err != nil {
t.Fatalf("Failed to create inaccessible provenance file: %v", err)
}
require.NoErrorf(t, os.WriteFile(provFile, []byte("test"), 0o000), "Failed to create inaccessible provenance file")
defer os.Remove(provFile)
// Create keyring
@ -302,27 +246,21 @@ func TestInstallWithOptions_VerifyProvenanceAccessError(t *testing.T) {
// Create local installer
installer, err := NewLocalInstaller(pluginTgz)
if err != nil {
t.Fatalf("Failed to create installer: %v", err)
}
require.NoError(t, err, "Failed to create installer")
defer os.RemoveAll(installer.Path())
// Install with verification should fail due to access error
result, err := InstallWithOptions(installer, Options{Verify: true, Keyring: keyring})
// Should fail with access error (either at stat level or during verification)
if err == nil {
t.Fatal("Expected installation to fail with provenance file access error")
}
require.Error(t, err, "Expected installation to fail with provenance file access error")
// The error could be either "failed to access provenance file" or "plugin verification failed"
// depending on when the permission error occurs
if !strings.Contains(err.Error(), "failed to access provenance file") &&
!strings.Contains(err.Error(), "plugin verification failed") {
t.Errorf("Expected provenance file access or verification error, got: %v", err)
}
if result != nil {
t.Errorf("Expected nil verification result when verification fails, got: %+v", result)
}
assert.Nil(t, result, "Expected nil verification result when verification fails, got: %+v", result)
}
// Helper functions for test setup
@ -333,9 +271,7 @@ func createTestPluginDir(t *testing.T) string {
// Create temporary directory with plugin structure
tmpDir := t.TempDir()
pluginDir := filepath.Join(tmpDir, "test-plugin")
if err := os.MkdirAll(pluginDir, 0755); err != nil {
t.Fatalf("Failed to create plugin directory: %v", err)
}
require.NoErrorf(t, os.MkdirAll(pluginDir, 0o755), "Failed to create plugin directory")
// Create plugin.yaml using the standardized v1 format
pluginYaml := `apiVersion: v1
@ -346,9 +282,7 @@ version: 1.0.0
runtimeConfig:
platformCommand:
- command: echo`
if err := os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte(pluginYaml), 0644); err != nil {
t.Fatalf("Failed to create plugin.yaml: %v", err)
}
require.NoErrorf(t, os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte(pluginYaml), 0o644), "Failed to create plugin.yaml")
return pluginDir
}
@ -360,15 +294,10 @@ func createTarballFromPluginDir(t *testing.T, pluginDir string) string {
tmpDir := filepath.Dir(pluginDir)
tgzPath := filepath.Join(tmpDir, "test-plugin-1.0.0.tgz")
tarFile, err := os.Create(tgzPath)
if err != nil {
t.Fatalf("Failed to create tarball file: %v", err)
}
require.NoError(t, err, "Failed to create tarball file")
defer tarFile.Close()
if err := plugin.CreatePluginTarball(pluginDir, "test-plugin", tarFile); err != nil {
t.Fatalf("Failed to create tarball: %v", err)
}
require.NoErrorf(t, plugin.CreatePluginTarball(pluginDir, "test-plugin", tarFile), "Failed to create tarball")
return tgzPath
}
@ -379,9 +308,7 @@ func createProvFile(t *testing.T, provFile, pluginTgz, hash string) {
if hash == "" {
// Calculate actual hash of the tarball for realistic testing
data, err := os.ReadFile(pluginTgz)
if err != nil {
t.Fatalf("Failed to read tarball for hashing: %v", err)
}
require.NoError(t, err, "Failed to read tarball for hashing")
hashSum := sha256.Sum256(data)
hashStr = fmt.Sprintf("sha256:%x", hashSum)
} else {
@ -404,9 +331,7 @@ Version: GnuPG v1
iQEcBAEBCAAGBQJktest...
-----END PGP SIGNATURE-----
`, hashStr)
if err := os.WriteFile(provFile, []byte(provContent), 0644); err != nil {
t.Fatalf("Failed to create provenance file: %v", err)
}
require.NoErrorf(t, os.WriteFile(provFile, []byte(provContent), 0o644), "Failed to create provenance file")
}
func createProvFileInvalidFormat(t *testing.T, provFile string) {
@ -414,9 +339,7 @@ func createProvFileInvalidFormat(t *testing.T, provFile string) {
// Create an invalid provenance file (not PGP signed format)
invalidProv := "This is not a valid PGP signed message"
if err := os.WriteFile(provFile, []byte(invalidProv), 0644); err != nil {
t.Fatalf("Failed to create invalid provenance file: %v", err)
}
require.NoErrorf(t, os.WriteFile(provFile, []byte(invalidProv), 0o644), "Failed to create invalid provenance file")
}
func createTestKeyring(t *testing.T) string {
@ -427,9 +350,7 @@ func createTestKeyring(t *testing.T) string {
keyringPath := filepath.Join(tmpDir, "pubring.gpg")
// Create empty keyring for testing
if err := os.WriteFile(keyringPath, []byte{}, 0644); err != nil {
t.Fatalf("Failed to create test keyring: %v", err)
}
require.NoErrorf(t, os.WriteFile(keyringPath, []byte{}, 0o644), "Failed to create test keyring")
return keyringPath
}

@ -216,7 +216,6 @@ func findPlugins(pluginsDirs []string, findFn findFunc, filterFn filterFunc) ([]
var found []Plugin
for _, pluginsDir := range pluginsDirs {
ps, err := findFn(pluginsDir)
if err != nil {
return nil, err
}

@ -118,7 +118,7 @@ func TestLoadDir(t *testing.T) {
require.NoError(t, err, "error loading plugin from %s", tc.dirname)
assert.Equal(t, tc.dirname, plug.Dir())
assert.EqualValues(t, tc.expect, plug.Metadata())
assert.Equal(t, tc.expect, plug.Metadata())
})
}
}
@ -194,13 +194,9 @@ func TestDetectDuplicates(t *testing.T) {
mockSubprocessCLIPlugin(t, "foo"),
mockSubprocessCLIPlugin(t, "bar"),
}
if err := detectDuplicates(plugs); err != nil {
t.Error("no duplicates in the first set")
}
require.NoError(t, detectDuplicates(plugs), "no duplicates in the first set")
plugs = append(plugs, mockSubprocessCLIPlugin(t, "foo"))
if err := detectDuplicates(plugs); err == nil {
t.Error("duplicates in the second set")
}
assert.Error(t, detectDuplicates(plugs), "duplicates in the second set")
}
func TestLoadAllDir_Empty(t *testing.T) {
@ -295,8 +291,7 @@ command: echo test`,
m, err := loadMetadataLegacy([]byte(tc.yaml))
if tc.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.errorContains)
require.ErrorContains(t, err, tc.errorContains)
t.Logf("Legacy error (validation catches empty name): %v", err)
if tc.logNote != "" {
t.Log(tc.logNote)
@ -342,8 +337,7 @@ runtime: subprocess
m, err := loadMetadataV1([]byte(tc.yaml))
if tc.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.errorContains)
require.ErrorContains(t, err, tc.errorContains)
t.Logf("V1 error (strict unmarshalling): %v", err)
} else {
require.NoError(t, err)

@ -154,8 +154,7 @@ func buildLegacyConfig(m MetadataLegacy, pluginType string) Config {
func buildLegacyRuntimeConfig(m MetadataLegacy) RuntimeConfig {
var protocolCommands []SubprocessProtocolCommand
if len(m.Downloaders) > 0 {
protocolCommands =
make([]SubprocessProtocolCommand, 0, len(m.Downloaders))
protocolCommands = make([]SubprocessProtocolCommand, 0, len(m.Downloaders))
for _, d := range m.Downloaders {
protocolCommands = append(protocolCommands, SubprocessProtocolCommand{
Protocols: d.Protocols,
@ -165,7 +164,7 @@ func buildLegacyRuntimeConfig(m MetadataLegacy) RuntimeConfig {
}
platformCommand := m.PlatformCommand
if len(platformCommand) == 0 && len(m.Command) > 0 {
if len(platformCommand) == 0 && m.Command != "" {
platformCommand = []PlatformCommand{{Command: m.Command}}
}

@ -81,7 +81,7 @@ func (m *MetadataLegacy) Validate() error {
m.Usage = sanitizeString(m.Usage)
if len(m.PlatformCommand) > 0 && len(m.Command) > 0 {
if len(m.PlatformCommand) > 0 && m.Command != "" {
return errors.New("both platformCommand and command are set")
}

@ -16,10 +16,12 @@ limitations under the License.
package plugin
import (
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestValidatePluginData(t *testing.T) {
@ -61,15 +63,15 @@ func TestValidatePluginData(t *testing.T) {
{true, mockNoCommand, ""}, // Test no command metadata works
{true, mockLegacyCommand, ""}, // Test legacy command metadata works
} {
err := item.plug.Metadata().Validate()
if item.pass && err != nil {
t.Errorf("failed to validate case %d: %s", i, err)
} else if !item.pass && err == nil {
t.Errorf("expected case %d to fail", i)
}
if !item.pass && !strings.Contains(err.Error(), item.errString) {
t.Errorf("index [%d]: expected error to contain: %s, but got: %s", i, item.errString, err.Error())
}
t.Run(strconv.Itoa(i), func(t *testing.T) {
err := item.plug.Metadata().Validate()
if item.pass {
require.NoError(t, err, "failed to validate case %d", i)
} else {
require.Error(t, err, "expected case %d to fail", i)
assert.ErrorContains(t, err, item.errString, "expected case %d error to contain %q", i, item.errString)
}
})
}
}
@ -95,8 +97,7 @@ func TestMetadataValidateVersion(t *testing.T) {
t.Run(name, func(t *testing.T) {
m := mockSubprocessCLIPlugin(t, "testplugin")
m.metadata.Version = tc.version
err := m.Metadata().Validate()
assert.NoError(t, err)
assert.NoError(t, m.Metadata().Validate())
})
}
@ -104,8 +105,7 @@ func TestMetadataValidateVersion(t *testing.T) {
t.Run(name, func(t *testing.T) {
m := mockSubprocessCLIPlugin(t, "testplugin")
m.metadata.Version = tc.version
err := m.Metadata().Validate()
assert.ErrorContains(t, err, "invalid plugin version")
assert.ErrorContains(t, m.Metadata().Validate(), "invalid plugin version")
})
}
}
@ -122,9 +122,7 @@ func TestMetadataValidateMultipleErrors(t *testing.T) {
}
err := metadata.Validate()
if err == nil {
t.Fatal("expected validation to fail with multiple errors")
}
require.Error(t, err, "expected validation to fail with multiple errors")
errStr := err.Error()
@ -139,9 +137,7 @@ func TestMetadataValidateMultipleErrors(t *testing.T) {
}
for _, expectedErr := range expectedErrors {
if !strings.Contains(errStr, expectedErr) {
t.Errorf("expected error to contain %q, but got: %v", expectedErr, errStr)
}
require.ErrorContains(t, err, expectedErr)
}
// Verify that the error contains the correct number of error messages
@ -152,7 +148,5 @@ func TestMetadataValidateMultipleErrors(t *testing.T) {
}
}
if errorCount < len(expectedErrors) {
t.Errorf("expected %d errors, but only found %d in: %v", len(expectedErrors), errorCount, errStr)
}
assert.GreaterOrEqual(t, errorCount, len(expectedErrors), "expected %d errors, but only found %d in: %v", len(expectedErrors), errorCount, errStr)
}

@ -77,9 +77,7 @@ func TestMetadataV1ValidateVersion(t *testing.T) {
t.Run("invalid/"+name, func(t *testing.T) {
m := base()
m.Version = tc.version
err := m.Validate()
assert.Error(t, err)
assert.Contains(t, err.Error(), tc.errMsg)
assert.ErrorContains(t, m.Validate(), tc.errMsg)
})
}
}

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

@ -18,6 +18,8 @@ package plugin
import (
"testing"
"github.com/stretchr/testify/assert"
"helm.sh/helm/v4/internal/plugin/schema"
)
@ -34,9 +36,7 @@ func TestValidPluginName(t *testing.T) {
for name, pluginName := range validNames {
t.Run("valid/"+name, func(t *testing.T) {
if !validPluginName.MatchString(pluginName) {
t.Errorf("expected %q to match validPluginName regex", pluginName)
}
assert.Truef(t, validPluginName.MatchString(pluginName), "expected %q to match validPluginName regex", pluginName)
})
}
@ -52,9 +52,7 @@ func TestValidPluginName(t *testing.T) {
for name, pluginName := range invalidNames {
t.Run("invalid/"+name, func(t *testing.T) {
if validPluginName.MatchString(pluginName) {
t.Errorf("expected %q to not match validPluginName regex", pluginName)
}
assert.Falsef(t, validPluginName.MatchString(pluginName), "expected %q to not match validPluginName regex", pluginName)
})
}
}

@ -59,8 +59,7 @@ func buildLoadExtismPlugin(t *testing.T, dir string) pluginRaw {
func TestRuntimeConfigExtismV1Validate(t *testing.T) {
rc := RuntimeConfigExtismV1{}
err := rc.Validate()
assert.NoError(t, err, "expected no error for empty RuntimeConfigExtismV1")
assert.NoError(t, rc.Validate(), "expected no error for empty RuntimeConfigExtismV1")
}
func TestRuntimeExtismV1InvokePlugin(t *testing.T) {
@ -71,7 +70,7 @@ func TestRuntimeExtismV1InvokePlugin(t *testing.T) {
p, err := r.CreatePlugin(pr.Dir, &pr.Metadata)
assert.NoError(t, err, "expected no error creating plugin")
require.NoError(t, err, "expected no error creating plugin")
assert.NotNil(t, p, "expected plugin to be created")
output, err := p.Invoke(t.Context(), &Input{

@ -18,6 +18,7 @@ package plugin
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"log/slog"
@ -125,11 +126,7 @@ func (r *SubprocessPluginRuntime) InvokeWithEnv(main string, argv []string, env
cmd.Stdout = stdout
cmd.Stderr = stderr
if err := executeCmd(cmd, r.metadata.Name); err != nil {
return err
}
return nil
return executeCmd(cmd, r.metadata.Name)
}
func (r *SubprocessPluginRuntime) InvokeHook(event string) error {
@ -156,7 +153,8 @@ func (r *SubprocessPluginRuntime) InvokeHook(event string) error {
slog.Debug("executing plugin hook command", slog.String("pluginName", r.metadata.Name), slog.String("command", cmd.String()))
if err := cmd.Run(); err != nil {
if eerr, ok := err.(*exec.ExitError); ok {
var eerr *exec.ExitError
if errors.As(err, &eerr) {
os.Stderr.Write(eerr.Stderr)
return fmt.Errorf("plugin %s hook for %q exited with error", event, r.metadata.Name)
}
@ -170,7 +168,8 @@ func (r *SubprocessPluginRuntime) InvokeHook(event string) error {
// then replace the other three with a call to this func
func executeCmd(prog *exec.Cmd, pluginName string) error {
if err := prog.Run(); err != nil {
if eerr, ok := err.(*exec.ExitError); ok {
var eerr *exec.ExitError
if errors.As(err, &eerr) {
slog.Debug(
"plugin execution failed",
slog.String("pluginName", pluginName),

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

@ -16,7 +16,6 @@ limitations under the License.
package plugin
import (
"errors"
"fmt"
"os"
"path/filepath"
@ -78,8 +77,7 @@ func TestSubprocessPluginRuntime(t *testing.T) {
require.Error(t, err)
ieerr := &InvokeExecError{}
ok := errors.As(err, &ieerr)
require.True(t, ok, "expected InvokeExecError, got %T", err)
require.ErrorAs(t, err, &ieerr, "expected InvokeExecError")
assert.Equal(t, 56, ieerr.ExitCode)
assert.Nil(t, output)

@ -18,9 +18,11 @@ package plugin
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/pkg/provenance"
)
@ -28,9 +30,7 @@ func TestSignPlugin(t *testing.T) {
// Create a test plugin directory
tempDir := t.TempDir()
pluginDir := filepath.Join(tempDir, "test-plugin")
if err := os.MkdirAll(pluginDir, 0755); err != nil {
t.Fatal(err)
}
require.NoError(t, os.MkdirAll(pluginDir, 0o755))
// Create a plugin.yaml file
pluginYAML := `apiVersion: v1
@ -41,16 +41,12 @@ version: 1.0.0
runtimeConfig:
platformCommand:
- command: echo`
if err := os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte(pluginYAML), 0644); err != nil {
t.Fatal(err)
}
require.NoError(t, os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte(pluginYAML), 0o644))
// Create a tarball
tarballPath := filepath.Join(tempDir, "test-plugin.tgz")
tarFile, err := os.Create(tarballPath)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
if err := CreatePluginTarball(pluginDir, "test-plugin", tarFile); err != nil {
tarFile.Close()
t.Fatal(err)
@ -60,39 +56,25 @@ runtimeConfig:
// Create a test key for signing
keyring := "../../pkg/cmd/testdata/helm-test-key.secret"
signer, err := provenance.NewFromKeyring(keyring, "helm-test")
if err != nil {
t.Fatal(err)
}
if err := signer.DecryptKey(func(_ string) ([]byte, error) {
require.NoError(t, err)
require.NoError(t, signer.DecryptKey(func(_ string) ([]byte, error) {
return []byte(""), nil
}); err != nil {
t.Fatal(err)
}
}))
// Read the tarball data
tarballData, err := os.ReadFile(tarballPath)
if err != nil {
t.Fatalf("failed to read tarball: %v", err)
}
require.NoError(t, err, "failed to read tarball")
// Sign the plugin tarball
sig, err := SignPlugin(tarballData, filepath.Base(tarballPath), signer)
if err != nil {
t.Fatalf("failed to sign plugin: %v", err)
}
require.NoError(t, err, "failed to sign plugin")
// Verify the signature contains the expected content
if !strings.Contains(sig, "-----BEGIN PGP SIGNED MESSAGE-----") {
t.Error("signature does not contain PGP header")
}
assert.Contains(t, sig, "-----BEGIN PGP SIGNED MESSAGE-----", "signature does not contain PGP header")
// Verify the tarball hash is in the signature
expectedHash, err := provenance.DigestFile(tarballPath)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
// The signature should contain the tarball hash
if !strings.Contains(sig, "sha256:"+expectedHash) {
t.Errorf("signature does not contain expected tarball hash: sha256:%s", expectedHash)
}
assert.Contains(t, sig, "sha256:"+expectedHash, "signature does not contain expected tarball hash: sha256:%s", expectedHash)
}

@ -48,12 +48,12 @@ func getPlatformCommand(cmds []PlatformCommand) ([]string, []string) {
return strings.Split(c.Command, " "), c.Args
}
if (len(c.OperatingSystem) > 0 && !eq(c.OperatingSystem, runtime.GOOS)) || len(c.Architecture) > 0 {
if (c.OperatingSystem != "" && !eq(c.OperatingSystem, runtime.GOOS)) || c.Architecture != "" {
// Skip if OS is not empty and doesn't match or if arch is set as a set arch requires an OS match
continue
}
if !foundOs && len(c.OperatingSystem) > 0 && eq(c.OperatingSystem, runtime.GOOS) {
if !foundOs && c.OperatingSystem != "" && eq(c.OperatingSystem, runtime.GOOS) {
// First OS match with empty arch, can only be overridden by a direct match
command = strings.Split(c.Command, " ")
args = c.Args

@ -16,11 +16,11 @@ limitations under the License.
package plugin
import (
"reflect"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPrepareCommand(t *testing.T) {
@ -36,15 +36,9 @@ func TestPrepareCommand(t *testing.T) {
env := map[string]string{}
cmd, args, err := PrepareCommands(platformCommand, true, []string{}, env)
if err != nil {
t.Fatal(err)
}
if cmd != cmdMain {
t.Fatalf("Expected %q, got %q", cmdMain, cmd)
}
if !reflect.DeepEqual(args, cmdArgs) {
t.Fatalf("Expected %v, got %v", cmdArgs, args)
}
require.NoError(t, err)
require.Equal(t, cmdMain, cmd, "Expected %q, got %q", cmdMain, cmd)
require.Equalf(t, args, cmdArgs, "Expected %v, got %v", cmdArgs, args)
}
func TestPrepareCommandExtraArgs(t *testing.T) {
@ -94,9 +88,7 @@ func TestPrepareCommandExtraArgs(t *testing.T) {
env := map[string]string{}
cmd, args, err := PrepareCommands(platformCommand, true, testExtraArgs, env)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
assert.Equal(t, tc.expected.cmdMain, cmd, "Expected command to match")
assert.Equal(t, tc.expected.args, args, "Expected args to match")
})
@ -116,15 +108,9 @@ func TestPrepareCommands(t *testing.T) {
env := map[string]string{}
cmd, args, err := PrepareCommands(cmds, true, []string{}, env)
if err != nil {
t.Fatal(err)
}
if cmd != cmdMain {
t.Fatalf("Expected %q, got %q", cmdMain, cmd)
}
if !reflect.DeepEqual(args, cmdArgs) {
t.Fatalf("Expected %v, got %v", cmdArgs, args)
}
require.NoError(t, err)
require.Equal(t, cmdMain, cmd, "Expected %q, got %q", cmdMain, cmd)
require.Equalf(t, args, cmdArgs, "Expected %v, got %v", cmdArgs, args)
}
func TestPrepareCommandsExtraArgs(t *testing.T) {
@ -143,15 +129,9 @@ func TestPrepareCommandsExtraArgs(t *testing.T) {
env := map[string]string{}
cmd, args, err := PrepareCommands(cmds, true, extraArgs, env)
if err != nil {
t.Fatal(err)
}
if cmd != cmdMain {
t.Fatalf("Expected %q, got %q", cmdMain, cmd)
}
if !reflect.DeepEqual(args, expectedArgs) {
t.Fatalf("Expected %v, got %v", expectedArgs, args)
}
require.NoError(t, err)
require.Equal(t, cmdMain, cmd, "Expected %q, got %q", cmdMain, cmd)
require.Equalf(t, expectedArgs, args, "Expected %v, got %v", expectedArgs, args)
}
func TestPrepareCommandsNoArch(t *testing.T) {
@ -166,15 +146,9 @@ func TestPrepareCommandsNoArch(t *testing.T) {
env := map[string]string{}
cmd, args, err := PrepareCommands(cmds, true, []string{}, env)
if err != nil {
t.Fatal(err)
}
if cmd != cmdMain {
t.Fatalf("Expected %q, got %q", cmdMain, cmd)
}
if !reflect.DeepEqual(args, cmdArgs) {
t.Fatalf("Expected %v, got %v", cmdArgs, args)
}
require.NoError(t, err)
require.Equal(t, cmdMain, cmd, "Expected %q, got %q", cmdMain, cmd)
require.Equalf(t, args, cmdArgs, "Expected %v, got %v", cmdArgs, args)
}
func TestPrepareCommandsNoOsNoArch(t *testing.T) {
@ -189,15 +163,9 @@ func TestPrepareCommandsNoOsNoArch(t *testing.T) {
env := map[string]string{}
cmd, args, err := PrepareCommands(cmds, true, []string{}, env)
if err != nil {
t.Fatal(err)
}
if cmd != cmdMain {
t.Fatalf("Expected %q, got %q", cmdMain, cmd)
}
if !reflect.DeepEqual(args, cmdArgs) {
t.Fatalf("Expected %v, got %v", cmdArgs, args)
}
require.NoError(t, err)
require.Equal(t, cmdMain, cmd, "Expected %q, got %q", cmdMain, cmd)
require.Equalf(t, args, cmdArgs, "Expected %v, got %v", cmdArgs, args)
}
func TestPrepareCommandsNoMatch(t *testing.T) {
@ -208,18 +176,16 @@ func TestPrepareCommandsNoMatch(t *testing.T) {
}
env := map[string]string{}
if _, _, err := PrepareCommands(cmds, true, []string{}, env); err == nil {
t.Fatal("Expected error to be returned")
}
_, _, err := PrepareCommands(cmds, true, []string{}, env)
require.Error(t, err, "Expected error to be returned")
}
func TestPrepareCommandsNoCommands(t *testing.T) {
cmds := []PlatformCommand{}
env := map[string]string{}
if _, _, err := PrepareCommands(cmds, true, []string{}, env); err == nil {
t.Fatal("Expected error to be returned")
}
_, _, err := PrepareCommands(cmds, true, []string{}, env)
require.Error(t, err, "Expected error to be returned")
}
func TestPrepareCommandsExpand(t *testing.T) {
@ -237,15 +203,9 @@ func TestPrepareCommandsExpand(t *testing.T) {
}
cmd, args, err := PrepareCommands(cmds, true, []string{}, env)
if err != nil {
t.Fatal(err)
}
if cmd != cmdMain {
t.Fatalf("Expected %q, got %q", cmdMain, cmd)
}
if !reflect.DeepEqual(args, expectedArgs) {
t.Fatalf("Expected %v, got %v", expectedArgs, args)
}
require.NoError(t, err)
require.Equal(t, cmdMain, cmd, "Expected %q, got %q", cmdMain, cmd)
require.Equalf(t, expectedArgs, args, "Expected %v, got %v", expectedArgs, args)
}
func TestPrepareCommandsNoExpand(t *testing.T) {
@ -260,13 +220,7 @@ func TestPrepareCommandsNoExpand(t *testing.T) {
}
cmd, args, err := PrepareCommands(cmds, false, []string{}, env)
if err != nil {
t.Fatal(err)
}
if cmd != cmdMain {
t.Fatalf("Expected %q, got %q", cmdMain, cmd)
}
if !reflect.DeepEqual(args, cmdArgs) {
t.Fatalf("Expected %v, got %v", cmdArgs, args)
}
require.NoError(t, err)
require.Equal(t, cmdMain, cmd, "Expected %q, got %q", cmdMain, cmd)
require.Equalf(t, args, cmdArgs, "Expected %v, got %v", cmdArgs, args)
}

@ -20,11 +20,16 @@ import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/pkg/provenance"
)
const testKeyFile = "../../pkg/cmd/testdata/helm-test-key.secret"
const testPubFile = "../../pkg/cmd/testdata/helm-test-key.pub"
const (
testKeyFile = "../../pkg/cmd/testdata/helm-test-key.secret"
testPubFile = "../../pkg/cmd/testdata/helm-test-key.pub"
)
const testPluginYAML = `apiVersion: v1
name: test-plugin
@ -41,20 +46,14 @@ func TestVerifyPlugin(t *testing.T) {
// Create plugin directory
pluginDir := filepath.Join(tempDir, "verify-test-plugin")
if err := os.MkdirAll(pluginDir, 0755); err != nil {
t.Fatal(err)
}
require.NoError(t, os.MkdirAll(pluginDir, 0o755))
if err := os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte(testPluginYAML), 0644); err != nil {
t.Fatal(err)
}
require.NoError(t, os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte(testPluginYAML), 0o644))
// Create tarball
tarballPath := filepath.Join(tempDir, "verify-test-plugin.tar.gz")
tarFile, err := os.Create(tarballPath)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
if err := CreatePluginTarball(pluginDir, "test-plugin", tarFile); err != nil {
tarFile.Close()
@ -64,61 +63,39 @@ func TestVerifyPlugin(t *testing.T) {
// Sign the plugin with source directory
signer, err := provenance.NewFromKeyring(testKeyFile, "helm-test")
if err != nil {
t.Fatal(err)
}
if err := signer.DecryptKey(func(_ string) ([]byte, error) {
require.NoError(t, err)
require.NoError(t, signer.DecryptKey(func(_ string) ([]byte, error) {
return []byte(""), nil
}); err != nil {
t.Fatal(err)
}
}))
// Read the tarball data
tarballData, err := os.ReadFile(tarballPath)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
sig, err := SignPlugin(tarballData, filepath.Base(tarballPath), signer)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
// Write the signature to .prov file
provFile := tarballPath + ".prov"
if err := os.WriteFile(provFile, []byte(sig), 0644); err != nil {
t.Fatal(err)
}
require.NoError(t, os.WriteFile(provFile, []byte(sig), 0o644))
// Read the files for verification
archiveData, err := os.ReadFile(tarballPath)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
provData, err := os.ReadFile(provFile)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
// Now verify the plugin
verification, err := VerifyPlugin(archiveData, provData, filepath.Base(tarballPath), testPubFile)
if err != nil {
t.Fatalf("Failed to verify plugin: %v", err)
}
require.NoError(t, err, "Failed to verify plugin")
// Check verification results
if verification.SignedBy == nil {
t.Error("SignedBy is nil")
}
assert.NotNil(t, verification.SignedBy, "SignedBy is nil")
if verification.FileName != "verify-test-plugin.tar.gz" {
t.Errorf("Expected filename 'verify-test-plugin.tar.gz', got %s", verification.FileName)
}
assert.Equal(t, "verify-test-plugin.tar.gz", verification.FileName, "Expected filename 'verify-test-plugin.tar.gz', got %s", verification.FileName)
if verification.FileHash == "" {
t.Error("FileHash is empty")
}
assert.NotEmpty(t, verification.FileHash, "FileHash is empty")
}
func TestVerifyPluginBadSignature(t *testing.T) {
@ -126,19 +103,13 @@ func TestVerifyPluginBadSignature(t *testing.T) {
// Create a plugin tarball
pluginDir := filepath.Join(tempDir, "bad-plugin")
if err := os.MkdirAll(pluginDir, 0755); err != nil {
t.Fatal(err)
}
require.NoError(t, os.MkdirAll(pluginDir, 0o755))
if err := os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte(testPluginYAML), 0644); err != nil {
t.Fatal(err)
}
require.NoError(t, os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte(testPluginYAML), 0o644))
tarballPath := filepath.Join(tempDir, "bad-plugin.tar.gz")
tarFile, err := os.Create(tarballPath)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
if err := CreatePluginTarball(pluginDir, "test-plugin", tarFile); err != nil {
tarFile.Close()
@ -158,26 +129,18 @@ InvalidSignatureData
-----END PGP SIGNATURE-----`
provFile := tarballPath + ".prov"
if err := os.WriteFile(provFile, []byte(badSig), 0644); err != nil {
t.Fatal(err)
}
require.NoError(t, os.WriteFile(provFile, []byte(badSig), 0o644))
// Read the files
archiveData, err := os.ReadFile(tarballPath)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
provData, err := os.ReadFile(provFile)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
// Try to verify - should fail
_, err = VerifyPlugin(archiveData, provData, filepath.Base(tarballPath), testPubFile)
if err == nil {
t.Error("Expected verification to fail with bad signature")
}
assert.Error(t, err, "Expected verification to fail with bad signature")
}
func TestVerifyPluginMissingProvenance(t *testing.T) {
@ -185,21 +148,15 @@ func TestVerifyPluginMissingProvenance(t *testing.T) {
tarballPath := filepath.Join(tempDir, "no-prov.tar.gz")
// Create a minimal tarball
if err := os.WriteFile(tarballPath, []byte("dummy"), 0644); err != nil {
t.Fatal(err)
}
require.NoError(t, os.WriteFile(tarballPath, []byte("dummy"), 0o644))
// Read the tarball data
archiveData, err := os.ReadFile(tarballPath)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
// Try to verify with empty provenance data
_, err = VerifyPlugin(archiveData, nil, filepath.Base(tarballPath), testPubFile)
if err == nil {
t.Error("Expected verification to fail with empty provenance data")
}
assert.Error(t, err, "Expected verification to fail with empty provenance data")
}
func TestVerifyPluginMalformedData(t *testing.T) {
@ -208,7 +165,5 @@ func TestVerifyPluginMalformedData(t *testing.T) {
provData := []byte("fake provenance")
_, err := VerifyPlugin(malformedData, provData, "malformed.tar.gz", testPubFile)
if err == nil {
t.Error("Expected malformed data verification to fail, but it succeeded")
}
assert.Error(t, err, "Expected malformed data verification to fail, but it succeeded")
}

@ -170,12 +170,12 @@ func TestHookExecutionUnmarshalJSON(t *testing.T) {
err := json.Unmarshal([]byte(tt.input), &exec)
if tt.wantErr {
assert.Error(t, err)
return
} else {
require.NoError(t, err)
assert.Equal(t, tt.expected.StartedAt.Unix(), exec.StartedAt.Unix())
assert.Equal(t, tt.expected.CompletedAt.Unix(), exec.CompletedAt.Unix())
assert.Equal(t, tt.expected.Phase, exec.Phase)
}
require.NoError(t, err)
assert.Equal(t, tt.expected.StartedAt.Unix(), exec.StartedAt.Unix())
assert.Equal(t, tt.expected.CompletedAt.Unix(), exec.CompletedAt.Unix())
assert.Equal(t, tt.expected.Phase, exec.Phase)
})
}
}
@ -194,8 +194,7 @@ func TestHookExecutionRoundTrip(t *testing.T) {
require.NoError(t, err)
var decoded HookExecution
err = json.Unmarshal(data, &decoded)
require.NoError(t, err)
require.NoError(t, json.Unmarshal(data, &decoded))
assert.Equal(t, original.StartedAt.Unix(), decoded.StartedAt.Unix())
assert.Equal(t, original.CompletedAt.Unix(), decoded.CompletedAt.Unix())
@ -208,12 +207,11 @@ func TestHookExecutionEmptyStringRoundTrip(t *testing.T) {
input := `{"started_at":"","completed_at":"","phase":"Succeeded"}`
var exec HookExecution
err := json.Unmarshal([]byte(input), &exec)
require.NoError(t, err)
require.NoError(t, json.Unmarshal([]byte(input), &exec))
// Verify time fields are zero values
assert.True(t, exec.StartedAt.IsZero())
assert.True(t, exec.CompletedAt.IsZero())
assert.Zero(t, exec.StartedAt)
assert.Zero(t, exec.CompletedAt)
assert.Equal(t, HookPhaseSucceeded, exec.Phase)
// Marshal back and verify empty time fields are omitted
@ -221,8 +219,7 @@ func TestHookExecutionEmptyStringRoundTrip(t *testing.T) {
require.NoError(t, err)
var result map[string]any
err = json.Unmarshal(data, &result)
require.NoError(t, err)
require.NoError(t, json.Unmarshal(data, &result))
// Zero time values should be omitted
assert.NotContains(t, result, "started_at")

@ -253,17 +253,17 @@ func TestInfoUnmarshalJSON(t *testing.T) {
err := json.Unmarshal([]byte(tt.input), &info)
if tt.wantErr {
assert.Error(t, err)
return
} else {
require.NoError(t, err)
assert.Equal(t, tt.expected.FirstDeployed.Unix(), info.FirstDeployed.Unix())
assert.Equal(t, tt.expected.LastDeployed.Unix(), info.LastDeployed.Unix())
assert.Equal(t, tt.expected.Deleted.Unix(), info.Deleted.Unix())
assert.Equal(t, tt.expected.Description, info.Description)
assert.Equal(t, tt.expected.Status, info.Status)
assert.Equal(t, tt.expected.RollbackRevision, info.RollbackRevision)
assert.Equal(t, tt.expected.Notes, info.Notes)
assert.Equal(t, tt.expected.Resources, info.Resources)
}
require.NoError(t, err)
assert.Equal(t, tt.expected.FirstDeployed.Unix(), info.FirstDeployed.Unix())
assert.Equal(t, tt.expected.LastDeployed.Unix(), info.LastDeployed.Unix())
assert.Equal(t, tt.expected.Deleted.Unix(), info.Deleted.Unix())
assert.Equal(t, tt.expected.Description, info.Description)
assert.Equal(t, tt.expected.Status, info.Status)
assert.Equal(t, tt.expected.RollbackRevision, info.RollbackRevision)
assert.Equal(t, tt.expected.Notes, info.Notes)
assert.Equal(t, tt.expected.Resources, info.Resources)
})
}
}
@ -284,9 +284,7 @@ func TestInfoRoundTrip(t *testing.T) {
require.NoError(t, err)
var decoded Info
err = json.Unmarshal(data, &decoded)
require.NoError(t, err)
require.NoError(t, json.Unmarshal(data, &decoded))
assert.Equal(t, original.FirstDeployed.Unix(), decoded.FirstDeployed.Unix())
assert.Equal(t, original.LastDeployed.Unix(), decoded.LastDeployed.Unix())
assert.Equal(t, original.Deleted.Unix(), decoded.Deleted.Unix())
@ -330,8 +328,7 @@ func TestInfoRollbackRevisionRoundTrip(t *testing.T) {
require.NoError(t, err)
var decoded Info
err = json.Unmarshal(data, &decoded)
require.NoError(t, err)
require.NoError(t, json.Unmarshal(data, &decoded))
assert.Equal(t, tt.info.RollbackRevision, decoded.RollbackRevision)
assert.Equal(t, tt.info.FirstDeployed.Unix(), decoded.FirstDeployed.Unix())
@ -342,8 +339,7 @@ func TestInfoRollbackRevisionRoundTrip(t *testing.T) {
// Verify omitempty behavior: zero rollback_revision should not appear in JSON
if tt.info.RollbackRevision == 0 {
var raw map[string]any
err = json.Unmarshal(data, &raw)
require.NoError(t, err)
require.NoError(t, json.Unmarshal(data, &raw))
assert.NotContains(t, raw, "rollback_revision")
}
})
@ -356,13 +352,12 @@ func TestInfoEmptyStringRoundTrip(t *testing.T) {
input := `{"first_deployed":"","last_deployed":"","deleted":"","status":"deployed","description":"test"}`
var info Info
err := json.Unmarshal([]byte(input), &info)
require.NoError(t, err)
require.NoError(t, json.Unmarshal([]byte(input), &info))
// Verify time fields are zero values
assert.True(t, info.FirstDeployed.IsZero())
assert.True(t, info.LastDeployed.IsZero())
assert.True(t, info.Deleted.IsZero())
assert.Zero(t, info.FirstDeployed)
assert.Zero(t, info.LastDeployed)
assert.Zero(t, info.Deleted)
assert.Equal(t, common.StatusDeployed, info.Status)
assert.Equal(t, "test", info.Description)
@ -371,8 +366,7 @@ func TestInfoEmptyStringRoundTrip(t *testing.T) {
require.NoError(t, err)
var result map[string]any
err = json.Unmarshal(data, &result)
require.NoError(t, err)
require.NoError(t, json.Unmarshal(data, &result))
// Zero time values should be omitted due to omitzero tag
assert.NotContains(t, result, "first_deployed")

@ -22,8 +22,10 @@ import (
type ApplyMethod string
const ApplyMethodClientSideApply ApplyMethod = "csa"
const ApplyMethodServerSideApply ApplyMethod = "ssa"
const (
ApplyMethodClientSideApply ApplyMethod = "csa"
ApplyMethodServerSideApply ApplyMethod = "ssa"
)
// Release describes a deployment of a chart, together with the chart
// and the variables used to deploy that chart.

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package util // import "helm.sh/helm/v4/internal/release/v2/util"
package util
import (
v2 "helm.sh/helm/v4/internal/release/v2"

@ -14,28 +14,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package util // import "helm.sh/helm/v4/internal/release/v2/util"
package util
import (
"testing"
"github.com/stretchr/testify/require"
rspb "helm.sh/helm/v4/internal/release/v2"
"helm.sh/helm/v4/pkg/release/common"
)
func TestFilterAny(t *testing.T) {
ls := Any(StatusFilter(common.StatusUninstalled)).Filter(releases)
if len(ls) != 2 {
t.Fatalf("expected 2 results, got '%d'", len(ls))
}
require.Len(t, ls, 2)
r0, r1 := ls[0], ls[1]
switch {
case r0.Info.Status != common.StatusUninstalled:
t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String())
case r1.Info.Status != common.StatusUninstalled:
t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String())
}
require.Equal(t, common.StatusUninstalled, r0.Info.Status)
require.Equal(t, common.StatusUninstalled, r1.Info.Status)
}
func TestFilterAll(t *testing.T) {
@ -47,14 +43,9 @@ func TestFilterAll(t *testing.T) {
})
ls := All(fn).Filter(releases)
if len(ls) != 1 {
t.Fatalf("expected 1 result, got '%d'", len(ls))
}
switch r0 := ls[0]; {
case r0.Version == 4:
t.Fatal("got release with status revision 4")
case r0.Info.Status == common.StatusUninstalled:
t.Fatal("got release with status UNINSTALLED")
}
require.Len(t, ls, 1)
r0 := ls[0]
require.NotEqual(t, 4, r0.Version, "got release with status revision 4")
require.NotEqual(t, common.StatusUninstalled, r0.Info.Status, "got release with status UNINSTALLED")
}

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package util // import "helm.sh/helm/v4/internal/release/v2/util"
package util
import (
"sort"

@ -14,12 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package util // import "helm.sh/helm/v4/internal/release/v2/util"
package util
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
release "helm.sh/helm/v4/internal/release/v2"
)
@ -193,21 +196,15 @@ func TestKindSorter(t *testing.T) {
} {
var buf bytes.Buffer
t.Run(test.description, func(t *testing.T) {
if got, want := len(test.expected), len(manifests); got != want {
t.Fatalf("Expected %d names in order, got %d", want, got)
}
require.Len(t, manifests, len(test.expected), "Expected %d names in order", len(manifests))
defer buf.Reset()
orig := manifests
for _, r := range sortManifestsByKind(manifests, test.order) {
buf.WriteString(r.Name)
}
if got := buf.String(); got != test.expected {
t.Errorf("Expected %q, got %q", test.expected, got)
}
assert.Equal(t, test.expected, buf.String())
for i, manifest := range orig {
if manifest != manifests[i] {
t.Fatal("Expected input to sortManifestsByKind to stay the same")
}
require.Equal(t, manifest, manifests[i], "Expected input to sortManifestsByKind to stay the same")
}
})
}
@ -267,9 +264,7 @@ func TestKindSorterKeepOriginalOrder(t *testing.T) {
for _, r := range sortManifestsByKind(manifests, test.order) {
buf.WriteString(r.Name)
}
if got := buf.String(); got != test.expected {
t.Errorf("Expected %q, got %q", test.expected, got)
}
assert.Equal(t, test.expected, buf.String())
})
}
}
@ -289,9 +284,7 @@ func TestKindSorterNamespaceAgainstUnknown(t *testing.T) {
expectedOrder := []Manifest{namespace, unknown}
for i, manifest := range manifests {
if expectedOrder[i].Name != manifest.Name {
t.Errorf("Expected %s, got %s", expectedOrder[i].Name, manifest.Name)
}
assert.Equal(t, expectedOrder[i].Name, manifest.Name)
}
}
@ -326,22 +319,16 @@ func TestKindSorterForHooks(t *testing.T) {
} {
var buf bytes.Buffer
t.Run(test.description, func(t *testing.T) {
if got, want := len(test.expected), len(hooks); got != want {
t.Fatalf("Expected %d names in order, got %d", want, got)
}
require.Len(t, hooks, len(test.expected), "Expected %d names in order", len(hooks))
defer buf.Reset()
orig := hooks
for _, r := range sortHooksByKind(hooks, test.order) {
buf.WriteString(r.Name)
}
for i, hook := range orig {
if hook != hooks[i] {
t.Fatal("Expected input to sortHooksByKind to stay the same")
}
}
if got := buf.String(); got != test.expected {
t.Errorf("Expected %q, got %q", test.expected, got)
require.Equal(t, hook, hooks[i], "Expected input to sortHooksByKind to stay the same")
}
assert.Equal(t, test.expected, buf.String())
})
}
}

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package util // import "helm.sh/helm/v4/internal/release/v2/util"
package util
import (
"fmt"
@ -34,7 +34,7 @@ type SimpleHead struct {
} `json:"metadata,omitempty"`
}
var sep = regexp.MustCompile("(?:^|\\s*\n)---\\s*")
var sep = regexp.MustCompile(`(?m)^---[ \t]*`)
// SplitManifests takes a manifest string and returns a map containing individual manifests.
//
@ -64,7 +64,7 @@ func SplitManifests(bigFile string) map[string]string {
d = strings.TrimLeftFunc(d, unicode.IsSpace)
res[fmt.Sprintf(tpl, count)] = d
count = count + 1
count++
}
return res
}

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

@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package util // import "helm.sh/helm/v4/internal/release/v2/util"
package util
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sigs.k8s.io/yaml"
release "helm.sh/helm/v4/internal/release/v2"
@ -60,7 +61,8 @@ metadata:
annotations:
"helm.sh/hook": post-install
`,
}, {
},
{
name: []string{"third"},
path: "three",
kind: []string{"ReplicaSet"},
@ -72,7 +74,8 @@ metadata:
annotations:
"helm.sh/hook": no-such-hook
`,
}, {
},
{
name: []string{"fourth"},
path: "four",
kind: []string{"Pod"},
@ -83,7 +86,8 @@ metadata:
name: fourth
annotations:
nothing: here`,
}, {
},
{
name: []string{"fifth"},
path: "five",
kind: []string{"ReplicaSet"},
@ -95,14 +99,16 @@ metadata:
annotations:
"helm.sh/hook": post-delete, post-install
`,
}, {
},
{
// Regression test: files with an underscore in the base name should be skipped.
name: []string{"sixth"},
path: "six/_six",
kind: []string{"ReplicaSet"},
hooks: map[string][]release.HookEvent{"sixth": nil},
manifest: `invalid manifest`, // This will fail if partial is not skipped.
}, {
},
{
// Regression test: files with no content should be skipped.
name: []string{"seventh"},
path: "seven",
@ -138,55 +144,39 @@ metadata:
}
hs, generic, err := SortManifests(manifests, nil, InstallOrder)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
require.NoError(t, err)
// This test will fail if 'six' or 'seven' was added.
if len(generic) != 2 {
t.Errorf("Expected 2 generic manifests, got %d", len(generic))
}
if len(hs) != 4 {
t.Errorf("Expected 4 hooks, got %d", len(hs))
}
assert.Len(t, generic, 2)
assert.Len(t, hs, 4)
for _, out := range hs {
found := false
for _, expect := range data {
if out.Path == expect.path {
found = true
if out.Path != expect.path {
t.Errorf("Expected path %s, got %s", expect.path, out.Path)
}
nameFound := false
for _, expectedName := range expect.name {
if out.Name == expectedName {
nameFound = true
}
}
if !nameFound {
t.Errorf("Got unexpected name %s", out.Name)
}
kindFound := false
for _, expectedKind := range expect.kind {
if out.Kind == expectedKind {
kindFound = true
}
}
if !kindFound {
t.Errorf("Got unexpected kind %s", out.Kind)
if out.Path != expect.path {
continue
}
found = true
assert.Equal(t, expect.path, out.Path)
nameFound := false
for _, expectedName := range expect.name {
if out.Name == expectedName {
nameFound = true
}
expectedHooks := expect.hooks[out.Name]
if !reflect.DeepEqual(expectedHooks, out.Events) {
t.Errorf("expected events: %v but got: %v", expectedHooks, out.Events)
}
assert.True(t, nameFound, "Got unexpected name %s", out.Name)
kindFound := false
for _, expectedKind := range expect.kind {
if out.Kind == expectedKind {
kindFound = true
}
}
assert.True(t, kindFound, "Got unexpected kind %s", out.Kind)
expectedHooks := expect.hooks[out.Name]
assert.Equal(t, expectedHooks, out.Events, "expected events: %v but got: %v", expectedHooks, out.Events)
}
if !found {
t.Errorf("Result not found: %v", out)
}
assert.True(t, found, "Result not found: %v", out)
}
// Verify the sort order
@ -218,8 +208,6 @@ metadata:
sorted = sortManifestsByKind(sorted, InstallOrder)
for i, m := range generic {
if m.Content != sorted[i].Content {
t.Errorf("Expected %q, got %q", m.Content, sorted[i].Content)
}
assert.Equal(t, m.Content, sorted[i].Content)
}
}

@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package util // import "helm.sh/helm/v4/internal/release/v2/util"
package util
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSplitManifests(t *testing.T) {
@ -73,7 +74,7 @@ spec:
name: "whitespace-only doc after separator is skipped",
input: "---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: cm1\n---\n \n",
expected: map[string]string{
"manifest-0": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: cm1",
"manifest-0": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: cm1\n",
},
},
{
@ -109,7 +110,8 @@ metadata:
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm1`,
name: cm1
`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
@ -328,8 +330,8 @@ data:
},
},
// Multi-doc with block scalars: the regex consumes \s*\n before ---,
// so trailing newlines from non-last docs are stripped.
// Multi-doc with block scalars: the separator regex preserves trailing
// newlines from non-last documents.
{
name: "multi-doc block scalar clip (|) before separator",
input: `
@ -353,7 +355,8 @@ metadata:
name: test
data:
key: |
hello`,
hello
`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
@ -386,7 +389,10 @@ metadata:
name: test
data:
key: |+
hello`,
hello
`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
@ -431,7 +437,8 @@ metadata:
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm1`,
name: cm1
`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
@ -459,11 +466,13 @@ metadata:
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm1`,
name: cm1
`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm2`,
name: cm2
`,
"manifest-2": `apiVersion: v1
kind: ConfigMap
metadata:
@ -492,11 +501,13 @@ metadata:
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm1`,
name: cm1
`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm2`,
name: cm2
`,
"manifest-2": `apiVersion: v1
kind: ConfigMap
metadata:
@ -509,9 +520,7 @@ metadata:
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := SplitManifests(tt.input)
if !reflect.DeepEqual(result, tt.expected) {
t.Errorf("SplitManifests() =\n%v\nwant:\n%v", result, tt.expected)
}
assert.Equal(t, tt.expected, result, "SplitManifests() =\n%v\nwant:\n%v", result, tt.expected)
})
}
}

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

Loading…
Cancel
Save