Merge branch 'main' of https://github.com/helm/helm into optional-version

Signed-off-by: Felipe Santos <felipecassiors@gmail.com>
pull/10096/head
Felipe Santos 3 months ago
commit 8a9b854daa

@ -0,0 +1,69 @@
name: Bug Report
description: Report a bug encountered in Helm
labels: kind/bug
body:
- type: textarea
id: problem
attributes:
label: What happened?
description: |
Please provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner.
validations:
required: true
- type: textarea
id: expected
attributes:
label: What did you expect to happen?
validations:
required: true
- type: textarea
id: repro
attributes:
label: How can we reproduce it (as minimally and precisely as possible)?
description: |
Please list steps someone can follow to trigger the issue.
For example:
1. Run `helm install mychart ./path-to-chart -f values.yaml --debug`
2. Observe the following error: ...
You can include:
- a sample `values.yaml` block
- a link to a chart
- specific `helm` commands used
This helps others reproduce and debug your issue more effectively.
validations:
required: true
- type: textarea
id: helmVersion
attributes:
label: Helm version
value: |
<details>
```console
$ helm version
# paste output here
```
</details>
validations:
required: true
- type: textarea
id: kubeVersion
attributes:
label: Kubernetes version
value: |
<details>
```console
$ kubectl version
# paste output here
```
</details>
validations:
required: true

@ -0,0 +1,27 @@
name: Documentation
description: Report any mistakes or missing information from the documentation or the examples
labels: kind/documentation
body:
- type: markdown
attributes:
value: |
⚠️ **Note**: Most documentation lives in [helm/helm-www](https://github.com/helm/helm-www).
If your issue is about Helm website documentation or examples, please [open an issue there](https://github.com/helm/helm-www/issues/new/choose).
- type: textarea
id: feature
attributes:
label: What would you like to be added?
description: |
Link to the issue (please include a link to the specific documentation or example).
Link to the issue raised in [Helm Documentation Improvement Proposal](https://github.com/helm/helm-www)
validations:
required: true
- type: textarea
id: rationale
attributes:
label: Why is this needed?
validations:
required: true

@ -0,0 +1,21 @@
name: Enhancement/feature
description: Provide supporting details for a feature in development
labels: kind/feature
body:
- type: textarea
id: feature
attributes:
label: What would you like to be added?
description: |
Feature requests are unlikely to make progress as issues.
Initial discussion and ideas can happen on an issue.
But significant changes or features must be proposed as a [Helm Improvement Proposal](https://github.com/helm/community/blob/main/hips/hip-0001.md) (HIP)
validations:
required: true
- type: textarea
id: rationale
attributes:
label: Why is this needed?
validations:
required: true

2
.github/env vendored

@ -0,0 +1,2 @@
GOLANG_VERSION=1.24
GOLANGCI_LINT_VERSION=v2.1.0

@ -1,9 +0,0 @@
<!-- If you need help or think you have found a bug, please help us with your issue by entering the following information (otherwise you can delete this text): -->
Output of `helm version`:
Output of `kubectl version`:
Cloud Provider/Platform (AKS, GKE, Minikube etc.):

@ -19,10 +19,12 @@ jobs:
steps:
- name: Checkout source code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # pin@5.3.0
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
with:
go-version: '1.23'
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true
- name: Test source headers are present
run: make test-source-headers

@ -14,13 +14,14 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # pin@5.3.0
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
with:
go-version: '1.23'
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true
- name: golangci-lint
uses: golangci/golangci-lint-action@2226d7cb06a077cd73e56eedd38eecad18e5d837 #pin@6.5.0
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 #pin@8.0.0
with:
version: v1.62
version: ${{ env.GOLANGCI_LINT_VERSION }}

@ -13,10 +13,14 @@ jobs:
name: govulncheck
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # pin@5.3.0
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
with:
go-version: '1.23'
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true
- name: govulncheck
uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # pin@1.0.4

@ -24,14 +24,15 @@ jobs:
with:
fetch-depth: 0
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # pin@5.3.0
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
with:
go-version: '1.23'
go-version: '${{ env.GOLANG_VERSION }}'
- name: Run unit tests
run: make test-coverage
- name: Build Helm Binaries
run: |
set -eu -o pipefail
@ -80,10 +81,13 @@ jobs:
- name: Checkout source code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4.2.2
- name: Add variables to environment file
run: cat ".github/env" >> "$GITHUB_ENV"
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # pin@5.3.0
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # pin@5.5.0
with:
go-version: '1.23'
go-version: '${{ env.GOLANG_VERSION }}'
check-latest: true
- name: Run unit tests

@ -33,7 +33,7 @@ jobs:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
with:
results_file: results.sarif
results_format: sarif
@ -55,7 +55,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: SARIF file
path: results.sarif

1
.gitignore vendored

@ -12,4 +12,5 @@ bin/
vendor/
# 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

@ -1,45 +1,71 @@
run:
timeout: 10m
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
settings:
gofmt:
simplify: true
goimports:
local-prefixes:
- helm.sh/helm/v4
linters:
disable-all: true
default: none
enable:
- depguard
- dupl
- gofmt
- goimports
- gosimple
- gomodguard
- govet
- ineffassign
- misspell
- nakedret
- revive
- unused
- staticcheck
- thelper
- unused
- usestdlibvars
- usetesting
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules: []
warn-unused: true
settings:
depguard:
rules:
Main:
deny:
- pkg: github.com/hashicorp/go-multierror
desc: "use errors instead"
- pkg: github.com/pkg/errors
desc: "use errors instead"
dupl:
threshold: 400
gomodguard:
blocked:
modules:
- github.com/evanphx/json-patch:
recommendations:
- github.com/evanphx/json-patch/v5
run:
timeout: 10m
linters-settings:
gofmt:
simplify: true
goimports:
local-prefixes: helm.sh/helm/v4
dupl:
threshold: 400
issues:
exclude-rules:
# Helm, and the Go source code itself, sometimes uses these names outside their built-in
# functions. As the Go source code has re-used these names it's ok for Helm to do the same.
# Linting will look for redefinition of built-in id's but we opt-in to the ones we choose to use.
- linters:
- revive
text: "redefines-builtin-id: redefinition of the built-in function append"
- linters:
- revive
text: "redefines-builtin-id: redefinition of the built-in function clear"
- linters:
- revive
text: "redefines-builtin-id: redefinition of the built-in function max"
- linters:
- revive
text: "redefines-builtin-id: redefinition of the built-in function min"
- linters:
- revive
text: "redefines-builtin-id: redefinition of the built-in function new"
version: "2"

@ -65,8 +65,8 @@ K8S_MODULES_MINOR_VER=$(word 2,$(K8S_MODULES_VER))
LDFLAGS += -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/lint/rules.k8sVersionMinor=$(K8S_MODULES_MINOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/chart/util.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/chart/util.k8sVersionMinor=$(K8S_MODULES_MINOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/chart/v2/util.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/chart/v2/util.k8sVersionMinor=$(K8S_MODULES_MINOR_VER)
.PHONY: all
all: build
@ -156,7 +156,7 @@ format: $(GOIMPORTS)
# Generate golden files used in unit tests
.PHONY: gen-test-golden
gen-test-golden:
gen-test-golden: PKG = ./cmd/helm ./pkg/action
gen-test-golden: PKG = ./pkg/cmd ./pkg/action
gen-test-golden: TESTFLAGS = -update
gen-test-golden: test-unit

@ -9,6 +9,7 @@ maintainers:
- technosophos
triage:
- banjoh
- TerryHowe
- yxxhero
- zonggen
- z4ce

@ -1,10 +1,11 @@
# Helm
[![Build Status](https://github.com/helm/helm/workflows/release/badge.svg)](https://github.com/helm/helm/actions?workflow=release)
[![Go Report Card](https://goreportcard.com/badge/github.com/helm/helm)](https://goreportcard.com/report/github.com/helm/helm)
[![Go Report Card](https://goreportcard.com/badge/helm.sh/helm/v4)](https://goreportcard.com/report/helm.sh/helm/v4)
[![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/helm.sh/helm/v4)
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/3131/badge)](https://bestpractices.coreinfrastructure.org/projects/3131)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/helm/helm/badge)](https://scorecard.dev/viewer/?uri=github.com/helm/helm)
[![LFX Health Score](https://img.shields.io/static/v1?label=Health%20Score&message=Healthy&color=A7F3D0&logo=linuxfoundation&logoColor=white&style=flat)](https://insights.linuxfoundation.org/project/helm)
Helm is a tool for managing Charts. Charts are packages of pre-configured Kubernetes resources.
@ -56,7 +57,7 @@ including installing pre-releases.
## Docs
Get started with the [Quick Start guide](https://helm.sh/docs/intro/quickstart/) or plunge into the [complete documentation](https://helm.sh/docs)
Get started with the [Quick Start guide](https://helm.sh/docs/intro/quickstart/) or plunge into the [complete documentation](https://helm.sh/docs).
## Roadmap

@ -17,51 +17,16 @@ limitations under the License.
package main // import "helm.sh/helm/v4/cmd/helm"
import (
"fmt"
"io"
"log"
"log/slog"
"os"
"strings"
"time"
"github.com/spf13/cobra"
"sigs.k8s.io/yaml"
// Import to initialize client auth plugins.
_ "k8s.io/client-go/plugin/pkg/client/auth"
"helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/cli"
helmcmd "helm.sh/helm/v4/pkg/cmd"
"helm.sh/helm/v4/pkg/kube"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/storage/driver"
)
var settings = cli.New()
func init() {
log.SetFlags(log.Lshortfile)
}
func debug(format string, v ...interface{}) {
if settings.Debug {
timeNow := time.Now().String()
format = fmt.Sprintf("%s [debug] %s\n", timeNow, format)
log.Output(2, fmt.Sprintf(format, v...))
}
}
func warning(format string, v ...interface{}) {
format = fmt.Sprintf("WARNING: %s\n", format)
fmt.Fprintf(os.Stderr, format, v...)
}
// hookOutputWriter provides the writer for writing hook logs.
func hookOutputWriter(_, _, _ string) io.Writer {
return log.Writer()
}
func main() {
// Setting the name of the app for managedFields in the Kubernetes client.
// It is set here to the full name of "helm" so that renaming of helm to
@ -69,70 +34,18 @@ func main() {
// manager as picked up by the automated name detection.
kube.ManagedFieldsManager = "helm"
actionConfig := new(action.Configuration)
cmd, err := newRootCmd(actionConfig, os.Stdout, os.Args[1:])
cmd, err := helmcmd.NewRootCmd(os.Stdout, os.Args[1:], helmcmd.SetupLogging)
if err != nil {
warning("%+v", err)
slog.Warn("command failed", slog.Any("error", err))
os.Exit(1)
}
// run when each command's execute method is called
cobra.OnInitialize(func() {
helmDriver := os.Getenv("HELM_DRIVER")
if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), helmDriver, debug); err != nil {
log.Fatal(err)
}
if helmDriver == "memory" {
loadReleasesInMemory(actionConfig)
}
actionConfig.SetHookOutputFunc(hookOutputWriter)
})
if err := cmd.Execute(); err != nil {
debug("%+v", err)
switch e := err.(type) {
case pluginError:
os.Exit(e.code)
case helmcmd.PluginError:
os.Exit(e.Code)
default:
os.Exit(1)
}
}
}
// This function loads releases into the memory storage if the
// environment variable is properly set.
func loadReleasesInMemory(actionConfig *action.Configuration) {
filePaths := strings.Split(os.Getenv("HELM_MEMORY_DRIVER_DATA"), ":")
if len(filePaths) == 0 {
return
}
store := actionConfig.Releases
mem, ok := store.Driver.(*driver.Memory)
if !ok {
// For an unexpected reason we are not dealing with the memory storage driver.
return
}
actionConfig.KubeClient = &kubefake.PrintingKubeClient{Out: io.Discard}
for _, path := range filePaths {
b, err := os.ReadFile(path)
if err != nil {
log.Fatal("Unable to read memory driver data", err)
}
releases := []*release.Release{}
if err := yaml.Unmarshal(b, &releases); err != nil {
log.Fatal("Unable to unmarshal memory driver data: ", err)
}
for _, rel := range releases {
if err := store.Create(rel); err != nil {
log.Fatal(err)
}
}
}
// Must reset namespace to the proper one
mem.SetNamespace(settings.Namespace())
}

@ -18,153 +18,12 @@ package main
import (
"bytes"
"io"
"os"
"os/exec"
"runtime"
"strings"
"testing"
shellwords "github.com/mattn/go-shellwords"
"github.com/spf13/cobra"
"helm.sh/helm/v4/internal/test"
"helm.sh/helm/v4/pkg/action"
chartutil "helm.sh/helm/v4/pkg/chart/util"
"helm.sh/helm/v4/pkg/cli"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/storage"
"helm.sh/helm/v4/pkg/storage/driver"
"helm.sh/helm/v4/pkg/time"
)
func testTimestamper() time.Time { return time.Unix(242085845, 0).UTC() }
func init() {
action.Timestamper = testTimestamper
}
func runTestCmd(t *testing.T, tests []cmdTestCase) {
t.Helper()
for _, tt := range tests {
for i := 0; i <= tt.repeat; i++ {
t.Run(tt.name, func(t *testing.T) {
defer resetEnv()()
storage := storageFixture()
for _, rel := range tt.rels {
if err := storage.Create(rel); err != nil {
t.Fatal(err)
}
}
t.Logf("running cmd (attempt %d): %s", i+1, tt.cmd)
_, out, err := executeActionCommandC(storage, tt.cmd)
if tt.wantError && err == nil {
t.Errorf("expected error, got success with the following output:\n%s", out)
}
if !tt.wantError && err != nil {
t.Errorf("expected no error, got: '%v'", err)
}
if tt.golden != "" {
test.AssertGoldenString(t, out, tt.golden)
}
})
}
}
}
func storageFixture() *storage.Storage {
return storage.Init(driver.NewMemory())
}
func executeActionCommandC(store *storage.Storage, cmd string) (*cobra.Command, string, error) {
return executeActionCommandStdinC(store, nil, cmd)
}
func executeActionCommandStdinC(store *storage.Storage, in *os.File, cmd string) (*cobra.Command, string, error) {
args, err := shellwords.Parse(cmd)
if err != nil {
return nil, "", err
}
buf := new(bytes.Buffer)
actionConfig := &action.Configuration{
Releases: store,
KubeClient: &kubefake.PrintingKubeClient{Out: io.Discard},
Capabilities: chartutil.DefaultCapabilities,
Log: func(_ string, _ ...interface{}) {},
}
root, err := newRootCmd(actionConfig, buf, args)
if err != nil {
return nil, "", err
}
root.SetOut(buf)
root.SetErr(buf)
root.SetArgs(args)
oldStdin := os.Stdin
if in != nil {
root.SetIn(in)
os.Stdin = in
}
if mem, ok := store.Driver.(*driver.Memory); ok {
mem.SetNamespace(settings.Namespace())
}
c, err := root.ExecuteC()
result := buf.String()
os.Stdin = oldStdin
return c, result, err
}
// cmdTestCase describes a test case that works with releases.
type cmdTestCase struct {
name string
cmd string
golden string
wantError bool
// Rels are the available releases at the start of the test.
rels []*release.Release
// Number of repeats (in case a feature was previously flaky and the test checks
// it's now stably producing identical results). 0 means test is run exactly once.
repeat int
}
func executeActionCommand(cmd string) (*cobra.Command, string, error) {
return executeActionCommandC(storageFixture(), cmd)
}
func resetEnv() func() {
origEnv := os.Environ()
return func() {
os.Clearenv()
for _, pair := range origEnv {
kv := strings.SplitN(pair, "=", 2)
os.Setenv(kv[0], kv[1])
}
settings = cli.New()
}
}
func testChdir(t *testing.T, dir string) func() {
t.Helper()
old, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(dir); err != nil {
t.Fatal(err)
}
return func() { os.Chdir(old) }
}
func TestPluginExitCode(t *testing.T) {
if os.Getenv("RUN_MAIN_FOR_TESTING") == "1" {
os.Args = []string{"helm", "exitwith", "2"}
@ -190,10 +49,8 @@ func TestPluginExitCode(t *testing.T) {
"RUN_MAIN_FOR_TESTING=1",
// See pkg/cli/environment.go for which envvars can be used for configuring these passes
// and also see plugin_test.go for how a plugin env can be set up.
// We just does the same setup as plugin_test.go via envvars
"HELM_PLUGINS=testdata/helmhome/helm/plugins",
"HELM_REPOSITORY_CONFIG=testdata/helmhome/helm/repositories.yaml",
"HELM_REPOSITORY_CACHE=testdata/helmhome/helm/repository",
// This mimics the "exitwith" test case in TestLoadPlugins using envvars
"HELM_PLUGINS=../../pkg/cmd/testdata/helmhome/helm/plugins",
)
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}

@ -1 +0,0 @@
Error: INSTALLATION FAILED: Hiding Kubernetes secrets requires a dry-run mode

@ -1,8 +0,0 @@
---
# Source: issue-totoml/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: issue-totoml
data: |
key = 13

@ -1,3 +0,0 @@
apiVersion: v2
name: issue-totoml
version: 0.1.0

@ -1,6 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: issue-totoml
data: |
{{ .Values.global | toToml }}

139
go.mod

@ -1,51 +1,51 @@
module helm.sh/helm/v4
go 1.23.0
go 1.24.0
require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24
github.com/BurntSushi/toml v1.4.0
github.com/BurntSushi/toml v1.5.0
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/Masterminds/semver/v3 v3.3.0
github.com/Masterminds/semver/v3 v3.4.0
github.com/Masterminds/sprig/v3 v3.3.0
github.com/Masterminds/squirrel v1.5.4
github.com/Masterminds/vcs v1.13.3
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/containerd/containerd v1.7.25
github.com/cyphar/filepath-securejoin v0.4.1
github.com/distribution/distribution/v3 v3.0.0-rc.3
github.com/evanphx/json-patch v5.9.11+incompatible
github.com/distribution/distribution/v3 v3.0.0
github.com/evanphx/json-patch/v5 v5.9.11
github.com/fluxcd/cli-utils v0.36.0-flux.13
github.com/foxcpp/go-mockdns v1.1.0
github.com/gobwas/glob v0.2.3
github.com/gofrs/flock v0.12.1
github.com/gosuri/uitable v0.0.4
github.com/hashicorp/go-multierror v1.1.1
github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.10.9
github.com/mattn/go-shellwords v1.0.12
github.com/mitchellh/copystructure v1.2.0
github.com/moby/term v0.5.2
github.com/opencontainers/image-spec v1.1.0
github.com/opencontainers/image-spec v1.1.1
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5
github.com/pkg/errors v0.9.1
github.com/rubenv/sql-migrate v1.7.1
github.com/rubenv/sql-migrate v1.8.0
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0
github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/crypto v0.33.0
golang.org/x/term v0.29.0
golang.org/x/text v0.22.0
k8s.io/api v0.32.2
k8s.io/apiextensions-apiserver v0.32.2
k8s.io/apimachinery v0.32.2
k8s.io/apiserver v0.32.2
k8s.io/cli-runtime v0.32.2
k8s.io/client-go v0.32.2
golang.org/x/crypto v0.39.0
golang.org/x/term v0.32.0
golang.org/x/text v0.26.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.33.2
k8s.io/apiextensions-apiserver v0.33.2
k8s.io/apimachinery v0.33.2
k8s.io/apiserver v0.33.2
k8s.io/cli-runtime v0.33.2
k8s.io/client-go v0.33.2
k8s.io/klog/v2 v2.130.1
k8s.io/kubectl v0.32.2
oras.land/oras-go/v2 v2.5.0
sigs.k8s.io/yaml v1.4.0
k8s.io/kubectl v0.33.2
oras.land/oras-go/v2 v2.6.0
sigs.k8s.io/controller-runtime v0.21.0
sigs.k8s.io/yaml v1.5.0
)
require (
@ -59,9 +59,6 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/containerd/errdefs v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@ -70,43 +67,40 @@ require (
github.com/docker/docker-credential-helpers v0.8.2 // indirect
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/handlers v1.5.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect
github.com/hashicorp/golang-lru/arc/v2 v2.0.5 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
@ -119,66 +113,69 @@ require (
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/onsi/gomega v1.37.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.60.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 // indirect
github.com/redis/go-redis/v9 v9.1.0 // indirect
github.com/redis/go-redis/v9 v9.7.3 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 // indirect
go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect
go.opentelemetry.io/otel v1.32.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
go.opentelemetry.io/otel v1.34.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.54.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 // indirect
go.opentelemetry.io/otel/log v0.8.0 // indirect
go.opentelemetry.io/otel/metric v1.32.0 // indirect
go.opentelemetry.io/otel/sdk v1.32.0 // indirect
go.opentelemetry.io/otel/metric v1.34.0 // indirect
go.opentelemetry.io/otel/sdk v1.33.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.8.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
go.opentelemetry.io/otel/trace v1.32.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.26.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect
google.golang.org/grpc v1.68.0 // indirect
google.golang.org/protobuf v1.35.2 // indirect
go.opentelemetry.io/otel/trace v1.34.0 // indirect
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.3 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/oauth2 v0.29.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.33.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/grpc v1.68.1 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/component-base v0.32.2 // indirect
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/kustomize/api v0.18.0 // indirect
sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
k8s.io/component-base v0.33.2 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/kustomize/api v0.19.0 // indirect
sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
)

304
go.sum

@ -6,16 +6,16 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
@ -37,10 +37,11 @@ github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2y
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
github.com/bsm/ginkgo/v2 v2.9.5 h1:rtVBYPs3+TC5iLUVOis1B9tjLTup7Cj5IfzosKtvTJ0=
github.com/bsm/ginkgo/v2 v2.9.5/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@ -48,19 +49,10 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk=
github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=
github.com/containerd/containerd v1.7.25 h1:khEQOAXOEJalRO228yzVsuASLH42vT7DIo9Ss+9SMFQ=
github.com/containerd/containerd v1.7.25/go.mod h1:tWfHzVI0azhw4CT2vaIjsb2CoV4LJ9PrMPaULAr21Ok=
github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4=
github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
@ -71,34 +63,38 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/distribution/distribution/v3 v3.0.0-rc.3 h1:JRJso9IVLoooKX76oWR+DWCCdZlK5m4nRtDWvzB1ITg=
github.com/distribution/distribution/v3 v3.0.0-rc.3/go.mod h1:offoOgrnYs+CFwis8nE0hyzYZqRCZj5EFc5kgfszwiE=
github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM=
github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8=
github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fluxcd/cli-utils v0.36.0-flux.13 h1:2X5yjz/rk9mg7+bMFBDZKGKzeZpAmY2s6iwbNZz7OzM=
github.com/fluxcd/cli-utils v0.36.0-flux.13/go.mod h1:b2iSoIeDTtjfCB0IKtGgqlhhvWa1oux3e90CjOf81oA=
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@ -109,12 +105,12 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
@ -135,19 +131,17 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@ -156,19 +150,14 @@ github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyE
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw=
github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU=
github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4=
@ -189,15 +178,12 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
@ -210,8 +196,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@ -253,14 +239,14 @@ 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/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/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.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
@ -276,16 +262,16 @@ github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjz
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
@ -296,14 +282,16 @@ github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJu
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc=
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ=
github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY=
github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c=
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4=
github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4=
github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o=
github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw=
github.com/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/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
@ -319,41 +307,31 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w=
go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk=
go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4=
go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8=
@ -362,10 +340,10 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7Z
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI=
go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU=
@ -378,20 +356,30 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsu
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s=
go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk=
go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs=
go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -400,16 +388,16 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -423,10 +411,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -437,8 +425,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.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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=
@ -460,8 +448,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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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=
@ -469,8 +457,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.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
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=
@ -478,10 +466,10 @@ 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.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@ -490,20 +478,20 @@ 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.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g=
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@ -518,37 +506,43 @@ 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.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw=
k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y=
k8s.io/apiextensions-apiserver v0.32.2 h1:2YMk285jWMk2188V2AERy5yDwBYrjgWYggscghPCvV4=
k8s.io/apiextensions-apiserver v0.32.2/go.mod h1:GPwf8sph7YlJT3H6aKUWtd0E+oyShk/YHWQHf/OOgCA=
k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ=
k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/apiserver v0.32.2 h1:WzyxAu4mvLkQxwD9hGa4ZfExo3yZZaYzoYvvVDlM6vw=
k8s.io/apiserver v0.32.2/go.mod h1:PEwREHiHNU2oFdte7BjzA1ZyjWjuckORLIK/wLV5goM=
k8s.io/cli-runtime v0.32.2 h1:aKQR4foh9qeyckKRkNXUccP9moxzffyndZAvr+IXMks=
k8s.io/cli-runtime v0.32.2/go.mod h1:a/JpeMztz3xDa7GCyyShcwe55p8pbcCVQxvqZnIwXN8=
k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA=
k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94=
k8s.io/component-base v0.32.2 h1:1aUL5Vdmu7qNo4ZsE+569PV5zFatM9hl+lb3dEea2zU=
k8s.io/component-base v0.32.2/go.mod h1:PXJ61Vx9Lg+P5mS8TLd7bCIr+eMJRQTyXe8KvkrvJq0=
k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY=
k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs=
k8s.io/apiextensions-apiserver v0.33.2 h1:6gnkIbngnaUflR3XwE1mCefN3YS8yTD631JXQhsU6M8=
k8s.io/apiextensions-apiserver v0.33.2/go.mod h1:IvVanieYsEHJImTKXGP6XCOjTwv2LUMos0YWc9O+QP8=
k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apiserver v0.33.2 h1:KGTRbxn2wJagJowo29kKBp4TchpO1DRO3g+dB/KOJN4=
k8s.io/apiserver v0.33.2/go.mod h1:9qday04wEAMLPWWo9AwqCZSiIn3OYSZacDyu/AcoM/M=
k8s.io/cli-runtime v0.33.2 h1:koNYQKSDdq5AExa/RDudXMhhtFasEg48KLS2KSAU74Y=
k8s.io/cli-runtime v0.33.2/go.mod h1:gnhsAWpovqf1Zj5YRRBBU7PFsRc6NkEkwYNQE+mXL88=
k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E=
k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo=
k8s.io/component-base v0.33.2 h1:sCCsn9s/dG3ZrQTX/Us0/Sx2R0G5kwa0wbZFYoVp/+0=
k8s.io/component-base v0.33.2/go.mod h1:/41uw9wKzuelhN+u+/C59ixxf4tYQKW7p32ddkYNe2k=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
k8s.io/kubectl v0.32.2 h1:TAkag6+XfSBgkqK9I7ZvwtF0WVtUAvK8ZqTt+5zi1Us=
k8s.io/kubectl v0.32.2/go.mod h1:+h/NQFSPxiDZYX/WZaWw9fwYezGLISP0ud8nQKg+3g8=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c=
oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo=
sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U=
sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E=
sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
k8s.io/kubectl v0.33.2 h1:7XKZ6DYCklu5MZQzJe+CkCjoGZwD1wWl7t/FxzhMz7Y=
k8s.io/kubectl v0.33.2/go.mod h1:8rC67FB8tVTYraovAGNi/idWIK90z2CHFNMmGJZJ3KI=
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro=
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=
sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8=
sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ=
sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o=
sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA=
sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=

@ -0,0 +1,87 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package logging
import (
"context"
"log/slog"
"os"
)
// DebugEnabledFunc is a function type that determines if debug logging is enabled
// We use a function because we want to check the setting at log time, not when the logger is created
type DebugEnabledFunc func() bool
// DebugCheckHandler checks settings.Debug at log time
type DebugCheckHandler struct {
handler slog.Handler
debugEnabled DebugEnabledFunc
}
// Enabled implements slog.Handler.Enabled
func (h *DebugCheckHandler) Enabled(_ context.Context, level slog.Level) bool {
if level == slog.LevelDebug {
return h.debugEnabled()
}
return true // Always log other levels
}
// Handle implements slog.Handler.Handle
func (h *DebugCheckHandler) Handle(ctx context.Context, r slog.Record) error {
return h.handler.Handle(ctx, r)
}
// WithAttrs implements slog.Handler.WithAttrs
func (h *DebugCheckHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &DebugCheckHandler{
handler: h.handler.WithAttrs(attrs),
debugEnabled: h.debugEnabled,
}
}
// WithGroup implements slog.Handler.WithGroup
func (h *DebugCheckHandler) WithGroup(name string) slog.Handler {
return &DebugCheckHandler{
handler: h.handler.WithGroup(name),
debugEnabled: h.debugEnabled,
}
}
// NewLogger creates a new logger with dynamic debug checking
func NewLogger(debugEnabled DebugEnabledFunc) *slog.Logger {
// Create base handler that removes timestamps
baseHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
// Always use LevelDebug here to allow all messages through
// Our custom handler will do the filtering
Level: slog.LevelDebug,
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
// Remove the time attribute
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
})
// Wrap with our dynamic debug-checking handler
dynamicHandler := &DebugCheckHandler{
handler: baseHandler,
debugEnabled: debugEnabled,
}
return slog.New(dynamicHandler)
}

@ -29,9 +29,6 @@ type Client struct {
// The base URL for requests
BaseURL string
// The internal logger to use
Log func(string, ...interface{})
}
// New creates a new client
@ -44,12 +41,9 @@ func New(u string) (*Client, error) {
return &Client{
BaseURL: u,
Log: nopLogger,
}, nil
}
var nopLogger = func(_ string, _ ...interface{}) {}
// Validate if the base URL for monocular is valid.
func validate(u string) error {

@ -25,7 +25,7 @@ import (
"time"
"helm.sh/helm/v4/internal/version"
"helm.sh/helm/v4/pkg/chart"
chart "helm.sh/helm/v4/pkg/chart/v2"
)
// SearchPath is the url path to the search API in monocular.
@ -129,7 +129,7 @@ func (c *Client) Search(term string) ([]SearchResult, error) {
}
defer res.Body.Close()
if res.StatusCode != 200 {
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch %s : %s", p.String(), res.Status)
}

@ -18,17 +18,18 @@ package resolver
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"time"
"github.com/Masterminds/semver/v3"
"github.com/pkg/errors"
"helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v4/pkg/chart/loader"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/chart/v2/loader"
"helm.sh/helm/v4/pkg/helmpath"
"helm.sh/helm/v4/pkg/provenance"
"helm.sh/helm/v4/pkg/registry"
@ -66,7 +67,7 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
if d.Version != "" {
constraint, err = semver.NewConstraint(d.Version)
if err != nil {
return nil, errors.Wrapf(err, "dependency %q has an invalid version/constraint format", d.Name)
return nil, fmt.Errorf("dependency %q has an invalid version/constraint format: %w", d.Name, err)
}
}
@ -132,12 +133,12 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
if !registry.IsOCI(d.Repository) {
repoIndex, err := repo.LoadIndexFile(filepath.Join(r.cachepath, helmpath.CacheIndexFile(repoName)))
if err != nil {
return nil, errors.Wrapf(err, "no cached repository for %s found. (try 'helm repo update')", repoName)
return nil, fmt.Errorf("no cached repository for %s found. (try 'helm repo update'): %w", repoName, err)
}
vs, ok = repoIndex.Entries[d.Name]
if !ok {
return nil, errors.Errorf("%s chart not found in repo %s", d.Name, d.Repository)
return nil, fmt.Errorf("%s chart not found in repo %s", d.Name, d.Repository)
}
found = false
} else {
@ -159,7 +160,7 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
ref := fmt.Sprintf("%s/%s", strings.TrimPrefix(d.Repository, fmt.Sprintf("%s://", registry.OCIScheme)), d.Name)
tags, err := r.registryClient.Tags(ref)
if err != nil {
return nil, errors.Wrapf(err, "could not retrieve list of tags for repository %s", d.Repository)
return nil, fmt.Errorf("could not retrieve list of tags for repository %s: %w", d.Repository, err)
}
vs = make(repo.ChartVersions, len(tags))
@ -200,7 +201,7 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
}
}
if len(missing) > 0 {
return nil, errors.Errorf("can't get a valid version for %d subchart(s): %s. Make sure a matching chart version exists in the repo, or change the version constraint in Chart.yaml", len(missing), strings.Join(missing, ", "))
return nil, fmt.Errorf("can't get a valid version for %d subchart(s): %s. Make sure a matching chart version exists in the repo, or change the version constraint in Chart.yaml", len(missing), strings.Join(missing, ", "))
}
digest, err := HashReq(reqs, locked)
@ -260,8 +261,8 @@ func GetLocalPath(repo, chartpath string) (string, error) {
depPath = filepath.Join(chartpath, p)
}
if _, err = os.Stat(depPath); os.IsNotExist(err) {
return "", errors.Errorf("directory %s not found", depPath)
if _, err = os.Stat(depPath); errors.Is(err, fs.ErrNotExist) {
return "", fmt.Errorf("directory %s not found", depPath)
} else if err != nil {
return "", err
}

@ -19,7 +19,7 @@ import (
"runtime"
"testing"
"helm.sh/helm/v4/pkg/chart"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/registry"
)

@ -0,0 +1,121 @@
/*
Copyright The Helm Authors.
This file was initially copied and modified from
https://github.com/fluxcd/kustomize-controller/blob/main/internal/statusreaders/job.go
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package statusreaders
import (
"context"
"fmt"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/statusreaders"
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
"github.com/fluxcd/cli-utils/pkg/object"
)
type customJobStatusReader struct {
genericStatusReader engine.StatusReader
}
func NewCustomJobStatusReader(mapper meta.RESTMapper) engine.StatusReader {
genericStatusReader := statusreaders.NewGenericStatusReader(mapper, jobConditions)
return &customJobStatusReader{
genericStatusReader: genericStatusReader,
}
}
func (j *customJobStatusReader) Supports(gk schema.GroupKind) bool {
return gk == batchv1.SchemeGroupVersion.WithKind("Job").GroupKind()
}
func (j *customJobStatusReader) ReadStatus(ctx context.Context, reader engine.ClusterReader, resource object.ObjMetadata) (*event.ResourceStatus, error) {
return j.genericStatusReader.ReadStatus(ctx, reader, resource)
}
func (j *customJobStatusReader) ReadStatusForObject(ctx context.Context, reader engine.ClusterReader, resource *unstructured.Unstructured) (*event.ResourceStatus, error) {
return j.genericStatusReader.ReadStatusForObject(ctx, reader, resource)
}
// Ref: https://github.com/kubernetes-sigs/cli-utils/blob/v0.29.4/pkg/kstatus/status/core.go
// Modified to return Current status only when the Job has completed as opposed to when it's in progress.
func jobConditions(u *unstructured.Unstructured) (*status.Result, error) {
obj := u.UnstructuredContent()
parallelism := status.GetIntField(obj, ".spec.parallelism", 1)
completions := status.GetIntField(obj, ".spec.completions", parallelism)
succeeded := status.GetIntField(obj, ".status.succeeded", 0)
failed := status.GetIntField(obj, ".status.failed", 0)
// Conditions
// https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/job/utils.go#L24
objc, err := status.GetObjectWithConditions(obj)
if err != nil {
return nil, err
}
for _, c := range objc.Status.Conditions {
switch c.Type {
case "Complete":
if c.Status == corev1.ConditionTrue {
message := fmt.Sprintf("Job Completed. succeeded: %d/%d", succeeded, completions)
return &status.Result{
Status: status.CurrentStatus,
Message: message,
Conditions: []status.Condition{},
}, nil
}
case "Failed":
message := fmt.Sprintf("Job Failed. failed: %d/%d", failed, completions)
if c.Status == corev1.ConditionTrue {
return &status.Result{
Status: status.FailedStatus,
Message: message,
Conditions: []status.Condition{
{
Type: status.ConditionStalled,
Status: corev1.ConditionTrue,
Reason: "JobFailed",
Message: message,
},
},
}, nil
}
}
}
message := "Job in progress"
return &status.Result{
Status: status.InProgressStatus,
Message: message,
Conditions: []status.Condition{
{
Type: status.ConditionReconciling,
Status: corev1.ConditionTrue,
Reason: "JobInProgress",
Message: message,
},
},
}, nil
}

@ -0,0 +1,116 @@
/*
Copyright The Helm Authors.
This file was initially copied and modified from
https://github.com/fluxcd/kustomize-controller/blob/main/internal/statusreaders/job_test.go
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package statusreaders
import (
"testing"
"github.com/stretchr/testify/assert"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
)
func toUnstructured(t *testing.T, obj runtime.Object) (*unstructured.Unstructured, error) {
t.Helper()
// If the incoming object is already unstructured, perform a deep copy first
// otherwise DefaultUnstructuredConverter ends up returning the inner map without
// making a copy.
if _, ok := obj.(runtime.Unstructured); ok {
obj = obj.DeepCopyObject()
}
rawMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return nil, err
}
return &unstructured.Unstructured{Object: rawMap}, nil
}
func TestJobConditions(t *testing.T) {
t.Parallel()
tests := []struct {
name string
job *batchv1.Job
expectedStatus status.Status
}{
{
name: "job without Complete condition returns InProgress status",
job: &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "job-no-condition",
},
Spec: batchv1.JobSpec{},
Status: batchv1.JobStatus{},
},
expectedStatus: status.InProgressStatus,
},
{
name: "job with Complete condition as True returns Current status",
job: &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "job-complete",
},
Spec: batchv1.JobSpec{},
Status: batchv1.JobStatus{
Conditions: []batchv1.JobCondition{
{
Type: batchv1.JobComplete,
Status: corev1.ConditionTrue,
},
},
},
},
expectedStatus: status.CurrentStatus,
},
{
name: "job with Failed condition as True returns Failed status",
job: &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "job-failed",
},
Spec: batchv1.JobSpec{},
Status: batchv1.JobStatus{
Conditions: []batchv1.JobCondition{
{
Type: batchv1.JobFailed,
Status: corev1.ConditionTrue,
},
},
},
},
expectedStatus: status.FailedStatus,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
us, err := toUnstructured(t, tc.job)
assert.NoError(t, err)
result, err := jobConditions(us)
assert.NoError(t, err)
assert.Equal(t, tc.expectedStatus, result.Status)
})
}
}

@ -0,0 +1,104 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package statusreaders
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/event"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/statusreaders"
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
"github.com/fluxcd/cli-utils/pkg/object"
)
type customPodStatusReader struct {
genericStatusReader engine.StatusReader
}
func NewCustomPodStatusReader(mapper meta.RESTMapper) engine.StatusReader {
genericStatusReader := statusreaders.NewGenericStatusReader(mapper, podConditions)
return &customPodStatusReader{
genericStatusReader: genericStatusReader,
}
}
func (j *customPodStatusReader) Supports(gk schema.GroupKind) bool {
return gk == corev1.SchemeGroupVersion.WithKind("Pod").GroupKind()
}
func (j *customPodStatusReader) ReadStatus(ctx context.Context, reader engine.ClusterReader, resource object.ObjMetadata) (*event.ResourceStatus, error) {
return j.genericStatusReader.ReadStatus(ctx, reader, resource)
}
func (j *customPodStatusReader) ReadStatusForObject(ctx context.Context, reader engine.ClusterReader, resource *unstructured.Unstructured) (*event.ResourceStatus, error) {
return j.genericStatusReader.ReadStatusForObject(ctx, reader, resource)
}
func podConditions(u *unstructured.Unstructured) (*status.Result, error) {
obj := u.UnstructuredContent()
phase := status.GetStringField(obj, ".status.phase", "")
switch corev1.PodPhase(phase) {
case corev1.PodSucceeded:
message := fmt.Sprintf("pod %s succeeded", u.GetName())
return &status.Result{
Status: status.CurrentStatus,
Message: message,
Conditions: []status.Condition{
{
Type: status.ConditionStalled,
Status: corev1.ConditionTrue,
Message: message,
},
},
}, nil
case corev1.PodFailed:
message := fmt.Sprintf("pod %s failed", u.GetName())
return &status.Result{
Status: status.FailedStatus,
Message: message,
Conditions: []status.Condition{
{
Type: status.ConditionStalled,
Status: corev1.ConditionTrue,
Reason: "PodFailed",
Message: message,
},
},
}, nil
}
message := "Pod in progress"
return &status.Result{
Status: status.InProgressStatus,
Message: message,
Conditions: []status.Condition{
{
Type: status.ConditionReconciling,
Status: corev1.ConditionTrue,
Reason: "PodInProgress",
Message: message,
},
},
}, nil
}

@ -0,0 +1,111 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package statusreaders
import (
"testing"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
)
func TestPodConditions(t *testing.T) {
tests := []struct {
name string
pod *v1.Pod
expectedStatus status.Status
}{
{
name: "pod without status returns in progress",
pod: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod-no-status"},
Spec: v1.PodSpec{},
Status: v1.PodStatus{},
},
expectedStatus: status.InProgressStatus,
},
{
name: "pod succeeded returns current status",
pod: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod-succeeded"},
Spec: v1.PodSpec{},
Status: v1.PodStatus{
Phase: v1.PodSucceeded,
},
},
expectedStatus: status.CurrentStatus,
},
{
name: "pod failed returns failed status",
pod: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod-failed"},
Spec: v1.PodSpec{},
Status: v1.PodStatus{
Phase: v1.PodFailed,
},
},
expectedStatus: status.FailedStatus,
},
{
name: "pod pending returns in progress status",
pod: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod-pending"},
Spec: v1.PodSpec{},
Status: v1.PodStatus{
Phase: v1.PodPending,
},
},
expectedStatus: status.InProgressStatus,
},
{
name: "pod running returns in progress status",
pod: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod-running"},
Spec: v1.PodSpec{},
Status: v1.PodStatus{
Phase: v1.PodRunning,
},
},
expectedStatus: status.InProgressStatus,
},
{
name: "pod with unknown phase returns in progress status",
pod: &v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod-unknown"},
Spec: v1.PodSpec{},
Status: v1.PodStatus{
Phase: v1.PodUnknown,
},
},
expectedStatus: status.InProgressStatus,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
us, err := toUnstructured(t, tc.pod)
assert.NoError(t, err)
result, err := podConditions(us)
assert.NoError(t, err)
assert.Equal(t, tc.expectedStatus, result.Status)
})
}
}

@ -21,12 +21,11 @@ limitations under the License.
package sympath
import (
"log"
"fmt"
"log/slog"
"os"
"path/filepath"
"sort"
"github.com/pkg/errors"
)
// Walk walks the file tree rooted at root, calling walkFn for each file or directory
@ -69,10 +68,10 @@ func symwalk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error {
if IsSymlink(info) {
resolved, err := filepath.EvalSymlinks(path)
if err != nil {
return errors.Wrapf(err, "error evaluating symlink %s", path)
return fmt.Errorf("error evaluating symlink %s: %w", path, err)
}
//This log message is to highlight a symlink that is being used within a chart, symlinks can be used for nefarious reasons.
log.Printf("found symbolic link in path: %s resolves to %s. Contents of linked file included and used", path, resolved)
slog.Info("found symbolic link in path. Contents of linked file included and used", "path", path, "resolved", resolved)
if info, err = os.Lstat(resolved); err != nil {
return err
}

@ -76,6 +76,7 @@ func walkTree(n *Node, path string, f func(path string, n *Node)) {
}
func makeTree(t *testing.T) {
t.Helper()
walkTree(tree, tree.name, func(path string, n *Node) {
if n.entries == nil {
if n.symLinkedTo != "" {
@ -99,6 +100,7 @@ func makeTree(t *testing.T) {
}
func checkMarks(t *testing.T, report bool) {
t.Helper()
walkTree(tree, tree.name, func(path string, n *Node) {
if n.marks != n.expectedMarks && report {
t.Errorf("node %s mark = %d; expected %d", path, n.marks, n.expectedMarks)

@ -29,12 +29,12 @@ import (
func HelmHome(t *testing.T) {
t.Helper()
base := t.TempDir()
os.Setenv(xdg.CacheHomeEnvVar, base)
os.Setenv(xdg.ConfigHomeEnvVar, base)
os.Setenv(xdg.DataHomeEnvVar, base)
os.Setenv(helmpath.CacheHomeEnvVar, "")
os.Setenv(helmpath.ConfigHomeEnvVar, "")
os.Setenv(helmpath.DataHomeEnvVar, "")
t.Setenv(xdg.CacheHomeEnvVar, base)
t.Setenv(xdg.ConfigHomeEnvVar, base)
t.Setenv(xdg.DataHomeEnvVar, base)
t.Setenv(helmpath.CacheHomeEnvVar, "")
t.Setenv(helmpath.ConfigHomeEnvVar, "")
t.Setenv(helmpath.DataHomeEnvVar, "")
}
// TempFile ensures a temp file for unit testing purposes.
@ -46,9 +46,10 @@ func HelmHome(t *testing.T) {
// tempdir := TempFile(t, "foo", []byte("bar"))
// filename := filepath.Join(tempdir, "foo")
func TempFile(t *testing.T, name string, data []byte) string {
t.Helper()
path := t.TempDir()
filename := filepath.Join(path, name)
if err := os.WriteFile(filename, data, 0755); err != nil {
if err := os.WriteFile(filename, data, 0o755); err != nil {
t.Fatal(err)
}
return path

@ -19,10 +19,9 @@ package test
import (
"bytes"
"flag"
"fmt"
"os"
"path/filepath"
"github.com/pkg/errors"
)
// UpdateGolden writes out the golden files with the latest values, rather than failing the test.
@ -75,11 +74,11 @@ func compare(actual []byte, filename string) error {
expected, err := os.ReadFile(filename)
if err != nil {
return errors.Wrapf(err, "unable to read testdata %s", filename)
return fmt.Errorf("unable to read testdata %s: %w", filename, err)
}
expected = normalize(expected)
if !bytes.Equal(expected, actual) {
return errors.Errorf("does not match golden file %s\n\nWANT:\n'%s'\n\nGOT:\n'%s'", filename, expected, actual)
return fmt.Errorf("does not match golden file %s\n\nWANT:\n'%s'\n\nGOT:\n'%s'", filename, expected, actual)
}
return nil
}
@ -92,5 +91,5 @@ func update(filename string, in []byte) error {
}
func normalize(in []byte) []byte {
return bytes.Replace(in, []byte("\r\n"), []byte("\n"), -1)
return bytes.ReplaceAll(in, []byte("\r\n"), []byte("\n"))
}

@ -32,13 +32,14 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package fs
import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"runtime"
"syscall"
"github.com/pkg/errors"
)
// fs contains a copy of a few functions from dep tool code to avoid a dependency on golang/dep.
@ -51,7 +52,7 @@ import (
func RenameWithFallback(src, dst string) error {
_, err := os.Stat(src)
if err != nil {
return errors.Wrapf(err, "cannot stat %s", src)
return fmt.Errorf("cannot stat %s: %w", src, err)
}
err = os.Rename(src, dst)
@ -69,20 +70,24 @@ func renameByCopy(src, dst string) error {
if dir, _ := IsDir(src); dir {
cerr = CopyDir(src, dst)
if cerr != nil {
cerr = errors.Wrap(cerr, "copying directory failed")
cerr = fmt.Errorf("copying directory failed: %w", cerr)
}
} else {
cerr = copyFile(src, dst)
if cerr != nil {
cerr = errors.Wrap(cerr, "copying file failed")
cerr = fmt.Errorf("copying file failed: %w", cerr)
}
}
if cerr != nil {
return errors.Wrapf(cerr, "rename fallback failed: cannot rename %s to %s", src, dst)
return fmt.Errorf("rename fallback failed: cannot rename %s to %s: %w", src, dst, cerr)
}
if err := os.RemoveAll(src); err != nil {
return fmt.Errorf("cannot delete %s: %w", src, err)
}
return errors.Wrapf(os.RemoveAll(src), "cannot delete %s", src)
return nil
}
var (
@ -107,7 +112,7 @@ func CopyDir(src, dst string) error {
}
_, err = os.Stat(dst)
if err != nil && !os.IsNotExist(err) {
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
}
if err == nil {
@ -115,12 +120,12 @@ func CopyDir(src, dst string) error {
}
if err = os.MkdirAll(dst, fi.Mode()); err != nil {
return errors.Wrapf(err, "cannot mkdir %s", dst)
return fmt.Errorf("cannot mkdir %s: %w", dst, err)
}
entries, err := os.ReadDir(src)
if err != nil {
return errors.Wrapf(err, "cannot read directory %s", dst)
return fmt.Errorf("cannot read directory %s: %w", dst, err)
}
for _, entry := range entries {
@ -129,13 +134,13 @@ func CopyDir(src, dst string) error {
if entry.IsDir() {
if err = CopyDir(srcPath, dstPath); err != nil {
return errors.Wrap(err, "copying directory failed")
return fmt.Errorf("copying directory failed: %w", err)
}
} else {
// This will include symlinks, which is what we want when
// copying things.
if err = copyFile(srcPath, dstPath); err != nil {
return errors.Wrap(err, "copying file failed")
return fmt.Errorf("copying file failed: %w", err)
}
}
}
@ -149,7 +154,7 @@ func CopyDir(src, dst string) error {
// of the source file. The file mode will be copied from the source.
func copyFile(src, dst string) (err error) {
if sym, err := IsSymlink(src); err != nil {
return errors.Wrap(err, "symlink check failed")
return fmt.Errorf("symlink check failed: %w", err)
} else if sym {
if err := cloneSymlink(src, dst); err != nil {
if runtime.GOOS == "windows" {
@ -172,28 +177,28 @@ func copyFile(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return
return err
}
if _, err = io.Copy(out, in); err != nil {
out.Close()
return
return err
}
// Check for write errors on Close
if err = out.Close(); err != nil {
return
return err
}
si, err := os.Stat(src)
if err != nil {
return
return err
}
// Temporary fix for Go < 1.9
@ -205,7 +210,7 @@ func copyFile(src, dst string) (err error) {
}
err = os.Chmod(dst, si.Mode())
return
return err
}
// cloneSymlink will create a new symlink that points to the resolved path of sl.
@ -226,7 +231,7 @@ func IsDir(name string) (bool, error) {
return false, err
}
if !fi.IsDir() {
return false, errors.Errorf("%q is not a directory", name)
return false, fmt.Errorf("%q is not a directory", name)
}
return true, nil
}

@ -33,17 +33,11 @@ package fs
import (
"os"
"os/exec"
"path/filepath"
"runtime"
"sync"
"testing"
)
var (
mu sync.Mutex
)
func TestRenameWithFallback(t *testing.T) {
dir := t.TempDir()
@ -360,19 +354,6 @@ func TestCopyFile(t *testing.T) {
}
}
func cleanUpDir(dir string) {
// NOTE(mattn): It seems that sometimes git.exe is not dead
// when cleanUpDir() is called. But we do not know any way to wait for it.
if runtime.GOOS == "windows" {
mu.Lock()
exec.Command(`taskkill`, `/F`, `/IM`, `git.exe`).Run()
mu.Unlock()
}
if dir != "" {
os.RemoveAll(dir)
}
}
func TestCopyFileSymlink(t *testing.T) {
tempdir := t.TempDir()
@ -476,6 +457,7 @@ func TestCopyFileFail(t *testing.T) {
// files this function creates. It is the caller's responsibility to call
// this function before the test is done running, whether there's an error or not.
func setupInaccessibleDir(t *testing.T, op func(dir string) error) func() {
t.Helper()
dir := t.TempDir()
subdir := filepath.Join(dir, "dir")

@ -34,10 +34,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package fs
import (
"fmt"
"os"
"syscall"
"github.com/pkg/errors"
)
// renameFallback attempts to determine the appropriate fallback to failed rename
@ -51,7 +50,7 @@ func renameFallback(err error, src, dst string) error {
if !ok {
return err
} else if terr.Err != syscall.EXDEV {
return errors.Wrapf(terr, "link error: cannot rename %s to %s", src, dst)
return fmt.Errorf("link error: cannot rename %s to %s: %w", src, dst, terr)
}
return renameByCopy(src, dst)

@ -34,10 +34,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package fs
import (
"fmt"
"os"
"syscall"
"github.com/pkg/errors"
)
// renameFallback attempts to determine the appropriate fallback to failed rename
@ -61,7 +60,7 @@ func renameFallback(err error, src, dst string) error {
// 0x11 (ERROR_NOT_SAME_DEVICE) is the windows error.
// See https://msdn.microsoft.com/en-us/library/cc231199.aspx
if ok && noerr != 0x11 {
return errors.Wrapf(terr, "link error: cannot rename %s to %s", src, dst)
return fmt.Errorf("link error: cannot rename %s to %s: %w", src, dst, terr)
}
}

@ -30,8 +30,9 @@ const (
)
func testfile(t *testing.T, file string) (path string) {
var err error
if path, err = filepath.Abs(filepath.Join(tlsTestDir, file)); err != nil {
t.Helper()
path, err := filepath.Abs(filepath.Join(tlsTestDir, file))
if err != nil {
t.Fatalf("error getting absolute path to test file %q: %v", file, err)
}
return path

@ -18,29 +18,31 @@ package action
import (
"bytes"
"errors"
"fmt"
"io"
"log/slog"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"sync"
"text/template"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/discovery"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"helm.sh/helm/v4/pkg/chart"
chartutil "helm.sh/helm/v4/pkg/chart/util"
chart "helm.sh/helm/v4/pkg/chart/v2"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/engine"
"helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/postrender"
"helm.sh/helm/v4/pkg/registry"
"helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/releaseutil"
releaseutil "helm.sh/helm/v4/pkg/release/util"
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/storage"
"helm.sh/helm/v4/pkg/storage/driver"
"helm.sh/helm/v4/pkg/time"
@ -63,21 +65,6 @@ var (
errPending = errors.New("another operation (install/upgrade/rollback) is in progress")
)
// ValidName is a regular expression for resource names.
//
// DEPRECATED: This will be removed in Helm 4, and is no longer used here. See
// pkg/lint/rules.validateMetadataNameFunc for the replacement.
//
// According to the Kubernetes help text, the regular expression it uses is:
//
// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
//
// This follows the above regular expression (but requires a full string match, not partial).
//
// The Kubernetes documentation is here, though it is not entirely correct:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
var ValidName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
// Configuration injects the dependencies that all actions share.
type Configuration struct {
// RESTClientGetter is an interface that loads Kubernetes clients.
@ -95,10 +82,13 @@ type Configuration struct {
// Capabilities describes the capabilities of the Kubernetes cluster.
Capabilities *chartutil.Capabilities
Log func(string, ...interface{})
// CustomTemplateFuncs is defined by users to provide custom template funcs
CustomTemplateFuncs template.FuncMap
// HookOutputFunc called with container name and returns and expects writer that will receive the log output.
HookOutputFunc func(namespace, pod, container string) io.Writer
mutex sync.Mutex
}
// renderResources renders the templates in a chart
@ -118,7 +108,7 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu
if ch.Metadata.KubeVersion != "" {
if !chartutil.IsCompatibleRange(ch.Metadata.KubeVersion, caps.KubeVersion.String()) {
return hs, b, "", errors.Errorf("chart requires kubeVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.String())
return hs, b, "", fmt.Errorf("chart requires kubeVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.String())
}
}
@ -135,10 +125,14 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu
}
e := engine.New(restConfig)
e.EnableDNS = enableDNS
e.CustomTemplateFuncs = cfg.CustomTemplateFuncs
files, err2 = e.Render(ch, values)
} else {
var e engine.Engine
e.EnableDNS = enableDNS
e.CustomTemplateFuncs = cfg.CustomTemplateFuncs
files, err2 = e.Render(ch, values)
}
@ -229,7 +223,7 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu
if pr != nil {
b, err = pr.Run(b)
if err != nil {
return hs, b, notes, errors.Wrap(err, "error while running post render on files")
return hs, b, notes, fmt.Errorf("error while running post render on files: %w", err)
}
}
@ -243,9 +237,6 @@ type RESTClientGetter interface {
ToRESTMapper() (meta.RESTMapper, error)
}
// DebugLog sets the logger that writes debug strings
type DebugLog func(format string, v ...interface{})
// capabilities builds a Capabilities from discovery information.
func (cfg *Configuration) getCapabilities() (*chartutil.Capabilities, error) {
if cfg.Capabilities != nil {
@ -253,13 +244,13 @@ func (cfg *Configuration) getCapabilities() (*chartutil.Capabilities, error) {
}
dc, err := cfg.RESTClientGetter.ToDiscoveryClient()
if err != nil {
return nil, errors.Wrap(err, "could not get Kubernetes discovery client")
return nil, fmt.Errorf("could not get Kubernetes discovery client: %w", err)
}
// force a discovery cache invalidation to always fetch the latest server version/capabilities.
dc.Invalidate()
kubeVersion, err := dc.ServerVersion()
if err != nil {
return nil, errors.Wrap(err, "could not get server version from Kubernetes")
return nil, fmt.Errorf("could not get server version from Kubernetes: %w", err)
}
// Issue #6361:
// Client-Go emits an error when an API service is registered but unimplemented.
@ -269,10 +260,10 @@ func (cfg *Configuration) getCapabilities() (*chartutil.Capabilities, error) {
apiVersions, err := GetVersionSet(dc)
if err != nil {
if discovery.IsGroupDiscoveryFailedError(err) {
cfg.Log("WARNING: The Kubernetes server has an orphaned API service. Server reports: %s", err)
cfg.Log("WARNING: To fix this, kubectl delete apiservice <service-name>")
slog.Warn("the kubernetes server has an orphaned API service", slog.Any("error", err))
slog.Warn("to fix this, kubectl delete apiservice <service-name>")
} else {
return nil, errors.Wrap(err, "could not get apiVersions from Kubernetes")
return nil, fmt.Errorf("could not get apiVersions from Kubernetes: %w", err)
}
}
@ -292,7 +283,7 @@ func (cfg *Configuration) getCapabilities() (*chartutil.Capabilities, error) {
func (cfg *Configuration) KubernetesClientSet() (kubernetes.Interface, error) {
conf, err := cfg.RESTClientGetter.ToRESTConfig()
if err != nil {
return nil, errors.Wrap(err, "unable to generate config for kubernetes client")
return nil, fmt.Errorf("unable to generate config for kubernetes client: %w", err)
}
return kubernetes.NewForConfig(conf)
@ -308,7 +299,7 @@ func (cfg *Configuration) Now() time.Time {
func (cfg *Configuration) releaseContent(name string, version int) (*release.Release, error) {
if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, errors.Errorf("releaseContent: Release name is invalid: %s", name)
return nil, fmt.Errorf("releaseContent: Release name is invalid: %s", name)
}
if version <= 0 {
@ -322,7 +313,7 @@ func (cfg *Configuration) releaseContent(name string, version int) (*release.Rel
func GetVersionSet(client discovery.ServerResourcesInterface) (chartutil.VersionSet, error) {
groups, resources, err := client.ServerGroupsAndResources()
if err != nil && !discovery.IsGroupDiscoveryFailedError(err) {
return chartutil.DefaultVersionSet, errors.Wrap(err, "could not get apiVersions from Kubernetes")
return chartutil.DefaultVersionSet, fmt.Errorf("could not get apiVersions from Kubernetes: %w", err)
}
// FIXME: The Kubernetes test fixture for cli appears to always return nil
@ -369,14 +360,13 @@ func GetVersionSet(client discovery.ServerResourcesInterface) (chartutil.Version
// recordRelease with an update operation in case reuse has been set.
func (cfg *Configuration) recordRelease(r *release.Release) {
if err := cfg.Releases.Update(r); err != nil {
cfg.Log("warning: Failed to update release %s: %s", r.Name, err)
slog.Warn("failed to update release", "name", r.Name, "revision", r.Version, slog.Any("error", err))
}
}
// Init initializes the action configuration
func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace, helmDriver string, log DebugLog) error {
func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace, helmDriver string) error {
kc := kube.New(getter)
kc.Log = log
lazyClient := &lazyClient{
namespace: namespace,
@ -387,11 +377,9 @@ func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namesp
switch helmDriver {
case "secret", "secrets", "":
d := driver.NewSecrets(newSecretClient(lazyClient))
d.Log = log
store = storage.Init(d)
case "configmap", "configmaps":
d := driver.NewConfigMaps(newConfigMapClient(lazyClient))
d.Log = log
store = storage.Init(d)
case "memory":
var d *driver.Memory
@ -411,21 +399,19 @@ func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namesp
case "sql":
d, err := driver.NewSQL(
os.Getenv("HELM_DRIVER_SQL_CONNECTION_STRING"),
log,
namespace,
)
if err != nil {
return errors.Wrap(err, "unable to instantiate SQL driver")
return fmt.Errorf("unable to instantiate SQL driver: %w", err)
}
store = storage.Init(d)
default:
return errors.Errorf("unknown driver %q", helmDriver)
return fmt.Errorf("unknown driver %q", helmDriver)
}
cfg.RESTClientGetter = getter
cfg.KubeClient = kc
cfg.Releases = store
cfg.Log = log
cfg.HookOutputFunc = func(_, _, _ string) io.Writer { return io.Discard }
return nil

@ -19,25 +19,38 @@ import (
"flag"
"fmt"
"io"
"log/slog"
"testing"
"github.com/stretchr/testify/assert"
fakeclientset "k8s.io/client-go/kubernetes/fake"
"helm.sh/helm/v4/pkg/chart"
chartutil "helm.sh/helm/v4/pkg/chart/util"
"helm.sh/helm/v4/internal/logging"
chart "helm.sh/helm/v4/pkg/chart/v2"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/kube"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/registry"
"helm.sh/helm/v4/pkg/release"
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/storage"
"helm.sh/helm/v4/pkg/storage/driver"
"helm.sh/helm/v4/pkg/time"
)
var verbose = flag.Bool("test.log", false, "enable test logging")
var verbose = flag.Bool("test.log", false, "enable test logging (debug by default)")
func actionConfigFixture(t *testing.T) *Configuration {
t.Helper()
return actionConfigFixtureWithDummyResources(t, nil)
}
func actionConfigFixtureWithDummyResources(t *testing.T, dummyResources kube.ResourceList) *Configuration {
t.Helper()
logger := logging.NewLogger(func() bool {
return *verbose
})
slog.SetDefault(logger)
registryClient, err := registry.NewClient()
if err != nil {
@ -46,15 +59,9 @@ func actionConfigFixture(t *testing.T) *Configuration {
return &Configuration{
Releases: storage.Init(driver.NewMemory()),
KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}},
KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: dummyResources},
Capabilities: chartutil.DefaultCapabilities,
RegistryClient: registryClient,
Log: func(format string, v ...interface{}) {
t.Helper()
if *verbose {
t.Logf(format, v...)
}
},
}
}
@ -334,7 +341,7 @@ func TestConfiguration_Init(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
cfg := &Configuration{}
actualErr := cfg.Init(nil, "default", tt.helmDriver, nil)
actualErr := cfg.Init(nil, "default", tt.helmDriver)
if tt.expectErr {
assert.Error(t, actualErr)
assert.Contains(t, actualErr.Error(), tt.errMsg)
@ -347,7 +354,7 @@ func TestConfiguration_Init(t *testing.T) {
}
func TestGetVersionSet(t *testing.T) {
client := fakeclientset.NewSimpleClientset()
client := fakeclientset.NewClientset()
vs, err := GetVersionSet(client.Discovery())
if err != nil {

@ -26,8 +26,8 @@ import (
"github.com/Masterminds/semver/v3"
"github.com/gosuri/uitable"
"helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v4/pkg/chart/loader"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/chart/v2/loader"
)
// Dependency is the action for building a given chart's dependency tree.

@ -25,8 +25,8 @@ import (
"github.com/stretchr/testify/assert"
"helm.sh/helm/v4/internal/test"
"helm.sh/helm/v4/pkg/chart"
chartutil "helm.sh/helm/v4/pkg/chart/util"
chart "helm.sh/helm/v4/pkg/chart/v2"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
)
func TestList(t *testing.T) {

@ -17,7 +17,7 @@ limitations under the License.
package action
import (
"helm.sh/helm/v4/pkg/release"
release "helm.sh/helm/v4/pkg/release/v1"
)
// Get is the action for checking a given release's information.

@ -21,7 +21,7 @@ import (
"strings"
"time"
"helm.sh/helm/v4/pkg/chart"
chart "helm.sh/helm/v4/pkg/chart/v2"
)
// GetMetadata is the action for checking a given release's metadata.

@ -17,7 +17,7 @@ limitations under the License.
package action
import (
chartutil "helm.sh/helm/v4/pkg/chart/util"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
)
// GetValues is the action for checking a given release's values.

@ -17,10 +17,12 @@ limitations under the License.
package action
import (
"github.com/pkg/errors"
"log/slog"
chartutil "helm.sh/helm/v4/pkg/chart/util"
"helm.sh/helm/v4/pkg/release"
"fmt"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
release "helm.sh/helm/v4/pkg/release/v1"
)
// History is the action for checking the release's ledger.
@ -50,9 +52,9 @@ func (h *History) Run(name string) ([]*release.Release, error) {
}
if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, errors.Errorf("release name is invalid: %s", name)
return nil, fmt.Errorf("release name is invalid: %s", name)
}
h.cfg.Log("getting history for release %s", name)
slog.Debug("getting history for release", "release", name)
return h.cfg.Releases.History(name)
}

@ -25,17 +25,15 @@ import (
"helm.sh/helm/v4/pkg/kube"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"helm.sh/helm/v4/pkg/release"
release "helm.sh/helm/v4/pkg/release/v1"
helmtime "helm.sh/helm/v4/pkg/time"
)
// execHook executes all of the hooks for the given hook event.
func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, timeout time.Duration) error {
func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, waitStrategy kube.WaitStrategy, timeout time.Duration) error {
executingHooks := []*release.Hook{}
for _, h := range rl.Hooks {
@ -49,23 +47,17 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
// hooke are pre-ordered by kind, so keep order stable
sort.Stable(hookByWeight(executingHooks))
for _, h := range executingHooks {
for i, h := range executingHooks {
// Set default delete policy to before-hook-creation
if len(h.DeletePolicies) == 0 {
// TODO(jlegrone): Only apply before-hook-creation delete policy to run to completion
// resources. For all other resource types update in place if a
// resource with the same name already exists and is owned by the
// current release.
h.DeletePolicies = []release.HookDeletePolicy{release.HookBeforeHookCreation}
}
cfg.hookSetDeletePolicy(h)
if err := cfg.deleteHookByPolicy(h, release.HookBeforeHookCreation, timeout); err != nil {
if err := cfg.deleteHookByPolicy(h, release.HookBeforeHookCreation, waitStrategy, timeout); err != nil {
return err
}
resources, err := cfg.KubeClient.Build(bytes.NewBufferString(h.Manifest), true)
if err != nil {
return errors.Wrapf(err, "unable to build kubernetes object for %s hook %s", hook, h.Path)
return fmt.Errorf("unable to build kubernetes object for %s hook %s: %w", hook, h.Path, err)
}
// Record the time at which the hook was applied to the cluster
@ -84,11 +76,15 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
if _, err := cfg.KubeClient.Create(resources); err != nil {
h.LastRun.CompletedAt = helmtime.Now()
h.LastRun.Phase = release.HookPhaseFailed
return errors.Wrapf(err, "warning: Hook %s %s failed", hook, h.Path)
return fmt.Errorf("warning: Hook %s %s failed: %w", hook, h.Path, err)
}
waiter, err := cfg.KubeClient.GetWaiter(waitStrategy)
if err != nil {
return fmt.Errorf("unable to get waiter: %w", err)
}
// Watch hook resources until they have completed
err = cfg.KubeClient.WatchUntilReady(resources, timeout)
err = waiter.WatchUntilReady(resources, timeout)
// Note the time of success/failure
h.LastRun.CompletedAt = helmtime.Now()
// Mark hook as succeeded or failed
@ -101,10 +97,17 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
}
// If a hook is failed, check the annotation of the hook to determine whether the hook should be deleted
// under failed condition. If so, then clear the corresponding resource object in the hook
if errDeleting := cfg.deleteHookByPolicy(h, release.HookFailed, timeout); errDeleting != nil {
if errDeleting := cfg.deleteHookByPolicy(h, release.HookFailed, waitStrategy, timeout); errDeleting != nil {
// We log the error here as we want to propagate the hook failure upwards to the release object.
log.Printf("error deleting the hook resource on hook failure: %v", errDeleting)
}
// If a hook is failed, check the annotation of the previous successful hooks to determine whether the hooks
// should be deleted under succeeded condition.
if err := cfg.deleteHooksByPolicy(executingHooks[0:i], release.HookSucceeded, waitStrategy, timeout); err != nil {
return err
}
return err
}
h.LastRun.Phase = release.HookPhaseSucceeded
@ -118,7 +121,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
// We log here as we still want to attempt hook resource deletion even if output logging fails.
log.Printf("error outputting logs for hook failure: %v", err)
}
if err := cfg.deleteHookByPolicy(h, release.HookSucceeded, timeout); err != nil {
if err := cfg.deleteHookByPolicy(h, release.HookSucceeded, waitStrategy, timeout); err != nil {
return err
}
}
@ -139,41 +142,64 @@ func (x hookByWeight) Less(i, j int) bool {
}
// deleteHookByPolicy deletes a hook if the hook policy instructs it to
func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.HookDeletePolicy, timeout time.Duration) error {
func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.HookDeletePolicy, waitStrategy kube.WaitStrategy, timeout time.Duration) error {
// Never delete CustomResourceDefinitions; this could cause lots of
// cascading garbage collection.
if h.Kind == "CustomResourceDefinition" {
return nil
}
if hookHasDeletePolicy(h, policy) {
if cfg.hookHasDeletePolicy(h, policy) {
resources, err := cfg.KubeClient.Build(bytes.NewBufferString(h.Manifest), false)
if err != nil {
return errors.Wrapf(err, "unable to build kubernetes object for deleting hook %s", h.Path)
return fmt.Errorf("unable to build kubernetes object for deleting hook %s: %w", h.Path, err)
}
_, errs := cfg.KubeClient.Delete(resources)
if len(errs) > 0 {
return errors.New(joinErrors(errs))
return joinErrors(errs, "; ")
}
// wait for resources until they are deleted to avoid conflicts
if kubeClient, ok := cfg.KubeClient.(kube.InterfaceExt); ok {
if err := kubeClient.WaitForDelete(resources, timeout); err != nil {
return err
}
waiter, err := cfg.KubeClient.GetWaiter(waitStrategy)
if err != nil {
return err
}
if err := waiter.WaitForDelete(resources, timeout); err != nil {
return err
}
}
return nil
}
// deleteHooksByPolicy deletes all hooks if the hook policy instructs it to
func (cfg *Configuration) deleteHooksByPolicy(hooks []*release.Hook, policy release.HookDeletePolicy, waitStrategy kube.WaitStrategy, timeout time.Duration) error {
for _, h := range hooks {
if err := cfg.deleteHookByPolicy(h, policy, waitStrategy, timeout); err != nil {
return err
}
}
return nil
}
// hookHasDeletePolicy determines whether the defined hook deletion policy matches the hook deletion polices
// supported by helm. If so, mark the hook as one should be deleted.
func hookHasDeletePolicy(h *release.Hook, policy release.HookDeletePolicy) bool {
for _, v := range h.DeletePolicies {
if policy == v {
return true
}
func (cfg *Configuration) hookHasDeletePolicy(h *release.Hook, policy release.HookDeletePolicy) bool {
cfg.mutex.Lock()
defer cfg.mutex.Unlock()
return slices.Contains(h.DeletePolicies, policy)
}
// hookSetDeletePolicy determines whether the defined hook deletion policy matches the hook deletion polices
// supported by helm. If so, mark the hook as one should be deleted.
func (cfg *Configuration) hookSetDeletePolicy(h *release.Hook) {
cfg.mutex.Lock()
defer cfg.mutex.Unlock()
if len(h.DeletePolicies) == 0 {
// TODO(jlegrone): Only apply before-hook-creation delete policy to run to completion
// resources. For all other resource types update in place if a
// resource with the same name already exists and is owned by the
// current release.
h.DeletePolicies = []release.HookDeletePolicy{release.HookBeforeHookCreation}
}
return false
}
// outputLogsByPolicy outputs a pods logs if the hook policy instructs it to
@ -216,7 +242,7 @@ func (cfg *Configuration) deriveNamespace(h *release.Hook, namespace string) (st
}{}
err := yaml.Unmarshal([]byte(h.Manifest), &tmp)
if err != nil {
return "", errors.Wrapf(err, "unable to parse metadata.namespace from kubernetes manifest for output logs hook %s", h.Path)
return "", fmt.Errorf("unable to parse metadata.namespace from kubernetes manifest for output logs hook %s: %w", h.Path, err)
}
if tmp.Metadata.Namespace == "" {
return namespace, nil

@ -20,13 +20,22 @@ import (
"bytes"
"fmt"
"io"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/cli-runtime/pkg/resource"
"helm.sh/helm/v4/pkg/chart"
chart "helm.sh/helm/v4/pkg/chart/v2"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/kube"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/release"
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/storage"
"helm.sh/helm/v4/pkg/storage/driver"
)
func podManifestWithOutputLogs(hookDefinitions []release.HookOutputLogPolicy) string {
@ -158,6 +167,7 @@ func TestInstallRelease_HooksOutputLogsOnSuccessAndFailure(t *testing.T) {
}
func runInstallForHooksWithSuccess(t *testing.T, manifest, expectedNamespace string, shouldOutput bool) {
t.Helper()
var expectedOutput string
if shouldOutput {
expectedOutput = fmt.Sprintf("attempted to output logs for namespace: %s", expectedNamespace)
@ -181,6 +191,7 @@ func runInstallForHooksWithSuccess(t *testing.T, manifest, expectedNamespace str
}
func runInstallForHooksWithFailure(t *testing.T, manifest, expectedNamespace string, shouldOutput bool) {
t.Helper()
var expectedOutput string
if shouldOutput {
expectedOutput = fmt.Sprintf("attempted to output logs for namespace: %s", expectedNamespace)
@ -206,3 +217,187 @@ func runInstallForHooksWithFailure(t *testing.T, manifest, expectedNamespace str
is.Equal(expectedOutput, outBuffer.String())
is.Equal(release.StatusFailed, res.Info.Status)
}
type HookFailedError struct{}
func (e *HookFailedError) Error() string {
return "Hook failed!"
}
type HookFailingKubeClient struct {
kubefake.PrintingKubeClient
failOn resource.Info
deleteRecord []resource.Info
}
type HookFailingKubeWaiter struct {
*kubefake.PrintingKubeWaiter
failOn resource.Info
}
func (*HookFailingKubeClient) Build(reader io.Reader, _ bool) (kube.ResourceList, error) {
configMap := &v1.ConfigMap{}
err := yaml.NewYAMLOrJSONDecoder(reader, 1000).Decode(configMap)
if err != nil {
return kube.ResourceList{}, err
}
return kube.ResourceList{{
Name: configMap.Name,
Namespace: configMap.Namespace,
}}, nil
}
func (h *HookFailingKubeWaiter) WatchUntilReady(resources kube.ResourceList, _ time.Duration) error {
for _, res := range resources {
if res.Name == h.failOn.Name && res.Namespace == h.failOn.Namespace {
return &HookFailedError{}
}
}
return nil
}
func (h *HookFailingKubeClient) Delete(resources kube.ResourceList) (*kube.Result, []error) {
for _, res := range resources {
h.deleteRecord = append(h.deleteRecord, resource.Info{
Name: res.Name,
Namespace: res.Namespace,
})
}
return h.PrintingKubeClient.Delete(resources)
}
func (h *HookFailingKubeClient) GetWaiter(strategy kube.WaitStrategy) (kube.Waiter, error) {
waiter, _ := h.PrintingKubeClient.GetWaiter(strategy)
return &HookFailingKubeWaiter{
PrintingKubeWaiter: waiter.(*kubefake.PrintingKubeWaiter),
failOn: h.failOn,
}, nil
}
func TestHooksCleanUp(t *testing.T) {
hookEvent := release.HookPreInstall
testCases := []struct {
name string
inputRelease release.Release
failOn resource.Info
expectedDeleteRecord []resource.Info
expectError bool
}{
{
"Deletion hook runs for previously successful hook on failure of a heavier weight hook",
release.Release{
Name: "test-release",
Namespace: "test",
Hooks: []*release.Hook{
{
Name: "hook-1",
Kind: "ConfigMap",
Path: "templates/service_account.yaml",
Manifest: `apiVersion: v1
kind: ConfigMap
metadata:
name: build-config-1
namespace: test
data:
foo: bar
`,
Weight: -5,
Events: []release.HookEvent{
hookEvent,
},
DeletePolicies: []release.HookDeletePolicy{
release.HookBeforeHookCreation,
release.HookSucceeded,
release.HookFailed,
},
LastRun: release.HookExecution{
Phase: release.HookPhaseSucceeded,
},
},
{
Name: "hook-2",
Kind: "ConfigMap",
Path: "templates/job.yaml",
Manifest: `apiVersion: v1
kind: ConfigMap
metadata:
name: build-config-2
namespace: test
data:
foo: bar
`,
Weight: 0,
Events: []release.HookEvent{
hookEvent,
},
DeletePolicies: []release.HookDeletePolicy{
release.HookBeforeHookCreation,
release.HookSucceeded,
release.HookFailed,
},
LastRun: release.HookExecution{
Phase: release.HookPhaseFailed,
},
},
},
}, resource.Info{
Name: "build-config-2",
Namespace: "test",
}, []resource.Info{
{
// This should be in the record for `before-hook-creation`
Name: "build-config-1",
Namespace: "test",
},
{
// This should be in the record for `before-hook-creation`
Name: "build-config-2",
Namespace: "test",
},
{
// This should be in the record for cleaning up (the failure first)
Name: "build-config-2",
Namespace: "test",
},
{
// This should be in the record for cleaning up (then the previously successful)
Name: "build-config-1",
Namespace: "test",
},
}, true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
kubeClient := &HookFailingKubeClient{
kubefake.PrintingKubeClient{Out: io.Discard}, tc.failOn, []resource.Info{},
}
configuration := &Configuration{
Releases: storage.Init(driver.NewMemory()),
KubeClient: kubeClient,
Capabilities: chartutil.DefaultCapabilities,
}
err := configuration.execHook(&tc.inputRelease, hookEvent, kube.StatusWatcherStrategy, 600)
if !reflect.DeepEqual(kubeClient.deleteRecord, tc.expectedDeleteRecord) {
t.Fatalf("Got unexpected delete record, expected: %#v, but got: %#v", kubeClient.deleteRecord, tc.expectedDeleteRecord)
}
if err != nil && !tc.expectError {
t.Fatalf("Got an unexpected error.")
}
if err == nil && tc.expectError {
t.Fatalf("Expected and error but did not get it.")
}
})
}
}

@ -19,8 +19,11 @@ package action
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/fs"
"log/slog"
"net/url"
"os"
"path"
@ -31,7 +34,6 @@ import (
"time"
"github.com/Masterminds/sprig/v3"
"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
@ -39,8 +41,8 @@ import (
"k8s.io/cli-runtime/pkg/resource"
"sigs.k8s.io/yaml"
"helm.sh/helm/v4/pkg/chart"
chartutil "helm.sh/helm/v4/pkg/chart/util"
chart "helm.sh/helm/v4/pkg/chart/v2"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/cli"
"helm.sh/helm/v4/pkg/downloader"
"helm.sh/helm/v4/pkg/getter"
@ -48,8 +50,8 @@ import (
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/postrender"
"helm.sh/helm/v4/pkg/registry"
"helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/releaseutil"
releaseutil "helm.sh/helm/v4/pkg/release/util"
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/repo"
"helm.sh/helm/v4/pkg/storage"
"helm.sh/helm/v4/pkg/storage/driver"
@ -79,7 +81,7 @@ type Install struct {
HideSecret bool
DisableHooks bool
Replace bool
Wait bool
WaitStrategy kube.WaitStrategy
WaitForJobs bool
Devel bool
DependencyUpdate bool
@ -142,19 +144,19 @@ func NewInstall(cfg *Configuration) *Install {
in := &Install{
cfg: cfg,
}
in.ChartPathOptions.registryClient = cfg.RegistryClient
in.registryClient = cfg.RegistryClient
return in
}
// SetRegistryClient sets the registry client for the install action
func (i *Install) SetRegistryClient(registryClient *registry.Client) {
i.ChartPathOptions.registryClient = registryClient
i.registryClient = registryClient
}
// GetRegistryClient get the registry client.
func (i *Install) GetRegistryClient() *registry.Client {
return i.ChartPathOptions.registryClient
return i.registryClient
}
func (i *Install) installCRDs(crds []chart.CRD) error {
@ -164,7 +166,7 @@ func (i *Install) installCRDs(crds []chart.CRD) error {
// Read in the resources
res, err := i.cfg.KubeClient.Build(bytes.NewBuffer(obj.File.Data), false)
if err != nil {
return errors.Wrapf(err, "failed to install CRD %s", obj.Name)
return fmt.Errorf("failed to install CRD %s: %w", obj.Name, err)
}
// Send them to Kube
@ -172,16 +174,20 @@ func (i *Install) installCRDs(crds []chart.CRD) error {
// If the error is CRD already exists, continue.
if apierrors.IsAlreadyExists(err) {
crdName := res[0].Name
i.cfg.Log("CRD %s is already present. Skipping.", crdName)
slog.Debug("CRD is already present. Skipping", "crd", crdName)
continue
}
return errors.Wrapf(err, "failed to install CRD %s", obj.Name)
return fmt.Errorf("failed to install CRD %s: %w", obj.Name, err)
}
totalItems = append(totalItems, res...)
}
if len(totalItems) > 0 {
waiter, err := i.cfg.KubeClient.GetWaiter(i.WaitStrategy)
if err != nil {
return fmt.Errorf("unable to get waiter: %w", err)
}
// Give time for the CRD to be recognized.
if err := i.cfg.KubeClient.Wait(totalItems, 60*time.Second); err != nil {
if err := waiter.Wait(totalItems, 60*time.Second); err != nil {
return err
}
@ -196,7 +202,7 @@ func (i *Install) installCRDs(crds []chart.CRD) error {
return err
}
i.cfg.Log("Clearing discovery cache")
slog.Debug("clearing discovery cache")
discoveryClient.Invalidate()
_, _ = discoveryClient.ServerGroups()
@ -209,7 +215,7 @@ func (i *Install) installCRDs(crds []chart.CRD) error {
return err
}
if resettable, ok := restMapper.(meta.ResettableRESTMapper); ok {
i.cfg.Log("Clearing REST mapper cache")
slog.Debug("clearing REST mapper cache")
resettable.Reset()
}
}
@ -233,25 +239,25 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
// Check reachability of cluster unless in client-only mode (e.g. `helm template` without `--validate`)
if !i.ClientOnly {
if err := i.cfg.KubeClient.IsReachable(); err != nil {
i.cfg.Log(fmt.Sprintf("ERROR: Cluster reachability check failed: %v", err))
return nil, errors.Wrap(err, "cluster reachability check failed")
slog.Error(fmt.Sprintf("cluster reachability check failed: %v", err))
return nil, fmt.Errorf("cluster reachability check failed: %w", err)
}
}
// HideSecret must be used with dry run. Otherwise, return an error.
if !i.isDryRun() && i.HideSecret {
i.cfg.Log("ERROR: Hiding Kubernetes secrets requires a dry-run mode")
return nil, errors.New("Hiding Kubernetes secrets requires a dry-run mode")
slog.Error("hiding Kubernetes secrets requires a dry-run mode")
return nil, errors.New("hiding Kubernetes secrets requires a dry-run mode")
}
if err := i.availableName(); err != nil {
i.cfg.Log(fmt.Sprintf("ERROR: Release name check failed: %v", err))
return nil, errors.Wrap(err, "release name check failed")
slog.Error("release name check failed", slog.Any("error", err))
return nil, fmt.Errorf("release name check failed: %w", err)
}
if err := chartutil.ProcessDependencies(chrt, vals); err != nil {
i.cfg.Log(fmt.Sprintf("ERROR: Processing chart dependencies failed: %v", err))
return nil, errors.Wrap(err, "chart dependencies processing failed")
slog.Error("chart dependencies processing failed", slog.Any("error", err))
return nil, fmt.Errorf("chart dependencies processing failed: %w", err)
}
var interactWithRemote bool
@ -264,7 +270,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
if crds := chrt.CRDObjects(); !i.ClientOnly && !i.SkipCRDs && len(crds) > 0 {
// On dry run, bail here
if i.isDryRun() {
i.cfg.Log("WARNING: This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.")
slog.Warn("This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.")
} else if err := i.installCRDs(crds); err != nil {
return nil, err
}
@ -284,12 +290,14 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
mem.SetNamespace(i.Namespace)
i.cfg.Releases = storage.Init(mem)
} else if !i.ClientOnly && len(i.APIVersions) > 0 {
i.cfg.Log("API Version list given outside of client only mode, this list will be ignored")
slog.Debug("API Version list given outside of client only mode, this list will be ignored")
}
// Make sure if Atomic is set, that wait is set as well. This makes it so
// the user doesn't have to specify both
i.Wait = i.Wait || i.Atomic
if i.WaitStrategy == kube.HookOnlyStrategy && i.Atomic {
i.WaitStrategy = kube.StatusWatcherStrategy
}
caps, err := i.cfg.getCapabilities()
if err != nil {
@ -335,7 +343,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
var toBeAdopted kube.ResourceList
resources, err := i.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), !i.DisableOpenAPIValidation)
if err != nil {
return nil, errors.Wrap(err, "unable to build kubernetes objects from release manifest")
return nil, fmt.Errorf("unable to build kubernetes objects from release manifest: %w", err)
}
// It is safe to use "force" here because these are resources currently rendered by the chart.
@ -357,7 +365,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
toBeAdopted, err = existingResourceConflict(resources, rel.Name, rel.Namespace)
}
if err != nil {
return nil, errors.Wrap(err, "Unable to continue with install")
return nil, fmt.Errorf("unable to continue with install: %w", err)
}
}
@ -448,7 +456,7 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource
var err error
// pre-install hooks
if !i.DisableHooks {
if err := i.cfg.execHook(rel, release.HookPreInstall, i.Timeout); err != nil {
if err := i.cfg.execHook(rel, release.HookPreInstall, i.WaitStrategy, i.Timeout); err != nil {
return rel, fmt.Errorf("failed pre-install: %s", err)
}
}
@ -459,25 +467,32 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource
if len(toBeAdopted) == 0 && len(resources) > 0 {
_, err = i.cfg.KubeClient.Create(resources)
} else if len(resources) > 0 {
_, err = i.cfg.KubeClient.Update(toBeAdopted, resources, i.Force)
if i.TakeOwnership {
_, err = i.cfg.KubeClient.(kube.InterfaceThreeWayMerge).UpdateThreeWayMerge(toBeAdopted, resources, i.Force)
} else {
_, err = i.cfg.KubeClient.Update(toBeAdopted, resources, i.Force)
}
}
if err != nil {
return rel, err
}
if i.Wait {
if i.WaitForJobs {
err = i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout)
} else {
err = i.cfg.KubeClient.Wait(resources, i.Timeout)
}
if err != nil {
return rel, err
}
waiter, err := i.cfg.KubeClient.GetWaiter(i.WaitStrategy)
if err != nil {
return rel, fmt.Errorf("failed to get waiter: %w", err)
}
if i.WaitForJobs {
err = waiter.WaitWithJobs(resources, i.Timeout)
} else {
err = waiter.Wait(resources, i.Timeout)
}
if err != nil {
return rel, err
}
if !i.DisableHooks {
if err := i.cfg.execHook(rel, release.HookPostInstall, i.Timeout); err != nil {
if err := i.cfg.execHook(rel, release.HookPostInstall, i.WaitStrategy, i.Timeout); err != nil {
return rel, fmt.Errorf("failed post-install: %s", err)
}
}
@ -496,7 +511,7 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource
// One possible strategy would be to do a timed retry to see if we can get
// this stored in the future.
if err := i.recordRelease(rel); err != nil {
i.cfg.Log("failed to record the release: %s", err)
slog.Error("failed to record the release", slog.Any("error", err))
}
return rel, nil
@ -505,15 +520,15 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource
func (i *Install) failRelease(rel *release.Release, err error) (*release.Release, error) {
rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error()))
if i.Atomic {
i.cfg.Log("Install failed and atomic is set, uninstalling release")
slog.Debug("install failed, uninstalling release", "release", i.ReleaseName)
uninstall := NewUninstall(i.cfg)
uninstall.DisableHooks = i.DisableHooks
uninstall.KeepHistory = false
uninstall.Timeout = i.Timeout
if _, uninstallErr := uninstall.Run(i.ReleaseName); uninstallErr != nil {
return rel, errors.Wrapf(uninstallErr, "an error occurred while uninstalling the release. original install error: %s", err)
return rel, fmt.Errorf("an error occurred while uninstalling the release. original install error: %w: %w", err, uninstallErr)
}
return rel, errors.Wrapf(err, "release %s failed, and has been uninstalled due to atomic being set", i.ReleaseName)
return rel, fmt.Errorf("release %s failed, and has been uninstalled due to atomic being set: %w", i.ReleaseName, err)
}
i.recordRelease(rel) // Ignore the error, since we have another error to deal with.
return rel, err
@ -531,7 +546,7 @@ func (i *Install) availableName() error {
start := i.ReleaseName
if err := chartutil.ValidateReleaseName(start); err != nil {
return errors.Wrapf(err, "release name %q", start)
return fmt.Errorf("release name %q: %w", start, err)
}
// On dry run, bail here
if i.isDryRun() {
@ -618,7 +633,7 @@ func writeToFile(outputDir string, name string, data string, appendData bool) er
defer f.Close()
_, err = f.WriteString(fmt.Sprintf("---\n# Source: %s\n%s\n", name, data))
_, err = fmt.Fprintf(f, "---\n# Source: %s\n%s\n", name, data)
if err != nil {
return err
@ -639,7 +654,7 @@ func createOrOpenFile(filename string, appendData bool) (*os.File, error) {
func ensureDirectoryForFile(file string) error {
baseDir := path.Dir(file)
_, err := os.Stat(baseDir)
if err != nil && !os.IsNotExist(err) {
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
}
@ -661,7 +676,7 @@ func (i *Install) NameAndChart(args []string) (string, string, error) {
}
if len(args) > 2 {
return args[0], args[1], errors.Errorf("expected at most two arguments, unexpected arguments: %v", strings.Join(args[2:], ", "))
return args[0], args[1], fmt.Errorf("expected at most two arguments, unexpected arguments: %v", strings.Join(args[2:], ", "))
}
if len(args) == 2 {
@ -726,7 +741,7 @@ OUTER:
}
if len(missing) > 0 {
return errors.Errorf("found in Chart.yaml, but missing in charts/ directory: %s", strings.Join(missing, ", "))
return fmt.Errorf("found in Chart.yaml, but missing in charts/ directory: %s", strings.Join(missing, ", "))
}
return nil
}
@ -762,7 +777,7 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (
return abs, nil
}
if filepath.IsAbs(name) || strings.HasPrefix(name, ".") {
return name, errors.Errorf("path %q not found", name)
return name, fmt.Errorf("path %q not found", name)
}
dl := downloader.ChartDownloader{

@ -19,8 +19,11 @@ package action
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"regexp"
@ -31,12 +34,21 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kuberuntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest/fake"
"helm.sh/helm/v4/internal/test"
"helm.sh/helm/v4/pkg/chart"
chartutil "helm.sh/helm/v4/pkg/chart/util"
chart "helm.sh/helm/v4/pkg/chart/v2"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/kube"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/release"
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/storage/driver"
helmtime "helm.sh/helm/v4/pkg/time"
)
@ -47,7 +59,64 @@ type nameTemplateTestCase struct {
expectedErrorStr string
}
func createDummyResourceList(owned bool) kube.ResourceList {
obj := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "dummyName",
Namespace: "spaced",
},
}
if owned {
obj.Labels = map[string]string{
"app.kubernetes.io/managed-by": "Helm",
}
obj.Annotations = map[string]string{
"meta.helm.sh/release-name": "test-install-release",
"meta.helm.sh/release-namespace": "spaced",
}
}
resInfo := resource.Info{
Name: "dummyName",
Namespace: "spaced",
Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployment"},
GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
Scope: meta.RESTScopeNamespace,
},
Object: obj,
}
body := io.NopCloser(bytes.NewReader([]byte(kuberuntime.EncodeOrDie(appsv1Codec, obj))))
resInfo.Client = &fake.RESTClient{
GroupVersion: schema.GroupVersion{Group: "apps", Version: "v1"},
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
Client: fake.CreateHTTPClient(func(_ *http.Request) (*http.Response, error) {
header := http.Header{}
header.Set("Content-Type", kuberuntime.ContentTypeJSON)
return &http.Response{
StatusCode: http.StatusOK,
Header: header,
Body: body,
}, nil
}),
}
var resourceList kube.ResourceList
resourceList.Append(&resInfo)
return resourceList
}
func installActionWithConfig(config *Configuration) *Install {
instAction := NewInstall(config)
instAction.Namespace = "spaced"
instAction.ReleaseName = "test-install-release"
return instAction
}
func installAction(t *testing.T) *Install {
t.Helper()
config := actionConfigFixture(t)
instAction := NewInstall(config)
instAction.Namespace = "spaced"
@ -62,7 +131,7 @@ func TestInstallRelease(t *testing.T) {
instAction := installAction(t)
vals := map[string]interface{}{}
ctx, done := context.WithCancel(context.Background())
ctx, done := context.WithCancel(t.Context())
res, err := instAction.RunWithContext(ctx, buildChart(), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
@ -92,6 +161,61 @@ func TestInstallRelease(t *testing.T) {
is.Equal(lastRelease.Info.Status, release.StatusDeployed)
}
func TestInstallReleaseWithTakeOwnership_ResourceNotOwned(t *testing.T) {
// This test will test checking ownership of a resource
// returned by the fake client. If the resource is not
// owned by the chart, ownership is taken.
// To verify ownership has been taken, the fake client
// needs to store state which is a bigger rewrite.
// TODO: Ensure fake kube client stores state. Maybe using
// "k8s.io/client-go/kubernetes/fake" could be sufficient? i.e
// "Client{Namespace: namespace, kubeClient: k8sfake.NewClientset()}"
is := assert.New(t)
// Resource list from cluster is NOT owned by helm chart
config := actionConfigFixtureWithDummyResources(t, createDummyResourceList(false))
instAction := installActionWithConfig(config)
instAction.TakeOwnership = true
res, err := instAction.Run(buildChart(), nil)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
is.Equal(rel.Info.Description, "Install complete")
}
func TestInstallReleaseWithTakeOwnership_ResourceOwned(t *testing.T) {
is := assert.New(t)
// Resource list from cluster is owned by helm chart
config := actionConfigFixtureWithDummyResources(t, createDummyResourceList(true))
instAction := installActionWithConfig(config)
instAction.TakeOwnership = false
res, err := instAction.Run(buildChart(), nil)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
is.Equal(rel.Info.Description, "Install complete")
}
func TestInstallReleaseWithTakeOwnership_ResourceOwnedNoFlag(t *testing.T) {
is := assert.New(t)
// Resource list from cluster is NOT owned by helm chart
config := actionConfigFixtureWithDummyResources(t, createDummyResourceList(false))
instAction := installActionWithConfig(config)
_, err := instAction.Run(buildChart(), nil)
is.Error(err)
is.Contains(err.Error(), "unable to continue with install")
}
func TestInstallReleaseWithValues(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
@ -323,7 +447,9 @@ func TestInstallReleaseIncorrectTemplate_DryRun(t *testing.T) {
instAction.DryRun = true
vals := map[string]interface{}{}
_, err := instAction.Run(buildChart(withSampleIncludingIncorrectTemplates()), vals)
expectedErr := "\"hello/templates/incorrect\" at <.Values.bad.doh>: nil pointer evaluating interface {}.doh"
expectedErr := `hello/templates/incorrect:1:10
executing "hello/templates/incorrect" at <.Values.bad.doh>:
nil pointer evaluating interface {}.doh`
if err == nil {
t.Fatalf("Install should fail containing error: %s", expectedErr)
}
@ -411,7 +537,7 @@ func TestInstallRelease_Wait(t *testing.T) {
failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("I timed out")
instAction.cfg.KubeClient = failer
instAction.Wait = true
instAction.WaitStrategy = kube.StatusWatcherStrategy
vals := map[string]interface{}{}
goroutines := runtime.NumGoroutine()
@ -430,10 +556,10 @@ func TestInstallRelease_Wait_Interrupted(t *testing.T) {
failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitDuration = 10 * time.Second
instAction.cfg.KubeClient = failer
instAction.Wait = true
instAction.WaitStrategy = kube.StatusWatcherStrategy
vals := map[string]interface{}{}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
time.AfterFunc(time.Second, cancel)
goroutines := runtime.NumGoroutine()
@ -453,7 +579,7 @@ func TestInstallRelease_WaitForJobs(t *testing.T) {
failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("I timed out")
instAction.cfg.KubeClient = failer
instAction.Wait = true
instAction.WaitStrategy = kube.StatusWatcherStrategy
instAction.WaitForJobs = true
vals := map[string]interface{}{}
@ -517,9 +643,11 @@ func TestInstallRelease_Atomic_Interrupted(t *testing.T) {
instAction.Atomic = true
vals := map[string]interface{}{}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
time.AfterFunc(time.Second, cancel)
goroutines := runtime.NumGoroutine()
res, err := instAction.RunWithContext(ctx, buildChart(), vals)
is.Error(err)
is.Contains(err.Error(), "context canceled")
@ -530,6 +658,9 @@ func TestInstallRelease_Atomic_Interrupted(t *testing.T) {
_, err = instAction.cfg.Releases.Get(res.Name, res.Version)
is.Error(err)
is.Equal(err, driver.ErrReleaseNotFound)
is.Equal(goroutines+1, runtime.NumGoroutine()) // installation goroutine still is in background
time.Sleep(10 * time.Second) // wait for goroutine to finish
is.Equal(goroutines, runtime.NumGoroutine())
}
func TestNameTemplate(t *testing.T) {
@ -630,7 +761,7 @@ func TestInstallReleaseOutputDir(t *testing.T) {
test.AssertGoldenFile(t, filepath.Join(dir, "hello/templates/rbac"), "rbac.txt")
_, err = os.Stat(filepath.Join(dir, "hello/templates/empty"))
is.True(os.IsNotExist(err))
is.True(errors.Is(err, fs.ErrNotExist))
}
func TestInstallOutputDirWithReleaseName(t *testing.T) {
@ -666,7 +797,7 @@ func TestInstallOutputDirWithReleaseName(t *testing.T) {
test.AssertGoldenFile(t, filepath.Join(newDir, "hello/templates/rbac"), "rbac.txt")
_, err = os.Stat(filepath.Join(newDir, "hello/templates/empty"))
is.True(os.IsNotExist(err))
is.True(errors.Is(err, fs.ErrNotExist))
}
func TestNameAndChart(t *testing.T) {

@ -17,13 +17,12 @@ limitations under the License.
package action
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
chartutil "helm.sh/helm/v4/pkg/chart/util"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/lint"
"helm.sh/helm/v4/pkg/lint/support"
)
@ -94,26 +93,26 @@ func lintChart(path string, vals map[string]interface{}, namespace string, kubeV
if strings.HasSuffix(path, ".tgz") || strings.HasSuffix(path, ".tar.gz") {
tempDir, err := os.MkdirTemp("", "helm-lint")
if err != nil {
return linter, errors.Wrap(err, "unable to create temp dir to extract tarball")
return linter, fmt.Errorf("unable to create temp dir to extract tarball: %w", err)
}
defer os.RemoveAll(tempDir)
file, err := os.Open(path)
if err != nil {
return linter, errors.Wrap(err, "unable to open tarball")
return linter, fmt.Errorf("unable to open tarball: %w", err)
}
defer file.Close()
if err = chartutil.Expand(tempDir, file); err != nil {
return linter, errors.Wrap(err, "unable to extract tarball")
return linter, fmt.Errorf("unable to extract tarball: %w", err)
}
files, err := os.ReadDir(tempDir)
if err != nil {
return linter, errors.Wrapf(err, "unable to read temporary output directory %s", tempDir)
return linter, fmt.Errorf("unable to read temporary output directory %s: %w", tempDir, err)
}
if !files[0].IsDir() {
return linter, errors.Errorf("unexpected file %s in temporary output directory %s", files[0].Name(), tempDir)
return linter, fmt.Errorf("unexpected file %s in temporary output directory %s", files[0].Name(), tempDir)
}
chartPath = filepath.Join(tempDir, files[0].Name())
@ -123,7 +122,7 @@ func lintChart(path string, vals map[string]interface{}, namespace string, kubeV
// Guard: Error out if this is not a chart.
if _, err := os.Stat(filepath.Join(chartPath, "Chart.yaml")); err != nil {
return linter, errors.Wrap(err, "unable to check Chart.yaml file in chart")
return linter, fmt.Errorf("unable to check Chart.yaml file in chart: %w", err)
}
return lint.RunAll(

@ -22,8 +22,8 @@ import (
"k8s.io/apimachinery/pkg/labels"
"helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/releaseutil"
releaseutil "helm.sh/helm/v4/pkg/release/util"
release "helm.sh/helm/v4/pkg/release/v1"
)
// ListStates represents zero or more status codes that a list item may have set

@ -21,7 +21,7 @@ import (
"github.com/stretchr/testify/assert"
"helm.sh/helm/v4/pkg/release"
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/storage"
)
@ -64,13 +64,14 @@ func TestList_Empty(t *testing.T) {
}
func newListFixture(t *testing.T) *List {
t.Helper()
return NewList(actionConfigFixture(t))
}
func TestList_OneNamespace(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
makeMeSomeReleases(lister.cfg.Releases, t)
makeMeSomeReleases(t, lister.cfg.Releases)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 3)
@ -79,7 +80,7 @@ func TestList_OneNamespace(t *testing.T) {
func TestList_AllNamespaces(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
makeMeSomeReleases(lister.cfg.Releases, t)
makeMeSomeReleases(t, lister.cfg.Releases)
lister.AllNamespaces = true
lister.SetStateMask()
list, err := lister.Run()
@ -91,7 +92,7 @@ func TestList_Sort(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Sort = ByNameDesc // Other sorts are tested elsewhere
makeMeSomeReleases(lister.cfg.Releases, t)
makeMeSomeReleases(t, lister.cfg.Releases)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 3)
@ -104,7 +105,7 @@ func TestList_Limit(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Limit = 2
makeMeSomeReleases(lister.cfg.Releases, t)
makeMeSomeReleases(t, lister.cfg.Releases)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 2)
@ -117,7 +118,7 @@ func TestList_BigLimit(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Limit = 20
makeMeSomeReleases(lister.cfg.Releases, t)
makeMeSomeReleases(t, lister.cfg.Releases)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 3)
@ -133,7 +134,7 @@ func TestList_LimitOffset(t *testing.T) {
lister := newListFixture(t)
lister.Limit = 2
lister.Offset = 1
makeMeSomeReleases(lister.cfg.Releases, t)
makeMeSomeReleases(t, lister.cfg.Releases)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 2)
@ -148,7 +149,7 @@ func TestList_LimitOffsetOutOfBounds(t *testing.T) {
lister := newListFixture(t)
lister.Limit = 2
lister.Offset = 3 // Last item is index 2
makeMeSomeReleases(lister.cfg.Releases, t)
makeMeSomeReleases(t, lister.cfg.Releases)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 0)
@ -163,7 +164,7 @@ func TestList_LimitOffsetOutOfBounds(t *testing.T) {
func TestList_StateMask(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
makeMeSomeReleases(lister.cfg.Releases, t)
makeMeSomeReleases(t, lister.cfg.Releases)
one, err := lister.cfg.Releases.Get("one", 1)
is.NoError(err)
one.SetStatus(release.StatusUninstalled, "uninstalled")
@ -193,7 +194,7 @@ func TestList_StateMaskWithStaleRevisions(t *testing.T) {
lister := newListFixture(t)
lister.StateMask = ListFailed
makeMeSomeReleasesWithStaleFailure(lister.cfg.Releases, t)
makeMeSomeReleasesWithStaleFailure(t, lister.cfg.Releases)
res, err := lister.Run()
@ -205,7 +206,7 @@ func TestList_StateMaskWithStaleRevisions(t *testing.T) {
is.Equal("failed", res[0].Name)
}
func makeMeSomeReleasesWithStaleFailure(store *storage.Storage, t *testing.T) {
func makeMeSomeReleasesWithStaleFailure(t *testing.T, store *storage.Storage) {
t.Helper()
one := namedReleaseStub("clean", release.StatusDeployed)
one.Namespace = "default"
@ -242,7 +243,7 @@ func TestList_Filter(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Filter = "th."
makeMeSomeReleases(lister.cfg.Releases, t)
makeMeSomeReleases(t, lister.cfg.Releases)
res, err := lister.Run()
is.NoError(err)
@ -254,13 +255,13 @@ func TestList_FilterFailsCompile(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Filter = "t[h.{{{"
makeMeSomeReleases(lister.cfg.Releases, t)
makeMeSomeReleases(t, lister.cfg.Releases)
_, err := lister.Run()
is.Error(err)
}
func makeMeSomeReleases(store *storage.Storage, t *testing.T) {
func makeMeSomeReleases(t *testing.T, store *storage.Storage) {
t.Helper()
one := releaseStub()
one.Name = "one"

@ -18,16 +18,16 @@ package action
import (
"bufio"
"errors"
"fmt"
"os"
"syscall"
"github.com/Masterminds/semver/v3"
"github.com/pkg/errors"
"golang.org/x/term"
"helm.sh/helm/v4/pkg/chart/loader"
chartutil "helm.sh/helm/v4/pkg/chart/util"
"helm.sh/helm/v4/pkg/chart/v2/loader"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/provenance"
)
@ -39,6 +39,7 @@ type Package struct {
Key string
Keyring string
PassphraseFile string
cachedPassphrase []byte
Version string
AppVersion string
Destination string
@ -55,6 +56,10 @@ type Package struct {
InsecureSkipTLSverify bool
}
const (
passPhraseFileStdin = "-"
)
// NewPackage creates a new Package object with the given configuration.
func NewPackage() *Package {
return &Package{}
@ -100,7 +105,7 @@ func (p *Package) Run(path string, _ map[string]interface{}) (string, error) {
name, err := chartutil.Save(ch, dest)
if err != nil {
return "", errors.Wrap(err, "failed to save")
return "", fmt.Errorf("failed to save: %w", err)
}
if p.Sign {
@ -128,7 +133,7 @@ func (p *Package) Clearsign(filename string) error {
passphraseFetcher := promptUser
if p.PassphraseFile != "" {
passphraseFetcher, err = passphraseFileFetcher(p.PassphraseFile, os.Stdin)
passphraseFetcher, err = p.passphraseFileFetcher(p.PassphraseFile, os.Stdin)
if err != nil {
return err
}
@ -156,25 +161,42 @@ func promptUser(name string) ([]byte, error) {
return pw, err
}
func passphraseFileFetcher(passphraseFile string, stdin *os.File) (provenance.PassphraseFetcher, error) {
file, err := openPassphraseFile(passphraseFile, stdin)
if err != nil {
return nil, err
}
defer file.Close()
func (p *Package) passphraseFileFetcher(passphraseFile string, stdin *os.File) (provenance.PassphraseFetcher, error) {
// When reading from stdin we cache the passphrase here. If we are
// packaging multiple charts, we reuse the cached passphrase. This
// allows giving the passphrase once on stdin without failing with
// complaints about stdin already being closed.
//
// An alternative to this would be to omit file.Close() for stdin
// below and require the user to provide the same passphrase once
// per chart on stdin, but that does not seem very user-friendly.
if p.cachedPassphrase == nil {
file, err := openPassphraseFile(passphraseFile, stdin)
if err != nil {
return nil, err
}
defer file.Close()
reader := bufio.NewReader(file)
passphrase, _, err := reader.ReadLine()
if err != nil {
return nil, err
reader := bufio.NewReader(file)
passphrase, _, err := reader.ReadLine()
if err != nil {
return nil, err
}
p.cachedPassphrase = passphrase
return func(_ string) ([]byte, error) {
return passphrase, nil
}, nil
}
return func(_ string) ([]byte, error) {
return passphrase, nil
return p.cachedPassphrase, nil
}, nil
}
func openPassphraseFile(passphraseFile string, stdin *os.File) (*os.File, error) {
if passphraseFile == "-" {
if passphraseFile == passPhraseFileStdin {
stat, err := stdin.Stat()
if err != nil {
return nil, err

@ -29,8 +29,9 @@ import (
func TestPassphraseFileFetcher(t *testing.T) {
secret := "secret"
directory := ensure.TempFile(t, "passphrase-file", []byte(secret))
testPkg := NewPackage()
fetcher, err := passphraseFileFetcher(path.Join(directory, "passphrase-file"), nil)
fetcher, err := testPkg.passphraseFileFetcher(path.Join(directory, "passphrase-file"), nil)
if err != nil {
t.Fatal("Unable to create passphraseFileFetcher", err)
}
@ -48,8 +49,9 @@ func TestPassphraseFileFetcher(t *testing.T) {
func TestPassphraseFileFetcher_WithLineBreak(t *testing.T) {
secret := "secret"
directory := ensure.TempFile(t, "passphrase-file", []byte(secret+"\n\n."))
testPkg := NewPackage()
fetcher, err := passphraseFileFetcher(path.Join(directory, "passphrase-file"), nil)
fetcher, err := testPkg.passphraseFileFetcher(path.Join(directory, "passphrase-file"), nil)
if err != nil {
t.Fatal("Unable to create passphraseFileFetcher", err)
}
@ -66,17 +68,48 @@ func TestPassphraseFileFetcher_WithLineBreak(t *testing.T) {
func TestPassphraseFileFetcher_WithInvalidStdin(t *testing.T) {
directory := t.TempDir()
testPkg := NewPackage()
stdin, err := os.CreateTemp(directory, "non-existing")
if err != nil {
t.Fatal("Unable to create test file", err)
}
if _, err := passphraseFileFetcher("-", stdin); err == nil {
if _, err := testPkg.passphraseFileFetcher("-", stdin); err == nil {
t.Error("Expected passphraseFileFetcher returning an error")
}
}
func TestPassphraseFileFetcher_WithStdinAndMultipleFetches(t *testing.T) {
testPkg := NewPackage()
stdin, w, err := os.Pipe()
if err != nil {
t.Fatal("Unable to create pipe", err)
}
passphrase := "secret-from-stdin"
go func() {
w.Write([]byte(passphrase + "\n"))
}()
for i := 0; i < 4; i++ {
fetcher, err := testPkg.passphraseFileFetcher("-", stdin)
if err != nil {
t.Errorf("Expected passphraseFileFetcher to not return an error, but got %v", err)
}
pass, err := fetcher("key")
if err != nil {
t.Errorf("Expected passphraseFileFetcher invocation to succeed, failed with %v", err)
}
if string(pass) != string(passphrase) {
t.Errorf("Expected multiple passphrase fetch to return %q, got %q", passphrase, pass)
}
}
}
func TestValidateVersion(t *testing.T) {
type args struct {
ver string

@ -22,9 +22,7 @@ import (
"path/filepath"
"strings"
"github.com/pkg/errors"
chartutil "helm.sh/helm/v4/pkg/chart/util"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/cli"
"helm.sh/helm/v4/pkg/downloader"
"helm.sh/helm/v4/pkg/getter"
@ -111,7 +109,7 @@ func (p *Pull) Run(chartRef string) (string, error) {
var err error
dest, err = os.MkdirTemp("", "helm-")
if err != nil {
return out.String(), errors.Wrap(err, "failed to untar")
return out.String(), fmt.Errorf("failed to untar: %w", err)
}
defer os.RemoveAll(dest)
}
@ -163,11 +161,10 @@ func (p *Pull) Run(chartRef string) (string, error) {
if _, err := os.Stat(udCheck); err != nil {
if err := os.MkdirAll(udCheck, 0755); err != nil {
return out.String(), errors.Wrap(err, "failed to untar (mkdir)")
return out.String(), fmt.Errorf("failed to untar (mkdir): %w", err)
}
} else {
return out.String(), errors.Errorf("failed to untar: a file or directory with the name %s already exists", udCheck)
return out.String(), fmt.Errorf("failed to untar: a file or directory with the name %s already exists", udCheck)
}
return out.String(), chartutil.ExpandFile(ud, saved)

@ -24,11 +24,11 @@ import (
"sort"
"time"
"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
chartutil "helm.sh/helm/v4/pkg/chart/util"
"helm.sh/helm/v4/pkg/release"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/kube"
release "helm.sh/helm/v4/pkg/release/v1"
)
const (
@ -63,7 +63,7 @@ func (r *ReleaseTesting) Run(name string) (*release.Release, error) {
}
if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, errors.Errorf("releaseTest: Release name is invalid: %s", name)
return nil, fmt.Errorf("releaseTest: Release name is invalid: %s", name)
}
// finds the non-deleted release with the given name
@ -96,7 +96,7 @@ func (r *ReleaseTesting) Run(name string) (*release.Release, error) {
rel.Hooks = executingHooks
}
if err := r.cfg.execHook(rel, release.HookTest, r.Timeout); err != nil {
if err := r.cfg.execHook(rel, release.HookTest, kube.StatusWatcherStrategy, r.Timeout); err != nil {
rel.Hooks = append(skippedHooks, rel.Hooks...)
r.cfg.Releases.Update(rel)
return rel, err
@ -112,7 +112,7 @@ func (r *ReleaseTesting) Run(name string) (*release.Release, error) {
func (r *ReleaseTesting) GetPodLogs(out io.Writer, rel *release.Release) error {
client, err := r.cfg.KubernetesClientSet()
if err != nil {
return errors.Wrap(err, "unable to get kubernetes client to fetch pod logs")
return fmt.Errorf("unable to get kubernetes client to fetch pod logs: %w", err)
}
hooksByWight := append([]*release.Hook{}, rel.Hooks...)
@ -129,14 +129,14 @@ func (r *ReleaseTesting) GetPodLogs(out io.Writer, rel *release.Release) error {
req := client.CoreV1().Pods(r.Namespace).GetLogs(h.Name, &v1.PodLogOptions{})
logReader, err := req.Stream(context.Background())
if err != nil {
return errors.Wrapf(err, "unable to get pod logs for %s", h.Name)
return fmt.Errorf("unable to get pod logs for %s: %w", h.Name, err)
}
fmt.Fprintf(out, "POD LOGS: %s\n", h.Name)
_, err = io.Copy(out, logReader)
fmt.Fprintln(out)
if err != nil {
return errors.Wrapf(err, "unable to write pod logs for %s", h.Name)
return fmt.Errorf("unable to write pod logs for %s: %w", h.Name, err)
}
}
}

@ -20,7 +20,7 @@ import (
"strings"
"helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/releaseutil"
releaseutil "helm.sh/helm/v4/pkg/release/util"
)
func filterManifestsToKeep(manifests []releaseutil.Manifest) (keep, remaining []releaseutil.Manifest) {

@ -19,13 +19,13 @@ package action
import (
"bytes"
"fmt"
"log/slog"
"strings"
"time"
"github.com/pkg/errors"
chartutil "helm.sh/helm/v4/pkg/chart/util"
"helm.sh/helm/v4/pkg/release"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/kube"
release "helm.sh/helm/v4/pkg/release/v1"
helmtime "helm.sh/helm/v4/pkg/time"
)
@ -37,11 +37,10 @@ type Rollback struct {
Version int
Timeout time.Duration
Wait bool
WaitStrategy kube.WaitStrategy
WaitForJobs bool
DisableHooks bool
DryRun bool
Recreate bool // will (if true) recreate pods after a rollback.
Force bool // will (if true) force resource upgrade through uninstall/recreate if needed
CleanupOnFail bool
MaxHistory int // MaxHistory limits the maximum number of revisions saved per release
@ -62,26 +61,26 @@ func (r *Rollback) Run(name string) error {
r.cfg.Releases.MaxHistory = r.MaxHistory
r.cfg.Log("preparing rollback of %s", name)
slog.Debug("preparing rollback", "name", name)
currentRelease, targetRelease, err := r.prepareRollback(name)
if err != nil {
return err
}
if !r.DryRun {
r.cfg.Log("creating rolled back release for %s", name)
slog.Debug("creating rolled back release", "name", name)
if err := r.cfg.Releases.Create(targetRelease); err != nil {
return err
}
}
r.cfg.Log("performing rollback of %s", name)
slog.Debug("performing rollback", "name", name)
if _, err := r.performRollback(currentRelease, targetRelease); err != nil {
return err
}
if !r.DryRun {
r.cfg.Log("updating status for rolled back release for %s", name)
slog.Debug("updating status for rolled back release", "name", name)
if err := r.cfg.Releases.Update(targetRelease); err != nil {
return err
}
@ -93,7 +92,7 @@ func (r *Rollback) Run(name string) error {
// the previous release's configuration
func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Release, error) {
if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, nil, errors.Errorf("prepareRollback: Release name is invalid: %s", name)
return nil, nil, fmt.Errorf("prepareRollback: Release name is invalid: %s", name)
}
if r.Version < 0 {
@ -125,10 +124,10 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
}
}
if !previousVersionExist {
return nil, nil, errors.Errorf("release has no %d version", previousVersion)
return nil, nil, fmt.Errorf("release has no %d version", previousVersion)
}
r.cfg.Log("rolling back %s (current: v%d, target: v%d)", name, currentRelease.Version, previousVersion)
slog.Debug("rolling back", "name", name, "currentVersion", currentRelease.Version, "targetVersion", previousVersion)
previousRelease, err := r.cfg.Releases.Get(name, previousVersion)
if err != nil {
@ -161,89 +160,79 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
func (r *Rollback) performRollback(currentRelease, targetRelease *release.Release) (*release.Release, error) {
if r.DryRun {
r.cfg.Log("dry run for %s", targetRelease.Name)
slog.Debug("dry run", "name", targetRelease.Name)
return targetRelease, nil
}
current, err := r.cfg.KubeClient.Build(bytes.NewBufferString(currentRelease.Manifest), false)
if err != nil {
return targetRelease, errors.Wrap(err, "unable to build kubernetes objects from current release manifest")
return targetRelease, fmt.Errorf("unable to build kubernetes objects from current release manifest: %w", err)
}
target, err := r.cfg.KubeClient.Build(bytes.NewBufferString(targetRelease.Manifest), false)
if err != nil {
return targetRelease, errors.Wrap(err, "unable to build kubernetes objects from new release manifest")
return targetRelease, fmt.Errorf("unable to build kubernetes objects from new release manifest: %w", err)
}
// pre-rollback hooks
if !r.DisableHooks {
if err := r.cfg.execHook(targetRelease, release.HookPreRollback, r.Timeout); err != nil {
if err := r.cfg.execHook(targetRelease, release.HookPreRollback, r.WaitStrategy, r.Timeout); err != nil {
return targetRelease, err
}
} else {
r.cfg.Log("rollback hooks disabled for %s", targetRelease.Name)
slog.Debug("rollback hooks disabled", "name", targetRelease.Name)
}
// It is safe to use "force" here because these are resources currently rendered by the chart.
err = target.Visit(setMetadataVisitor(targetRelease.Name, targetRelease.Namespace, true))
if err != nil {
return targetRelease, errors.Wrap(err, "unable to set metadata visitor from target release")
return targetRelease, fmt.Errorf("unable to set metadata visitor from target release: %w", err)
}
results, err := r.cfg.KubeClient.Update(current, target, r.Force)
if err != nil {
msg := fmt.Sprintf("Rollback %q failed: %s", targetRelease.Name, err)
r.cfg.Log("warning: %s", msg)
slog.Warn(msg)
currentRelease.Info.Status = release.StatusSuperseded
targetRelease.Info.Status = release.StatusFailed
targetRelease.Info.Description = msg
r.cfg.recordRelease(currentRelease)
r.cfg.recordRelease(targetRelease)
if r.CleanupOnFail {
r.cfg.Log("Cleanup on fail set, cleaning up %d resources", len(results.Created))
slog.Debug("cleanup on fail set, cleaning up resources", "count", len(results.Created))
_, errs := r.cfg.KubeClient.Delete(results.Created)
if errs != nil {
var errorList []string
for _, e := range errs {
errorList = append(errorList, e.Error())
}
return targetRelease, errors.Wrapf(fmt.Errorf("unable to cleanup resources: %s", strings.Join(errorList, ", ")), "an error occurred while cleaning up resources. original rollback error: %s", err)
return targetRelease, fmt.Errorf(
"an error occurred while cleaning up resources. original rollback error: %w",
fmt.Errorf("unable to cleanup resources: %w", joinErrors(errs, ", ")))
}
r.cfg.Log("Resource cleanup complete")
slog.Debug("resource cleanup complete")
}
return targetRelease, err
}
if r.Recreate {
// NOTE: Because this is not critical for a release to succeed, we just
// log if an error occurs and continue onward. If we ever introduce log
// levels, we should make these error level logs so users are notified
// that they'll need to go do the cleanup on their own
if err := recreate(r.cfg, results.Updated); err != nil {
r.cfg.Log(err.Error())
waiter, err := r.cfg.KubeClient.GetWaiter(r.WaitStrategy)
if err != nil {
return nil, fmt.Errorf("unable to set metadata visitor from target release: %w", err)
}
if r.WaitForJobs {
if err := waiter.WaitWithJobs(target, r.Timeout); err != nil {
targetRelease.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error()))
r.cfg.recordRelease(currentRelease)
r.cfg.recordRelease(targetRelease)
return targetRelease, fmt.Errorf("release %s failed: %w", targetRelease.Name, err)
}
}
if r.Wait {
if r.WaitForJobs {
if err := r.cfg.KubeClient.WaitWithJobs(target, r.Timeout); err != nil {
targetRelease.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error()))
r.cfg.recordRelease(currentRelease)
r.cfg.recordRelease(targetRelease)
return targetRelease, errors.Wrapf(err, "release %s failed", targetRelease.Name)
}
} else {
if err := r.cfg.KubeClient.Wait(target, r.Timeout); err != nil {
targetRelease.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error()))
r.cfg.recordRelease(currentRelease)
r.cfg.recordRelease(targetRelease)
return targetRelease, errors.Wrapf(err, "release %s failed", targetRelease.Name)
}
} else {
if err := waiter.Wait(target, r.Timeout); err != nil {
targetRelease.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error()))
r.cfg.recordRelease(currentRelease)
r.cfg.recordRelease(targetRelease)
return targetRelease, fmt.Errorf("release %s failed: %w", targetRelease.Name, err)
}
}
// post-rollback hooks
if !r.DisableHooks {
if err := r.cfg.execHook(targetRelease, release.HookPostRollback, r.Timeout); err != nil {
if err := r.cfg.execHook(targetRelease, release.HookPostRollback, r.WaitStrategy, r.Timeout); err != nil {
return targetRelease, err
}
}
@ -254,7 +243,7 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
}
// Supersede all previous deployments, see issue #2941.
for _, rel := range deployed {
r.cfg.Log("superseding previous deployment %d", rel.Version)
slog.Debug("superseding previous deployment", "version", rel.Version)
rel.Info.Status = release.StatusSuperseded
r.cfg.recordRelease(rel)
}

@ -21,13 +21,12 @@ import (
"fmt"
"strings"
"github.com/pkg/errors"
"k8s.io/cli-runtime/pkg/printers"
"sigs.k8s.io/yaml"
"helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v4/pkg/chart/loader"
chartutil "helm.sh/helm/v4/pkg/chart/util"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/chart/v2/loader"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/registry"
)
@ -69,14 +68,14 @@ func NewShow(output ShowOutputFormat, cfg *Configuration) *Show {
sh := &Show{
OutputFormat: output,
}
sh.ChartPathOptions.registryClient = cfg.RegistryClient
sh.registryClient = cfg.RegistryClient
return sh
}
// SetRegistryClient sets the registry client to use when pulling a chart from a registry.
func (s *Show) SetRegistryClient(client *registry.Client) {
s.ChartPathOptions.registryClient = client
s.registryClient = client
}
// Run executes 'helm show' against the given release.
@ -105,7 +104,7 @@ func (s *Show) Run(chartpath string) (string, error) {
if s.JSONPathTemplate != "" {
printer, err := printers.NewJSONPathPrinter(s.JSONPathTemplate)
if err != nil {
return "", errors.Wrapf(err, "error parsing jsonpath %s", s.JSONPathTemplate)
return "", fmt.Errorf("error parsing jsonpath %s: %w", s.JSONPathTemplate, err)
}
printer.Execute(&out, s.chart.Values)
} else {

@ -19,7 +19,7 @@ package action
import (
"testing"
"helm.sh/helm/v4/pkg/chart"
chart "helm.sh/helm/v4/pkg/chart/v2"
)
func TestShow(t *testing.T) {

@ -21,7 +21,7 @@ import (
"errors"
"helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/release"
release "helm.sh/helm/v4/pkg/release/v1"
)
// Status is the action for checking the deployment status of releases.

@ -17,17 +17,17 @@ limitations under the License.
package action
import (
"fmt"
"log/slog"
"strings"
"time"
"github.com/pkg/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
chartutil "helm.sh/helm/v4/pkg/chart/util"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/releaseutil"
releaseutil "helm.sh/helm/v4/pkg/release/util"
release "helm.sh/helm/v4/pkg/release/v1"
helmtime "helm.sh/helm/v4/pkg/time"
)
@ -41,7 +41,7 @@ type Uninstall struct {
DryRun bool
IgnoreNotFound bool
KeepHistory bool
Wait bool
WaitStrategy kube.WaitStrategy
DeletionPropagation string
Timeout time.Duration
Description string
@ -60,6 +60,11 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
return nil, err
}
waiter, err := u.cfg.KubeClient.GetWaiter(u.WaitStrategy)
if err != nil {
return nil, err
}
if u.DryRun {
// In the dry run case, just see if the release exists
r, err := u.cfg.releaseContent(name, 0)
@ -70,7 +75,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
}
if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, errors.Errorf("uninstall: Release name is invalid: %s", name)
return nil, fmt.Errorf("uninstall: Release name is invalid: %s", name)
}
rels, err := u.cfg.Releases.History(name)
@ -78,7 +83,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
if u.IgnoreNotFound {
return nil, nil
}
return nil, errors.Wrapf(err, "uninstall: Release not loaded: %s", name)
return nil, fmt.Errorf("uninstall: Release not loaded: %s: %w", name, err)
}
if len(rels) < 1 {
return nil, errMissingRelease
@ -92,37 +97,37 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
if rel.Info.Status == release.StatusUninstalled {
if !u.KeepHistory {
if err := u.purgeReleases(rels...); err != nil {
return nil, errors.Wrap(err, "uninstall: Failed to purge the release")
return nil, fmt.Errorf("uninstall: Failed to purge the release: %w", err)
}
return &release.UninstallReleaseResponse{Release: rel}, nil
}
return nil, errors.Errorf("the release named %q is already deleted", name)
return nil, fmt.Errorf("the release named %q is already deleted", name)
}
u.cfg.Log("uninstall: Deleting %s", name)
slog.Debug("uninstall: deleting release", "name", name)
rel.Info.Status = release.StatusUninstalling
rel.Info.Deleted = helmtime.Now()
rel.Info.Description = "Deletion in progress (or silently failed)"
res := &release.UninstallReleaseResponse{Release: rel}
if !u.DisableHooks {
if err := u.cfg.execHook(rel, release.HookPreDelete, u.Timeout); err != nil {
if err := u.cfg.execHook(rel, release.HookPreDelete, u.WaitStrategy, u.Timeout); err != nil {
return res, err
}
} else {
u.cfg.Log("delete hooks disabled for %s", name)
slog.Debug("delete hooks disabled", "release", name)
}
// From here on out, the release is currently considered to be in StatusUninstalling
// state.
if err := u.cfg.Releases.Update(rel); err != nil {
u.cfg.Log("uninstall: Failed to store updated release: %s", err)
slog.Debug("uninstall: Failed to store updated release", slog.Any("error", err))
}
deletedResources, kept, errs := u.deleteRelease(rel)
if errs != nil {
u.cfg.Log("uninstall: Failed to delete release: %s", errs)
return nil, errors.Errorf("failed to delete release: %s", name)
slog.Debug("uninstall: Failed to delete release", slog.Any("error", errs))
return nil, fmt.Errorf("failed to delete release: %s", name)
}
if kept != "" {
@ -130,16 +135,12 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
}
res.Info = kept
if u.Wait {
if kubeClient, ok := u.cfg.KubeClient.(kube.InterfaceExt); ok {
if err := kubeClient.WaitForDelete(deletedResources, u.Timeout); err != nil {
errs = append(errs, err)
}
}
if err := waiter.WaitForDelete(deletedResources, u.Timeout); err != nil {
errs = append(errs, err)
}
if !u.DisableHooks {
if err := u.cfg.execHook(rel, release.HookPostDelete, u.Timeout); err != nil {
if err := u.cfg.execHook(rel, release.HookPostDelete, u.WaitStrategy, u.Timeout); err != nil {
errs = append(errs, err)
}
}
@ -152,26 +153,26 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
}
if !u.KeepHistory {
u.cfg.Log("purge requested for %s", name)
slog.Debug("purge requested", "release", name)
err := u.purgeReleases(rels...)
if err != nil {
errs = append(errs, errors.Wrap(err, "uninstall: Failed to purge the release"))
errs = append(errs, fmt.Errorf("uninstall: Failed to purge the release: %w", err))
}
// Return the errors that occurred while deleting the release, if any
if len(errs) > 0 {
return res, errors.Errorf("uninstallation completed with %d error(s): %s", len(errs), joinErrors(errs))
return res, fmt.Errorf("uninstallation completed with %d error(s): %w", len(errs), joinErrors(errs, "; "))
}
return res, nil
}
if err := u.cfg.Releases.Update(rel); err != nil {
u.cfg.Log("uninstall: Failed to store updated release: %s", err)
slog.Debug("uninstall: Failed to store updated release", slog.Any("error", err))
}
if len(errs) > 0 {
return res, errors.Errorf("uninstallation completed with %d error(s): %s", len(errs), joinErrors(errs))
return res, fmt.Errorf("uninstallation completed with %d error(s): %w", len(errs), joinErrors(errs, "; "))
}
return res, nil
}
@ -185,12 +186,28 @@ func (u *Uninstall) purgeReleases(rels ...*release.Release) error {
return nil
}
func joinErrors(errs []error) string {
es := make([]string, 0, len(errs))
for _, e := range errs {
es = append(es, e.Error())
type joinedErrors struct {
errs []error
sep string
}
func joinErrors(errs []error, sep string) error {
return &joinedErrors{
errs: errs,
sep: sep,
}
return strings.Join(es, "; ")
}
func (e *joinedErrors) Error() string {
errs := make([]string, 0, len(e.errs))
for _, err := range e.errs {
errs = append(errs, err.Error())
}
return strings.Join(errs, e.sep)
}
func (e *joinedErrors) Unwrap() []error {
return e.errs
}
// deleteRelease deletes the release and returns list of delete resources and manifests that were kept in the deletion process
@ -204,7 +221,7 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, stri
// FIXME: One way to delete at this point would be to try a label-based
// deletion. The problem with this is that we could get a false positive
// and delete something that was not legitimately part of this release.
return nil, rel.Manifest, []error{errors.Wrap(err, "corrupted release record. You must manually delete the resources")}
return nil, rel.Manifest, []error{fmt.Errorf("corrupted release record. You must manually delete the resources: %w", err)}
}
filesToKeep, filesToDelete := filterManifestsToKeep(files)
@ -220,11 +237,11 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, stri
resources, err := u.cfg.KubeClient.Build(strings.NewReader(builder.String()), false)
if err != nil {
return nil, "", []error{errors.Wrap(err, "unable to build kubernetes objects for delete")}
return nil, "", []error{fmt.Errorf("unable to build kubernetes objects for delete: %w", err)}
}
if len(resources) > 0 {
if kubeClient, ok := u.cfg.KubeClient.(kube.InterfaceDeletionPropagation); ok {
_, errs = kubeClient.DeleteWithPropagationPolicy(resources, parseCascadingFlag(u.cfg, u.DeletionPropagation))
_, errs = kubeClient.DeleteWithPropagationPolicy(resources, parseCascadingFlag(u.DeletionPropagation))
return resources, kept, errs
}
_, errs = u.cfg.KubeClient.Delete(resources)
@ -232,7 +249,7 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, stri
return resources, kept, errs
}
func parseCascadingFlag(cfg *Configuration, cascadingFlag string) v1.DeletionPropagation {
func parseCascadingFlag(cascadingFlag string) v1.DeletionPropagation {
switch cascadingFlag {
case "orphan":
return v1.DeletePropagationOrphan
@ -241,7 +258,7 @@ func parseCascadingFlag(cfg *Configuration, cascadingFlag string) v1.DeletionPro
case "background":
return v1.DeletePropagationBackground
default:
cfg.Log("uninstall: given cascade value: %s, defaulting to delete propagation background", cascadingFlag)
slog.Debug("uninstall: given cascade value, defaulting to delete propagation background", "value", cascadingFlag)
return v1.DeletePropagationBackground
}
}

@ -22,11 +22,13 @@ import (
"github.com/stretchr/testify/assert"
"helm.sh/helm/v4/pkg/kube"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/release"
release "helm.sh/helm/v4/pkg/release/v1"
)
func uninstallAction(t *testing.T) *Uninstall {
t.Helper()
config := actionConfigFixture(t)
unAction := NewUninstall(config)
return unAction
@ -82,7 +84,7 @@ func TestUninstallRelease_Wait(t *testing.T) {
unAction := uninstallAction(t)
unAction.DisableHooks = true
unAction.DryRun = false
unAction.Wait = true
unAction.WaitStrategy = kube.StatusWatcherStrategy
rel := releaseStub()
rel.Name = "come-fail-away"
@ -99,7 +101,7 @@ func TestUninstallRelease_Wait(t *testing.T) {
}`
unAction.cfg.Releases.Create(rel)
failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("U timed out")
failer.WaitForDeleteError = fmt.Errorf("U timed out")
unAction.cfg.KubeClient = failer
res, err := unAction.Run(rel.Name)
is.Error(err)
@ -113,7 +115,7 @@ func TestUninstallRelease_Cascade(t *testing.T) {
unAction := uninstallAction(t)
unAction.DisableHooks = true
unAction.DryRun = false
unAction.Wait = false
unAction.WaitStrategy = kube.HookOnlyStrategy
unAction.DeletionPropagation = "foreground"
rel := releaseStub()

@ -19,22 +19,22 @@ package action
import (
"bytes"
"context"
"errors"
"fmt"
"log/slog"
"strings"
"sync"
"time"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/resource"
"helm.sh/helm/v4/pkg/chart"
chartutil "helm.sh/helm/v4/pkg/chart/util"
chart "helm.sh/helm/v4/pkg/chart/v2"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/postrender"
"helm.sh/helm/v4/pkg/registry"
"helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/releaseutil"
releaseutil "helm.sh/helm/v4/pkg/release/util"
release "helm.sh/helm/v4/pkg/release/v1"
"helm.sh/helm/v4/pkg/storage/driver"
)
@ -64,8 +64,8 @@ type Upgrade struct {
SkipCRDs bool
// Timeout is the timeout for this operation
Timeout time.Duration
// Wait determines whether the wait operation should be performed after the upgrade is requested.
Wait bool
// WaitStrategy determines what type of waiting should be done
WaitStrategy kube.WaitStrategy
// WaitForJobs determines whether the wait operation for the Jobs should be performed after the upgrade is requested.
WaitForJobs bool
// DisableHooks disables hook processing if set to true.
@ -87,8 +87,6 @@ type Upgrade struct {
ReuseValues bool
// ResetThenReuseValues will reset the values to the chart's built-ins then merge with user's last supplied values.
ResetThenReuseValues bool
// Recreate will (if true) recreate pods after a rollback.
Recreate bool
// MaxHistory limits the maximum number of revisions saved per release
MaxHistory int
// Atomic, if true, will roll back on failure.
@ -131,14 +129,14 @@ func NewUpgrade(cfg *Configuration) *Upgrade {
up := &Upgrade{
cfg: cfg,
}
up.ChartPathOptions.registryClient = cfg.RegistryClient
up.registryClient = cfg.RegistryClient
return up
}
// SetRegistryClient sets the registry client to use when fetching charts.
func (u *Upgrade) SetRegistryClient(client *registry.Client) {
u.ChartPathOptions.registryClient = client
u.registryClient = client
}
// Run executes the upgrade on the given release.
@ -155,13 +153,15 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
// Make sure if Atomic is set, that wait is set as well. This makes it so
// the user doesn't have to specify both
u.Wait = u.Wait || u.Atomic
if u.WaitStrategy == kube.HookOnlyStrategy && u.Atomic {
u.WaitStrategy = kube.StatusWatcherStrategy
}
if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, errors.Errorf("release name is invalid: %s", name)
return nil, fmt.Errorf("release name is invalid: %s", name)
}
u.cfg.Log("preparing upgrade for %s", name)
slog.Debug("preparing upgrade", "name", name)
currentRelease, upgradedRelease, err := u.prepareUpgrade(name, chart, vals)
if err != nil {
return nil, err
@ -169,7 +169,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
u.cfg.Releases.MaxHistory = u.MaxHistory
u.cfg.Log("performing update for %s", name)
slog.Debug("performing update", "name", name)
res, err := u.performUpgrade(ctx, currentRelease, upgradedRelease)
if err != nil {
return res, err
@ -177,7 +177,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
// Do not update for dry runs
if !u.isDryRun() {
u.cfg.Log("updating status for upgraded release for %s", name)
slog.Debug("updating status for upgraded release", "name", name)
if err := u.cfg.Releases.Update(upgradedRelease); err != nil {
return res, err
}
@ -202,7 +202,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
// HideSecret must be used with dry run. Otherwise, return an error.
if !u.isDryRun() && u.HideSecret {
return nil, nil, errors.New("Hiding Kubernetes secrets requires a dry-run mode")
return nil, nil, errors.New("hiding Kubernetes secrets requires a dry-run mode")
}
// finds the last non-deleted release with the given name
@ -313,15 +313,15 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR
// Checking for removed Kubernetes API error so can provide a more informative error message to the user
// Ref: https://github.com/helm/helm/issues/7219
if strings.Contains(err.Error(), "unable to recognize \"\": no matches for kind") {
return upgradedRelease, errors.Wrap(err, "current release manifest contains removed kubernetes api(s) for this "+
return upgradedRelease, fmt.Errorf("current release manifest contains removed kubernetes api(s) for this "+
"kubernetes version and it is therefore unable to build the kubernetes "+
"objects for performing the diff. error from kubernetes")
"objects for performing the diff. error from kubernetes: %w", err)
}
return upgradedRelease, errors.Wrap(err, "unable to build kubernetes objects from current release manifest")
return upgradedRelease, fmt.Errorf("unable to build kubernetes objects from current release manifest: %w", err)
}
target, err := u.cfg.KubeClient.Build(bytes.NewBufferString(upgradedRelease.Manifest), !u.DisableOpenAPIValidation)
if err != nil {
return upgradedRelease, errors.Wrap(err, "unable to build kubernetes objects from new release manifest")
return upgradedRelease, fmt.Errorf("unable to build kubernetes objects from new release manifest: %w", err)
}
// It is safe to use force only on target because these are resources currently rendered by the chart.
@ -350,7 +350,7 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR
toBeUpdated, err = existingResourceConflict(toBeCreated, upgradedRelease.Name, upgradedRelease.Namespace)
}
if err != nil {
return nil, errors.Wrap(err, "Unable to continue with update")
return nil, fmt.Errorf("unable to continue with update: %w", err)
}
toBeUpdated.Visit(func(r *resource.Info, err error) error {
@ -363,7 +363,7 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR
// Run if it is a dry run
if u.isDryRun() {
u.cfg.Log("dry run for %s", upgradedRelease.Name)
slog.Debug("dry run for release", "name", upgradedRelease.Name)
if len(u.Description) > 0 {
upgradedRelease.Info.Description = u.Description
} else {
@ -372,7 +372,7 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR
return upgradedRelease, nil
}
u.cfg.Log("creating upgraded release for %s", upgradedRelease.Name)
slog.Debug("creating upgraded release", "name", upgradedRelease.Name)
if err := u.cfg.Releases.Create(upgradedRelease); err != nil {
return nil, err
}
@ -418,12 +418,12 @@ func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *rele
// pre-upgrade hooks
if !u.DisableHooks {
if err := u.cfg.execHook(upgradedRelease, release.HookPreUpgrade, u.Timeout); err != nil {
if err := u.cfg.execHook(upgradedRelease, release.HookPreUpgrade, u.WaitStrategy, u.Timeout); err != nil {
u.reportToPerformUpgrade(c, upgradedRelease, kube.ResourceList{}, fmt.Errorf("pre-upgrade hooks failed: %s", err))
return
}
} else {
u.cfg.Log("upgrade hooks disabled for %s", upgradedRelease.Name)
slog.Debug("upgrade hooks disabled", "name", upgradedRelease.Name)
}
results, err := u.cfg.KubeClient.Update(current, target, u.Force)
@ -433,38 +433,29 @@ func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *rele
return
}
if u.Recreate {
// NOTE: Because this is not critical for a release to succeed, we just
// log if an error occurs and continue onward. If we ever introduce log
// levels, we should make these error level logs so users are notified
// that they'll need to go do the cleanup on their own
if err := recreate(u.cfg, results.Updated); err != nil {
u.cfg.Log(err.Error())
}
waiter, err := u.cfg.KubeClient.GetWaiter(u.WaitStrategy)
if err != nil {
u.cfg.recordRelease(originalRelease)
u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err)
return
}
if u.Wait {
u.cfg.Log(
"waiting for release %s resources (created: %d updated: %d deleted: %d)",
upgradedRelease.Name, len(results.Created), len(results.Updated), len(results.Deleted))
if u.WaitForJobs {
if err := u.cfg.KubeClient.WaitWithJobs(target, u.Timeout); err != nil {
u.cfg.recordRelease(originalRelease)
u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err)
return
}
} else {
if err := u.cfg.KubeClient.Wait(target, u.Timeout); err != nil {
u.cfg.recordRelease(originalRelease)
u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err)
return
}
if u.WaitForJobs {
if err := waiter.WaitWithJobs(target, u.Timeout); err != nil {
u.cfg.recordRelease(originalRelease)
u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err)
return
}
} else {
if err := waiter.Wait(target, u.Timeout); err != nil {
u.cfg.recordRelease(originalRelease)
u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err)
return
}
}
// post-upgrade hooks
if !u.DisableHooks {
if err := u.cfg.execHook(upgradedRelease, release.HookPostUpgrade, u.Timeout); err != nil {
if err := u.cfg.execHook(upgradedRelease, release.HookPostUpgrade, u.WaitStrategy, u.Timeout); err != nil {
u.reportToPerformUpgrade(c, upgradedRelease, results.Created, fmt.Errorf("post-upgrade hooks failed: %s", err))
return
}
@ -484,32 +475,35 @@ func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *rele
func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, err error) (*release.Release, error) {
msg := fmt.Sprintf("Upgrade %q failed: %s", rel.Name, err)
u.cfg.Log("warning: %s", msg)
slog.Warn("upgrade failed", "name", rel.Name, slog.Any("error", err))
rel.Info.Status = release.StatusFailed
rel.Info.Description = msg
u.cfg.recordRelease(rel)
if u.CleanupOnFail && len(created) > 0 {
u.cfg.Log("Cleanup on fail set, cleaning up %d resources", len(created))
slog.Debug("cleanup on fail set", "cleaning_resources", len(created))
_, errs := u.cfg.KubeClient.Delete(created)
if errs != nil {
var errorList []string
for _, e := range errs {
errorList = append(errorList, e.Error())
}
return rel, errors.Wrapf(fmt.Errorf("unable to cleanup resources: %s", strings.Join(errorList, ", ")), "an error occurred while cleaning up resources. original upgrade error: %s", err)
return rel, fmt.Errorf(
"an error occurred while cleaning up resources. original upgrade error: %w: %w",
err,
fmt.Errorf(
"unable to cleanup resources: %w",
joinErrors(errs, ", "),
),
)
}
u.cfg.Log("Resource cleanup complete")
slog.Debug("resource cleanup complete")
}
if u.Atomic {
u.cfg.Log("Upgrade failed and atomic is set, rolling back to last successful release")
slog.Debug("upgrade failed and atomic is set, rolling back to last successful release")
// As a protection, get the last successful release before rollback.
// If there are no successful releases, bail out
hist := NewHistory(u.cfg)
fullHistory, herr := hist.Run(rel.Name)
if herr != nil {
return rel, errors.Wrapf(herr, "an error occurred while finding last successful release. original upgrade error: %s", err)
return rel, fmt.Errorf("an error occurred while finding last successful release. original upgrade error: %w: %w", err, herr)
}
// There isn't a way to tell if a previous release was successful, but
@ -519,23 +513,24 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e
return r.Info.Status == release.StatusSuperseded || r.Info.Status == release.StatusDeployed
}).Filter(fullHistory)
if len(filteredHistory) == 0 {
return rel, errors.Wrap(err, "unable to find a previously successful release when attempting to rollback. original upgrade error")
return rel, fmt.Errorf("unable to find a previously successful release when attempting to rollback. original upgrade error: %w", err)
}
releaseutil.Reverse(filteredHistory, releaseutil.SortByRevision)
rollin := NewRollback(u.cfg)
rollin.Version = filteredHistory[0].Version
rollin.Wait = true
if u.WaitStrategy == kube.HookOnlyStrategy {
rollin.WaitStrategy = kube.StatusWatcherStrategy
}
rollin.WaitForJobs = u.WaitForJobs
rollin.DisableHooks = u.DisableHooks
rollin.Recreate = u.Recreate
rollin.Force = u.Force
rollin.Timeout = u.Timeout
if rollErr := rollin.Run(rel.Name); rollErr != nil {
return rel, errors.Wrapf(rollErr, "an error occurred while rolling back the release. original upgrade error: %s", err)
return rel, fmt.Errorf("an error occurred while rolling back the release. original upgrade error: %w: %w", err, rollErr)
}
return rel, errors.Wrapf(err, "release %s failed, and has been rolled back due to atomic being set", rel.Name)
return rel, fmt.Errorf("release %s failed, and has been rolled back due to atomic being set: %w", rel.Name, err)
}
return rel, err
@ -552,18 +547,18 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e
func (u *Upgrade) reuseValues(chart *chart.Chart, current *release.Release, newVals map[string]interface{}) (map[string]interface{}, error) {
if u.ResetValues {
// If ResetValues is set, we completely ignore current.Config.
u.cfg.Log("resetting values to the chart's original version")
slog.Debug("resetting values to the chart's original version")
return newVals, nil
}
// If the ReuseValues flag is set, we always copy the old values over the new config's values.
if u.ReuseValues {
u.cfg.Log("reusing the old release's values")
slog.Debug("reusing the old release's values")
// We have to regenerate the old coalesced values:
oldVals, err := chartutil.CoalesceValues(current.Chart, current.Config)
if err != nil {
return nil, errors.Wrap(err, "failed to rebuild old values")
return nil, fmt.Errorf("failed to rebuild old values: %w", err)
}
newVals = chartutil.CoalesceTables(newVals, current.Config)
@ -575,7 +570,7 @@ func (u *Upgrade) reuseValues(chart *chart.Chart, current *release.Release, newV
// If the ResetThenReuseValues flag is set, we use the new chart's values, but we copy the old config's values over the new config's values.
if u.ResetThenReuseValues {
u.cfg.Log("merging values from old release to new values")
slog.Debug("merging values from old release to new values")
newVals = chartutil.CoalesceTables(newVals, current.Config)
@ -583,7 +578,7 @@ func (u *Upgrade) reuseValues(chart *chart.Chart, current *release.Release, newV
}
if len(newVals) == 0 && len(current.Config) > 0 {
u.cfg.Log("copying values from %s (v%d) to new release.", current.Name, current.Version)
slog.Debug("copying values from old release", "name", current.Name, "version", current.Version)
newVals = current.Config
}
return newVals, nil
@ -594,42 +589,6 @@ func validateManifest(c kube.Interface, manifest []byte, openAPIValidation bool)
return err
}
// recreate captures all the logic for recreating pods for both upgrade and
// rollback. If we end up refactoring rollback to use upgrade, this can just be
// made an unexported method on the upgrade action.
func recreate(cfg *Configuration, resources kube.ResourceList) error {
for _, res := range resources {
versioned := kube.AsVersioned(res)
selector, err := kube.SelectorsForObject(versioned)
if err != nil {
// If no selector is returned, it means this object is
// definitely not a pod, so continue onward
continue
}
client, err := cfg.KubernetesClientSet()
if err != nil {
return errors.Wrapf(err, "unable to recreate pods for object %s/%s because an error occurred", res.Namespace, res.Name)
}
pods, err := client.CoreV1().Pods(res.Namespace).List(context.Background(), metav1.ListOptions{
LabelSelector: selector.String(),
})
if err != nil {
return errors.Wrapf(err, "unable to recreate pods for object %s/%s because an error occurred", res.Namespace, res.Name)
}
// Restart pods
for _, pod := range pods.Items {
// Delete each pod for get them restarted with changed spec.
if err := client.CoreV1().Pods(pod.Namespace).Delete(context.Background(), pod.Name, *metav1.NewPreconditionDeleteOptions(string(pod.UID))); err != nil {
return errors.Wrapf(err, "unable to recreate pods for object %s/%s because an error occurred", res.Namespace, res.Name)
}
}
}
return nil
}
func objectKey(r *resource.Info) string {
gvk := r.Object.GetObjectKind().GroupVersionKind()
return fmt.Sprintf("%s/%s/%s/%s", gvk.GroupVersion().String(), gvk.Kind, r.Namespace, r.Name)

@ -23,18 +23,20 @@ import (
"testing"
"time"
"helm.sh/helm/v4/pkg/chart"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/storage/driver"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/release"
release "helm.sh/helm/v4/pkg/release/v1"
helmtime "helm.sh/helm/v4/pkg/time"
)
func upgradeAction(t *testing.T) *Upgrade {
t.Helper()
config := actionConfigFixture(t)
upAction := NewUpgrade(config)
upAction.Namespace = "spaced"
@ -52,10 +54,10 @@ func TestUpgradeRelease_Success(t *testing.T) {
rel.Info.Status = release.StatusDeployed
req.NoError(upAction.cfg.Releases.Create(rel))
upAction.Wait = true
upAction.WaitStrategy = kube.StatusWatcherStrategy
vals := map[string]interface{}{}
ctx, done := context.WithCancel(context.Background())
ctx, done := context.WithCancel(t.Context())
res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals)
done()
req.NoError(err)
@ -82,7 +84,7 @@ func TestUpgradeRelease_Wait(t *testing.T) {
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("I timed out")
upAction.cfg.KubeClient = failer
upAction.Wait = true
upAction.WaitStrategy = kube.StatusWatcherStrategy
vals := map[string]interface{}{}
res, err := upAction.Run(rel.Name, buildChart(), vals)
@ -104,7 +106,7 @@ func TestUpgradeRelease_WaitForJobs(t *testing.T) {
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("I timed out")
upAction.cfg.KubeClient = failer
upAction.Wait = true
upAction.WaitStrategy = kube.StatusWatcherStrategy
upAction.WaitForJobs = true
vals := map[string]interface{}{}
@ -128,7 +130,7 @@ func TestUpgradeRelease_CleanupOnFail(t *testing.T) {
failer.WaitError = fmt.Errorf("I timed out")
failer.DeleteError = fmt.Errorf("I tried to delete nil")
upAction.cfg.KubeClient = failer
upAction.Wait = true
upAction.WaitStrategy = kube.StatusWatcherStrategy
upAction.CleanupOnFail = true
vals := map[string]interface{}{}
@ -382,7 +384,6 @@ func TestUpgradeRelease_Pending(t *testing.T) {
}
func TestUpgradeRelease_Interrupted_Wait(t *testing.T) {
is := assert.New(t)
req := require.New(t)
@ -395,11 +396,10 @@ func TestUpgradeRelease_Interrupted_Wait(t *testing.T) {
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitDuration = 10 * time.Second
upAction.cfg.KubeClient = failer
upAction.Wait = true
upAction.WaitStrategy = kube.StatusWatcherStrategy
vals := map[string]interface{}{}
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
ctx, cancel := context.WithCancel(t.Context())
time.AfterFunc(time.Second, cancel)
res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals)
@ -407,11 +407,9 @@ func TestUpgradeRelease_Interrupted_Wait(t *testing.T) {
req.Error(err)
is.Contains(res.Info.Description, "Upgrade \"interrupted-release\" failed: context canceled")
is.Equal(res.Info.Status, release.StatusFailed)
}
func TestUpgradeRelease_Interrupted_Atomic(t *testing.T) {
is := assert.New(t)
req := require.New(t)
@ -427,8 +425,7 @@ func TestUpgradeRelease_Interrupted_Atomic(t *testing.T) {
upAction.Atomic = true
vals := map[string]interface{}{}
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
ctx, cancel := context.WithCancel(t.Context())
time.AfterFunc(time.Second, cancel)
res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals)
@ -444,7 +441,7 @@ func TestUpgradeRelease_Interrupted_Atomic(t *testing.T) {
}
func TestMergeCustomLabels(t *testing.T) {
var tests = [][3]map[string]string{
tests := [][3]map[string]string{
{nil, nil, map[string]string{}},
{map[string]string{}, map[string]string{}, map[string]string{}},
{map[string]string{"k1": "v1", "k2": "v2"}, nil, map[string]string{"k1": "v1", "k2": "v2"}},
@ -549,7 +546,7 @@ func TestUpgradeRelease_DryRun(t *testing.T) {
upAction.DryRun = true
vals := map[string]interface{}{}
ctx, done := context.WithCancel(context.Background())
ctx, done := context.WithCancel(t.Context())
res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals)
done()
req.NoError(err)
@ -565,7 +562,7 @@ func TestUpgradeRelease_DryRun(t *testing.T) {
upAction.HideSecret = true
vals = map[string]interface{}{}
ctx, done = context.WithCancel(context.Background())
ctx, done = context.WithCancel(t.Context())
res, err = upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals)
done()
req.NoError(err)
@ -581,7 +578,7 @@ func TestUpgradeRelease_DryRun(t *testing.T) {
upAction.DryRun = false
vals = map[string]interface{}{}
ctx, done = context.WithCancel(context.Background())
ctx, done = context.WithCancel(t.Context())
_, err = upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals)
done()
req.Error(err)

@ -18,8 +18,8 @@ package action
import (
"fmt"
"maps"
"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
@ -52,7 +52,7 @@ func requireAdoption(resources kube.ResourceList) (kube.ResourceList, error) {
if apierrors.IsNotFound(err) {
return nil
}
return errors.Wrapf(err, "could not get information about the resource %s", resourceString(info))
return fmt.Errorf("could not get information about the resource %s: %w", resourceString(info), err)
}
requireUpdate.Append(info)
@ -76,7 +76,7 @@ func existingResourceConflict(resources kube.ResourceList, releaseName, releaseN
if apierrors.IsNotFound(err) {
return nil
}
return errors.Wrapf(err, "could not get information about the resource %s", resourceString(info))
return fmt.Errorf("could not get information about the resource %s: %w", resourceString(info), err)
}
// Allow adoption of the resource if it is managed by Helm and is annotated with correct release name and namespace.
@ -113,11 +113,7 @@ func checkOwnership(obj runtime.Object, releaseName, releaseNamespace string) er
}
if len(errs) > 0 {
err := errors.New("invalid ownership metadata")
for _, e := range errs {
err = fmt.Errorf("%w; %s", err, e)
}
return err
return fmt.Errorf("invalid ownership metadata; %w", joinErrors(errs, "; "))
}
return nil
@ -199,11 +195,7 @@ func mergeAnnotations(obj runtime.Object, annotations map[string]string) error {
// merge two maps, always taking the value on the right
func mergeStrStrMaps(current, desired map[string]string) map[string]string {
result := make(map[string]string)
for k, v := range current {
result[k] = v
}
for k, desiredVal := range desired {
result[k] = desiredVal
}
maps.Copy(result, current)
maps.Copy(result, desired)
return result
}

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package chart
package v2
import (
"path/filepath"
@ -113,6 +113,8 @@ func (ch *Chart) ChartPath() string {
}
// ChartFullPath returns the full path to this chart.
// Note that the path may not correspond to the path where the file can be found on the file system if the path
// points to an aliased subchart.
func (ch *Chart) ChartFullPath() string {
if !ch.IsRoot() {
return ch.Parent().ChartFullPath() + "/charts/" + ch.Name()

@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package chart
package v2
import (
"encoding/json"

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package chart
package v2
import "time"

@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package chart
package v2
import (
"testing"

@ -0,0 +1,23 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
Package v2 provides chart handling for apiVersion v1 and v2 charts
This package and its sub-packages provide handling for apiVersion v1 and v2 charts.
The changes from v1 to v2 charts are minor and were able to be handled with minor
switches based on characteristics.
*/
package v2

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package chart
package v2
import "fmt"

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package chart
package v2
// File represents a file as a name/value pair.
//

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package chart
package v2
import (
"testing"

@ -20,6 +20,7 @@ import (
"archive/tar"
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"net/http"
@ -28,11 +29,18 @@ import (
"regexp"
"strings"
"github.com/pkg/errors"
"helm.sh/helm/v4/pkg/chart"
chart "helm.sh/helm/v4/pkg/chart/v2"
)
// MaxDecompressedChartSize is the maximum size of a chart archive that will be
// decompressed. This is the decompressed size of all the files.
// The default value is 100 MiB.
var MaxDecompressedChartSize int64 = 100 * 1024 * 1024 // Default 100 MiB
// MaxDecompressedFileSize is the size of the largest file that Helm will attempt to load.
// The size of the file is the decompressed version of it when it is stored in an archive.
var MaxDecompressedFileSize int64 = 5 * 1024 * 1024 // Default 5 MiB
var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`)
// FileLoader loads a chart from a file
@ -119,6 +127,7 @@ func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) {
files := []*BufferedFile{}
tr := tar.NewReader(unzipped)
remainingSize := MaxDecompressedChartSize
for {
b := bytes.NewBuffer(nil)
hd, err := tr.Next()
@ -160,7 +169,7 @@ func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) {
n = path.Clean(n)
if n == "." {
// In this case, the original path was relative when it should have been absolute.
return nil, errors.Errorf("chart illegally contains content outside the base directory: %q", hd.Name)
return nil, fmt.Errorf("chart illegally contains content outside the base directory: %q", hd.Name)
}
if strings.HasPrefix(n, "..") {
return nil, errors.New("chart illegally references parent directory")
@ -178,10 +187,30 @@ func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) {
return nil, errors.New("chart yaml not in base directory")
}
if _, err := io.Copy(b, tr); err != nil {
if hd.Size > remainingSize {
return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d", MaxDecompressedChartSize)
}
if hd.Size > MaxDecompressedFileSize {
return nil, fmt.Errorf("decompressed chart file %q is larger than the maximum file size %d", hd.Name, MaxDecompressedFileSize)
}
limitedReader := io.LimitReader(tr, remainingSize)
bytesWritten, err := io.Copy(b, limitedReader)
if err != nil {
return nil, err
}
remainingSize -= bytesWritten
// When the bytesWritten are less than the file size it means the limit reader ended
// copying early. Here we report that error. This is important if the last file extracted
// is the one that goes over the limit. It assumes the Size stored in the tar header
// is correct, something many applications do.
if bytesWritten < hd.Size || remainingSize <= 0 {
return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d", MaxDecompressedChartSize)
}
data := bytes.TrimPrefix(b.Bytes(), utf8bom)
files = append(files, &BufferedFile{Name: n, Data: data})

@ -33,6 +33,7 @@ func TestLoadArchiveFiles(t *testing.T) {
name: "empty input should return no files",
generate: func(_ *tar.Writer) {},
check: func(t *testing.T, _ []*BufferedFile, err error) {
t.Helper()
if err.Error() != "no files in chart archive" {
t.Fatalf(`expected "no files in chart archive", got [%#v]`, err)
}
@ -61,6 +62,7 @@ func TestLoadArchiveFiles(t *testing.T) {
}
},
check: func(t *testing.T, files []*BufferedFile, err error) {
t.Helper()
if err != nil {
t.Fatalf(`got unwanted error [%#v] for tar file with pax_global_header content`, err)
}

@ -23,10 +23,8 @@ import (
"path/filepath"
"strings"
"github.com/pkg/errors"
"helm.sh/helm/v4/internal/sympath"
"helm.sh/helm/v4/pkg/chart"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/ignore"
)
@ -101,9 +99,13 @@ func LoadDir(dir string) (*chart.Chart, error) {
return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", name)
}
if fi.Size() > MaxDecompressedFileSize {
return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), MaxDecompressedFileSize)
}
data, err := os.ReadFile(name)
if err != nil {
return errors.Wrapf(err, "error reading %s", n)
return fmt.Errorf("error reading %s: %w", n, err)
}
data = bytes.TrimPrefix(data, utf8bom)

@ -19,18 +19,19 @@ package loader
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"maps"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
utilyaml "k8s.io/apimachinery/pkg/util/yaml"
"sigs.k8s.io/yaml"
"helm.sh/helm/v4/pkg/chart"
chart "helm.sh/helm/v4/pkg/chart/v2"
)
// ChartLoader loads a chart.
@ -48,7 +49,6 @@ func Loader(name string) (ChartLoader, error) {
return DirLoader(name), nil
}
return FileLoader(name), nil
}
// Load takes a string name, tries to resolve it to a file or directory, and then loads it.
@ -86,7 +86,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
c.Metadata = new(chart.Metadata)
}
if err := yaml.Unmarshal(f.Data, c.Metadata); err != nil {
return c, errors.Wrap(err, "cannot load Chart.yaml")
return c, fmt.Errorf("cannot load Chart.yaml: %w", err)
}
// NOTE(bacongobbler): while the chart specification says that APIVersion must be set,
// Helm 2 accepted charts that did not provide an APIVersion in their chart metadata.
@ -104,12 +104,12 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
case f.Name == "Chart.lock":
c.Lock = new(chart.Lock)
if err := yaml.Unmarshal(f.Data, &c.Lock); err != nil {
return c, errors.Wrap(err, "cannot load Chart.lock")
return c, fmt.Errorf("cannot load Chart.lock: %w", err)
}
case f.Name == "values.yaml":
values, err := LoadValues(bytes.NewReader(f.Data))
if err != nil {
return c, errors.Wrap(err, "cannot load values.yaml")
return c, fmt.Errorf("cannot load values.yaml: %w", err)
}
c.Values = values
case f.Name == "values.schema.json":
@ -125,7 +125,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
log.Printf("Warning: Dependencies are handled in Chart.yaml since apiVersion \"v2\". We recommend migrating dependencies to Chart.yaml.")
}
if err := yaml.Unmarshal(f.Data, c.Metadata); err != nil {
return c, errors.Wrap(err, "cannot load requirements.yaml")
return c, fmt.Errorf("cannot load requirements.yaml: %w", err)
}
if c.Metadata.APIVersion == chart.APIVersionV1 {
c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data})
@ -134,7 +134,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
case f.Name == "requirements.lock":
c.Lock = new(chart.Lock)
if err := yaml.Unmarshal(f.Data, &c.Lock); err != nil {
return c, errors.Wrap(err, "cannot load requirements.lock")
return c, fmt.Errorf("cannot load requirements.lock: %w", err)
}
if c.Metadata == nil {
c.Metadata = new(chart.Metadata)
@ -163,7 +163,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
}
if c.Metadata == nil {
return c, errors.New("Chart.yaml file is missing")
return c, errors.New("Chart.yaml file is missing") //nolint:staticcheck
}
if err := c.Validate(); err != nil {
@ -179,7 +179,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
case filepath.Ext(n) == ".tgz":
file := files[0]
if file.Name != n {
return c, errors.Errorf("error unpacking subchart tar in %s: expected %s, got %s", c.Name(), n, file.Name)
return c, fmt.Errorf("error unpacking subchart tar in %s: expected %s, got %s", c.Name(), n, file.Name)
}
// Untar the chart and add to c.Dependencies
sc, err = LoadArchive(bytes.NewBuffer(file.Data))
@ -199,7 +199,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
}
if err != nil {
return c, errors.Wrapf(err, "error unpacking subchart %s in %s", n, c.Name())
return c, fmt.Errorf("error unpacking subchart %s in %s: %w", n, c.Name(), err)
}
c.AddDependency(sc)
}
@ -221,13 +221,10 @@ func LoadValues(data io.Reader) (map[string]interface{}, error) {
if err == io.EOF {
break
}
return nil, errors.Wrap(err, "error reading yaml document")
return nil, fmt.Errorf("error reading yaml document: %w", err)
}
if err := yaml.Unmarshal(raw, &currentMap, func(d *json.Decoder) *json.Decoder {
d.UseNumber()
return d
}); err != nil {
return nil, errors.Wrap(err, "cannot unmarshal yaml document")
if err := yaml.Unmarshal(raw, &currentMap); err != nil {
return nil, fmt.Errorf("cannot unmarshal yaml document: %w", err)
}
values = MergeMaps(values, currentMap)
}
@ -238,9 +235,7 @@ func LoadValues(data io.Reader) (map[string]interface{}, error) {
// If the value is a map, the maps will be merged recursively.
func MergeMaps(a, b map[string]interface{}) map[string]interface{} {
out := make(map[string]interface{}, len(a))
for k, v := range a {
out[k] = v
}
maps.Copy(out, a)
for k, v := range b {
if v, ok := v.(map[string]interface{}); ok {
if bv, ok := out[k]; ok {

@ -30,7 +30,7 @@ import (
"testing"
"time"
"helm.sh/helm/v4/pkg/chart"
chart "helm.sh/helm/v4/pkg/chart/v2"
)
func TestLoadDir(t *testing.T) {
@ -648,6 +648,7 @@ func verifyChart(t *testing.T, c *chart.Chart) {
}
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))
}
@ -670,6 +671,7 @@ func verifyDependencies(t *testing.T, c *chart.Chart) {
}
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))
}
@ -692,10 +694,12 @@ func verifyDependenciesLock(t *testing.T, c *chart.Chart) {
}
func verifyFrobnitz(t *testing.T, c *chart.Chart) {
t.Helper()
verifyChartFileAndTemplate(t, c, "frobnitz")
}
func verifyChartFileAndTemplate(t *testing.T, c *chart.Chart, name string) {
t.Helper()
if c.Metadata == nil {
t.Fatal("Metadata is nil")
}
@ -750,6 +754,7 @@ func verifyChartFileAndTemplate(t *testing.T, c *chart.Chart, name string) {
}
func verifyBomStripped(t *testing.T, files []*chart.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)

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

Loading…
Cancel
Save