Merge remote-tracking branch 'origin/main' into fix-include-needs-transitive-1003

Resolve merge conflicts in pkg/app/app.go:
- WithPreparedCharts rename (withPreparedCharts -> WithPreparedCharts)
- WriteOutput handling for fetch command
- DetailedExitcode handling for sync command
- GetPlannedAndSelectedReleasesWithNeeds new function (pass both includeNeeds params)
- SyncState using GetPlannedAndSelectedReleasesWithNeeds

Also fix ForEachState call signatures in tests to include both bool params.
This commit is contained in:
copilot-swe-agent[bot]
2026-05-27 08:46:23 +00:00
committed by GitHub
133 changed files with 10958 additions and 2933 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
HELM_VERSION ?= v4.1.0
HELM_VERSION ?= v4.2.0
KUSTOMIZE_VERSION ?= v5.8.0
K8S_VERSION ?= v1.34.0
MINIKUBE_VERSION ?= v1.37.0
+15 -15
View File
@@ -31,13 +31,13 @@ jobs:
run: df -h
- uses: golangci/golangci-lint-action@v9
with:
version: v2.6.2
version: v2.11.4
tests:
runs-on: ubuntu-latest
strategy:
matrix:
helm-version: [v3.18.6, v3.20.1, v4.1.3]
helm-version: [v3.18.6, v3.21.0, v4.2.0]
steps:
- uses: actions/checkout@v6
with:
@@ -67,16 +67,16 @@ jobs:
env:
HELMFILE_HELM4: ${{ startsWith(matrix.helm-version, 'v4') && '1' || '0' }}
- name: Archive built binaries
if: matrix.helm-version == 'v4.1.3'
if: matrix.helm-version == 'v4.2.0'
run: tar -cvf built-binaries.tar helmfile diff-yamls dyff
- uses: actions/upload-artifact@v7
if: matrix.helm-version == 'v4.1.3'
if: matrix.helm-version == 'v4.2.0'
with:
name: built-binaries-${{ github.run_id }}
path: built-binaries.tar
retention-days: 1
- name: Display built binaries
if: matrix.helm-version == 'v4.1.3'
if: matrix.helm-version == 'v4.2.0'
run: ls -l helmfile diff-yamls dyff
integration_tests:
@@ -96,35 +96,35 @@ jobs:
- helm-version: v3.18.6
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.4
plugin-diff-version: 3.15.3
plugin-diff-version: 3.15.7
extra-helmfile-flags: ''
# In case you need to test some optional helmfile features,
# enable it via extra-helmfile-flags below.
- helm-version: v3.18.6
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.4
plugin-diff-version: 3.15.3
plugin-diff-version: 3.15.7
extra-helmfile-flags: '--enable-live-output'
- helm-version: v3.20.1
- helm-version: v3.21.0
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.4
plugin-diff-version: 3.15.3
plugin-diff-version: 3.15.7
extra-helmfile-flags: ''
- helm-version: v3.20.1
- helm-version: v3.21.0
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.4
plugin-diff-version: 3.15.3
plugin-diff-version: 3.15.7
extra-helmfile-flags: '--enable-live-output'
# Helmfile now supports both Helm 3.x and Helm 4.x
- helm-version: v4.1.3
- helm-version: v4.2.0
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.4
plugin-diff-version: 3.15.3
plugin-diff-version: 3.15.7
extra-helmfile-flags: ''
- helm-version: v4.1.3
- helm-version: v4.2.0
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.4
plugin-diff-version: 3.15.3
plugin-diff-version: 3.15.7
extra-helmfile-flags: '--enable-live-output'
steps:
- uses: actions/checkout@v6
+1 -1
View File
@@ -4,7 +4,7 @@
### Essential Setup
```bash
# Check Go version (requires 1.24.2+)
# Check Go version (requires 1.26.2+)
go version
# Check Helm dependency (required at runtime)
+4 -1
View File
@@ -19,7 +19,7 @@ $ git checkout -b your-shiny-new-feature origin/main
...
$ git commit -m 'feat: do whatever for whatever purpose
$ git commit -s -m 'feat: do whatever for whatever purpose
This adds ... by:
@@ -30,6 +30,9 @@ This adds ... by:
Resolves #ISSUE_NUMBER
'
Remember to include a `Signed-off-by: Author Name <authoremail@example.com>`
line in the commit message, either manually or using the `-s` flag.
$ hub fork
$ git push YOUR_GITHUB_USER your-shiny-new-feature
$ hub pull-request
+4 -4
View File
@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
RUN apk add --no-cache make git
WORKDIR /workspace/helmfile
@@ -30,7 +30,7 @@ ENV HELM_CONFIG_HOME="${HELM_CONFIG_HOME}"
ARG HELM_DATA_HOME="${HOME}/.local/share/helm"
ENV HELM_DATA_HOME="${HELM_DATA_HOME}"
ARG HELM_VERSION="v4.1.3"
ARG HELM_VERSION="v4.2.0"
ENV HELM_VERSION="${HELM_VERSION}"
ENV HELM_BIN="/usr/local/bin/helm"
ARG HELM_LOCATION="https://get.helm.sh"
@@ -39,8 +39,8 @@ RUN set -x && \
curl --retry 5 --retry-connrefused -LO "${HELM_LOCATION}/${HELM_FILENAME}" && \
echo Verifying ${HELM_FILENAME}... && \
case ${TARGETPLATFORM} in \
"linux/amd64") HELM_SHA256="02ce9722d541238f81459938b84cf47df2fdf1187493b4bfb2346754d82a4700" ;; \
"linux/arm64") HELM_SHA256="5db45e027cc8de4677ec869e5d803fc7631b0bab1c1eb62ac603a62d22359a43" ;; \
"linux/amd64") HELM_SHA256="97dbeb971be4ac4b27e3839976d9564c0fb35c6f3b1da89dd1e292d236af4096" ;; \
"linux/arm64") HELM_SHA256="1f8de130dfbd04de64978e7b852a7a547be1404956a366608276d2520b678670" ;; \
esac && \
echo "${HELM_SHA256} ${HELM_FILENAME}" | sha256sum -c && \
echo Extracting ${HELM_FILENAME}... && \
+4 -4
View File
@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
RUN apk add --no-cache make git
WORKDIR /workspace/helmfile
@@ -38,7 +38,7 @@ ENV HELM_CONFIG_HOME="${HELM_CONFIG_HOME}"
ARG HELM_DATA_HOME="${HOME}/.local/share/helm"
ENV HELM_DATA_HOME="${HELM_DATA_HOME}"
ARG HELM_VERSION="v4.1.3"
ARG HELM_VERSION="v4.2.0"
ENV HELM_VERSION="${HELM_VERSION}"
ENV HELM_BIN="/usr/local/bin/helm"
ARG HELM_LOCATION="https://get.helm.sh"
@@ -47,8 +47,8 @@ RUN set -x && \
curl --retry 5 --retry-connrefused -LO "${HELM_LOCATION}/${HELM_FILENAME}" && \
echo Verifying ${HELM_FILENAME}... && \
case ${TARGETPLATFORM} in \
"linux/amd64") HELM_SHA256="02ce9722d541238f81459938b84cf47df2fdf1187493b4bfb2346754d82a4700" ;; \
"linux/arm64") HELM_SHA256="5db45e027cc8de4677ec869e5d803fc7631b0bab1c1eb62ac603a62d22359a43" ;; \
"linux/amd64") HELM_SHA256="97dbeb971be4ac4b27e3839976d9564c0fb35c6f3b1da89dd1e292d236af4096" ;; \
"linux/arm64") HELM_SHA256="1f8de130dfbd04de64978e7b852a7a547be1404956a366608276d2520b678670" ;; \
esac && \
echo "${HELM_SHA256} ${HELM_FILENAME}" | sha256sum -c && \
echo Extracting ${HELM_FILENAME}... && \
+4 -4
View File
@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
RUN apk add --no-cache make git
WORKDIR /workspace/helmfile
@@ -38,7 +38,7 @@ ENV HELM_CONFIG_HOME="${HELM_CONFIG_HOME}"
ARG HELM_DATA_HOME="${HOME}/.local/share/helm"
ENV HELM_DATA_HOME="${HELM_DATA_HOME}"
ARG HELM_VERSION="v4.1.3"
ARG HELM_VERSION="v4.2.0"
ENV HELM_VERSION="${HELM_VERSION}"
ENV HELM_BIN="/usr/local/bin/helm"
ARG HELM_LOCATION="https://get.helm.sh"
@@ -47,8 +47,8 @@ RUN set -x && \
curl --retry 5 --retry-connrefused -LO "${HELM_LOCATION}/${HELM_FILENAME}" && \
echo Verifying ${HELM_FILENAME}... && \
case ${TARGETPLATFORM} in \
"linux/amd64") HELM_SHA256="02ce9722d541238f81459938b84cf47df2fdf1187493b4bfb2346754d82a4700" ;; \
"linux/arm64") HELM_SHA256="5db45e027cc8de4677ec869e5d803fc7631b0bab1c1eb62ac603a62d22359a43" ;; \
"linux/amd64") HELM_SHA256="97dbeb971be4ac4b27e3839976d9564c0fb35c6f3b1da89dd1e292d236af4096" ;; \
"linux/arm64") HELM_SHA256="1f8de130dfbd04de64978e7b852a7a547be1404956a366608276d2520b678670" ;; \
esac && \
echo "${HELM_SHA256} ${HELM_FILENAME}" | sha256sum -c && \
echo Extracting ${HELM_FILENAME}... && \
+14 -1
View File
@@ -61,11 +61,24 @@ Helmfile 是一个声明式Helm Chart管理工具
> 安装后请运行一次 `helmfile init`。 检查[helm-diff](https://github.com/databus23/helm-diff) 等插件安装正确。
**方式4: 源码安装**
依赖: [Go](https://golang.org/dl/)
` go install github.com/helmfile/helmfile@latest `
## 使用
让我们从最简单的 helmfile 开始,逐渐改进它以适应您的用例!
假设表示您 helm releases 的期望状态的 helmfile.yaml 看起来像这样:
使用脚手架命令生成具有最佳实践目录结构的项目:
```console
helmfile create my-project && cd my-project
```
或者手动创建 `helmfile.yaml`。假设表示您 helm releases 的期望状态的 helmfile.yaml 看起来像这样:
```yaml
repositories:
+14 -1
View File
@@ -75,11 +75,24 @@ For more details, see [run as a container](https://helmfile.readthedocs.io/en/la
> Make sure to run `helmfile init` once after installation. Helmfile uses the [helm-diff](https://github.com/databus23/helm-diff) plugin.
**4: Build from source**
requirements: [Go](https://golang.org/dl/)
` go install github.com/helmfile/helmfile@latest `
## Getting Started
Let's start with a simple `helmfile` and gradually improve it to fit your use-case!
Suppose the `helmfile.yaml` representing the desired state of your helm releases looks like:
Generate a project scaffold with best-practice directory structure:
```console
helmfile create my-project && cd my-project
```
Or create a `helmfile.yaml` manually. Suppose the `helmfile.yaml` representing the desired state of your helm releases looks like:
```yaml
repositories:
+1
View File
@@ -72,6 +72,7 @@ func NewApplyCmd(globalCfg *config.GlobalImpl) *cobra.Command {
f.StringVar(&applyOptions.TrackMode, "track-mode", "", "Track mode for releases: 'helm' (default), 'helm-legacy' (Helm v4 only), or 'kubedog'")
f.IntVar(&applyOptions.TrackTimeout, "track-timeout", 0, `Timeout in seconds for kubedog tracking (0 to use default 300s timeout)`)
f.BoolVar(&applyOptions.TrackLogs, "track-logs", false, "Enable log streaming with kubedog tracking")
f.BoolVar(&applyOptions.TrackFailOnError, "track-fail-on-error", false, "Fail with non-zero exit code when kubedog tracking fails")
f.StringVar(&applyOptions.Description, "description", "", `Set description for all releases. If set, overridesdescriptions in helmfile.yaml. Will be passed to "helm upgrade --description"`)
return cmd
+45
View File
@@ -0,0 +1,45 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/helmfile/helmfile/pkg/app"
"github.com/helmfile/helmfile/pkg/config"
)
func NewCreateCmd(globalCfg *config.GlobalImpl) *cobra.Command {
options := config.NewCreateOptions()
cmd := &cobra.Command{
Use: "create [NAME]",
Short: "Create a helmfile deployment project scaffold",
Long: `Create a helmfile deployment project with best-practice directory structure.
Generates:
- helmfile.yaml Main configuration with commented examples
- environments/ Environment-specific value files
- values/ Release-specific value files
If NAME is provided, creates the project in a new directory named NAME.
Otherwise, creates the project in the current directory.`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
options.Name = args[0]
}
createImpl := config.NewCreateImpl(globalCfg, options)
if err := config.NewCLIConfigImpl(createImpl.GlobalImpl); err != nil {
return err
}
if err := createImpl.ValidateConfig(); err != nil {
return err
}
a := app.New(createImpl)
return toCLIError(createImpl.GlobalImpl, a.Create(createImpl))
},
}
f := cmd.Flags()
f.StringVarP(&options.OutputDir, "output-dir", "o", "", "Output directory (default: NAME or current directory)")
f.BoolVar(&options.Force, "force", false, "Overwrite existing scaffold files (helmfile.yaml, environments/default.yaml, values/.gitkeep)")
return cmd
}
+9
View File
@@ -15,6 +15,14 @@ func NewFetchCmd(globalCfg *config.GlobalImpl) *cobra.Command {
cmd := &cobra.Command{
Use: "fetch",
Short: "Fetch charts from state file",
Long: `Fetch downloads all charts referenced in the Helmfile state.
Useful for air-gapped environments: download charts with --output-dir and --write-output,
then transfer the output directory and the generated helmfile.yaml to the air-gapped environment.
The --write-output flag requires a single helmfile state file specified with -f.
It fails if the input resolves to multiple state files (e.g. a directory or a helmfile
with nested helmfiles: entries).`,
RunE: func(cmd *cobra.Command, args []string) error {
fetchImpl := config.NewFetchImpl(globalCfg, fetchOptions)
err := config.NewCLIConfigImpl(fetchImpl.GlobalImpl)
@@ -35,6 +43,7 @@ func NewFetchCmd(globalCfg *config.GlobalImpl) *cobra.Command {
f.IntVar(&fetchOptions.Concurrency, "concurrency", 0, "maximum number of concurrent helm processes to run, 0 is unlimited")
f.StringVar(&fetchOptions.OutputDir, "output-dir", "", "directory to store charts (default: temporary directory which is deleted when the command terminates)")
f.StringVar(&fetchOptions.OutputDirTemplate, "output-dir-template", state.DefaultFetchOutputDirTemplate, "go text template for generating the output directory. Available fields: {{ .OutputDir }}, {{ .ChartName }}, {{ .Release.* }}, {{ .Environment.Name }}, {{ .Environment.KubeContext }}, {{ .Environment.Values.* }}")
f.BoolVar(&fetchOptions.WriteOutput, "write-output", false, "write a helmfile.yaml to stdout with chart references updated to local chart paths; requires --output-dir and a single helmfile (use -f)")
return cmd
}
+14 -13
View File
@@ -48,6 +48,8 @@ func toCLIError(g *config.GlobalImpl, err error) error {
// NewRootCmd creates the root command for the CLI.
func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) {
globalImpl := config.NewGlobalImpl(globalConfig)
cmd := &cobra.Command{
Use: "helmfile",
Short: globalUsage,
@@ -58,11 +60,11 @@ func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) {
PersistentPreRunE: func(c *cobra.Command, args []string) error {
// Valid levels:
// https://github.com/uber-go/zap/blob/7e7e266a8dbce911a49554b945538c5b950196b8/zapcore/level.go#L126
logLevel := globalConfig.LogLevel
logLevel := globalImpl.LogLevel()
switch {
case globalConfig.Debug:
case globalImpl.Debug():
logLevel = "debug"
case globalConfig.Quiet:
case globalImpl.Quiet():
logLevel = "warn"
}
@@ -83,8 +85,6 @@ func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) {
flags.ParseErrorsAllowlist.UnknownFlags = true
globalImpl := config.NewGlobalImpl(globalConfig)
// when set environment HELMFILE_UPGRADE_NOTICE_DISABLED any value, skip upgrade notice.
var versionOpts []extension.CobraOption
if os.Getenv(envvar.UpgradeNoticeDisabled) == "" {
@@ -92,6 +92,7 @@ func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) {
}
cmd.AddCommand(
NewCreateCmd(globalImpl),
NewInitCmd(globalImpl),
NewApplyCmd(globalImpl),
NewBuildCmd(globalImpl),
@@ -120,8 +121,8 @@ func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) {
}
func setGlobalOptionsForRootCmd(fs *pflag.FlagSet, globalOptions *config.GlobalOptions) {
fs.StringVarP(&globalOptions.HelmBinary, "helm-binary", "b", app.DefaultHelmBinary, "Path to the helm binary")
fs.StringVarP(&globalOptions.KustomizeBinary, "kustomize-binary", "k", app.DefaultKustomizeBinary, "Path to the kustomize binary")
fs.StringVarP(&globalOptions.HelmBinary, "helm-binary", "b", "", fmt.Sprintf(`Path to the helm binary. Overrides "HELMFILE_HELM_BINARY" OS environment variable when specified (default %q)`, app.DefaultHelmBinary))
fs.StringVarP(&globalOptions.KustomizeBinary, "kustomize-binary", "k", "", fmt.Sprintf(`Path to the kustomize binary. Overrides "HELMFILE_KUSTOMIZE_BINARY" OS environment variable when specified (default %q)`, app.DefaultKustomizeBinary))
fs.StringVarP(&globalOptions.File, "file", "f", "", "load config from file or directory. defaults to \"`helmfile.yaml`\" or \"helmfile.yaml.gotmpl\" or \"helmfile.d\" (means \"helmfile.d/*.yaml\" or \"helmfile.d/*.yaml.gotmpl\") in this preference. Specify - to load the config from the standard input.")
fs.StringVarP(&globalOptions.Environment, "environment", "e", "", `specify the environment name. Overrides "HELMFILE_ENVIRONMENT" OS environment variable when specified. defaults to "default"`)
fs.StringArrayVar(&globalOptions.StateValuesSet, "state-values-set", nil, "set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2). Used to override .Values within the helmfile template (not values template).")
@@ -133,14 +134,14 @@ func setGlobalOptionsForRootCmd(fs *pflag.FlagSet, globalOptions *config.GlobalO
fs.BoolVar(&globalOptions.DisableForceUpdate, "disable-force-update", false, `do not force helm repos to update when executing "helm repo add" (Helm 3 only)`)
fs.BoolVar(&globalOptions.EnforcePluginVerification, "enforce-plugin-verification", false, `fail plugin installation if verification is not supported (for security purposes)`)
fs.BoolVar(&globalOptions.HelmOCIPlainHTTP, "oci-plain-http", false, `use plain HTTP for OCI registries (required for local/insecure registries in Helm 4)`)
fs.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "Silence output. Equivalent to log-level warn")
fs.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, `Silence output. Equivalent to log-level warn. Overrides "HELMFILE_QUIET" OS environment variable when specified`)
fs.StringVar(&globalOptions.Kubeconfig, "kubeconfig", "", "Use a particular kubeconfig file")
fs.StringVar(&globalOptions.KubeContext, "kube-context", "", "Set kubectl context. Uses current context by default")
fs.BoolVar(&globalOptions.Debug, "debug", false, "Enable verbose output for Helm and set log-level to debug, this disables --quiet/-q effect")
fs.StringVar(&globalOptions.KubeContext, "kube-context", "", `Set kubectl context. Overrides "HELMFILE_KUBE_CONTEXT" OS environment variable when specified. Uses current kubectl context by default`)
fs.BoolVar(&globalOptions.Debug, "debug", false, `Enable verbose output for Helm and set log-level to debug, this disables --quiet/-q effect. Overrides "HELMFILE_DEBUG" OS environment variable when specified`)
fs.BoolVar(&globalOptions.Color, "color", false, "Output with color")
fs.BoolVar(&globalOptions.NoColor, "no-color", false, "Output without color")
fs.StringVar(&globalOptions.LogLevel, "log-level", "info", "Set log level, default info")
fs.StringVarP(&globalOptions.Namespace, "namespace", "n", "", "Set namespace. Uses the namespace set in the context by default, and is available in templates as {{ .Namespace }}")
fs.BoolVar(&globalOptions.NoColor, "no-color", false, `Output without color. Overrides "HELMFILE_NO_COLOR" and "NO_COLOR" OS environment variables when specified`)
fs.StringVar(&globalOptions.LogLevel, "log-level", "", `Set log level. Overrides "HELMFILE_LOG_LEVEL" OS environment variable when specified (default "info")`)
fs.StringVarP(&globalOptions.Namespace, "namespace", "n", "", `Set namespace. Overrides "HELMFILE_NAMESPACE" OS environment variable when specified. Uses the namespace set in the context by default, and is available in templates as {{ .Namespace }}`)
fs.StringVarP(&globalOptions.Chart, "chart", "c", "", "Set chart. Uses the chart set in release by default, and is available in template as {{ .Chart }}")
fs.StringArrayVarP(&globalOptions.Selector, "selector", "l", nil, `Only run using the releases that match labels. Labels can take the form of foo=bar or foo!=bar.
A release must match all labels in a group in order to be used. Multiple groups can be specified at once.
+16
View File
@@ -57,7 +57,23 @@ func NewSyncCmd(globalCfg *config.GlobalImpl) *cobra.Command {
f.StringVar(&syncOptions.TrackMode, "track-mode", "", "Track mode for releases: 'helm' (default), 'helm-legacy' (Helm v4 only), or 'kubedog'")
f.IntVar(&syncOptions.TrackTimeout, "track-timeout", 0, `Timeout in seconds for kubedog tracking (0 to use default 300s timeout)`)
f.BoolVar(&syncOptions.TrackLogs, "track-logs", false, "Enable log streaming with kubedog tracking")
f.BoolVar(&syncOptions.TrackFailOnError, "track-fail-on-error", false, "Fail with non-zero exit code when kubedog tracking fails")
f.StringVar(&syncOptions.Description, "description", "", `Set description for all releases. If set, overrides descriptions in helmfile.yaml. Will be passed to "helm upgrade --description"`)
// Diff-related flags for --interactive mode
f.IntVar(&syncOptions.Context, "context", 0, "output NUM lines of context around changes (interactive preview only)")
f.StringVar(&syncOptions.DiffOutput, "output", "", "output format for diff plugin (interactive preview only)")
f.StringVar(&syncOptions.DiffArgs, "diff-args", "", "pass args to helm-diff (interactive preview only)")
f.StringArrayVar(&syncOptions.Suppress, "suppress", nil, "suppress specified Kubernetes objects in the diff output (interactive preview only). Can be provided multiple times. For example: --suppress KeycloakClient --suppress VaultSecret")
f.BoolVar(&syncOptions.SuppressSecrets, "suppress-secrets", false, "suppress secrets in the diff output (interactive preview only). highly recommended to specify on CI/CD use-cases")
f.BoolVar(&syncOptions.ShowSecrets, "show-secrets", false, "do not redact secret values in the diff output (interactive preview only). should be used for debug purpose only")
f.BoolVar(&syncOptions.NoHooks, "no-hooks", false, "do not diff changes made by hooks (interactive preview only)")
f.BoolVar(&syncOptions.SuppressDiff, "suppress-diff", false, "suppress diff in the output (interactive preview only). Usable in new installs")
f.BoolVar(&syncOptions.SkipDiffOnInstall, "skip-diff-on-install", false, "Skips running helm-diff on releases being newly installed on this sync (interactive preview only). Useful when the release manifests are too huge to be reviewed, or it's too time-consuming to diff at all")
f.BoolVar(&syncOptions.IncludeTests, "include-tests", false, "enable the diffing of the helm test hooks (interactive preview only)")
f.BoolVar(&syncOptions.DetailedExitcode, "detailed-exitcode", false, "return a non-zero exit code 2 instead of 0 when releases are synced (use --interactive to also see a diff preview)")
f.BoolVar(&syncOptions.StripTrailingCR, "strip-trailing-cr", false, "strip trailing carriage return on input (interactive preview only)")
f.StringArrayVar(&syncOptions.SuppressOutputLineRegex, "suppress-output-line-regex", nil, "a list of regex patterns to suppress output lines from diff output (interactive preview only)")
return cmd
}
+36 -20
View File
@@ -1,4 +1,4 @@
## Advanced Features
# Advanced Features
- [Resource Tracking with Kubedog](#resource-tracking-with-kubedog)
- [Import Configuration Parameters into Helmfile](#import-configuration-parameters-into-helmfile)
@@ -6,11 +6,11 @@
- [Adhoc Kustomization of Helm Charts](#adhoc-kustomization-of-helm-charts)
- [Adding dependencies without forking the chart](#adding-dependencies-without-forking-the-chart)
### Resource Tracking with Kubedog
## Resource Tracking with Kubedog
Helmfile can use [kubedog](https://github.com/werf/kubedog) for advanced resource tracking instead of Helm's built-in `--wait` flag. This provides more detailed feedback and control over deployment progress.
#### Basic Usage
### Basic Usage
Enable kubedog tracking in your `helmfile.yaml`:
@@ -29,13 +29,13 @@ Or use command-line flags:
helmfile apply --track-mode kubedog --track-timeout 300 --track-logs
```
#### Configuration Options
### Configuration Options
- **`trackMode`**: Set to `kubedog` to enable kubedog tracking, or `helm-legacy` to use Helm v4's legacy wait mode (default: `helm`)
- **`trackTimeout`**: Timeout in seconds for tracking resources (default: 300)
- **`trackLogs`**: Enable real-time log streaming from tracked resources
#### Track Modes
### Track Modes
Helmfile supports three track modes:
@@ -43,7 +43,7 @@ Helmfile supports three track modes:
- **`helm-legacy`**: Uses Helm v4's `--wait=legacy` flag. This is useful when migrating from Helm v3 to Helm v4 and you have charts that may have compatibility issues with the new watcher-based wait mechanism (e.g., charts with `livenessProbe` but no `startupProbe`). Note: This mode only works with Helm v4; with Helm v3 it falls back to regular `--wait`.
- **`kubedog`**: Uses kubedog for advanced resource tracking with detailed feedback
#### Resource Filtering
### Resource Filtering
Control which resources to track using whitelist/blacklist:
@@ -62,7 +62,7 @@ releases:
- Secret
```
#### Specific Resource Tracking
### Specific Resource Tracking
Track only specific resources by name and namespace:
@@ -79,7 +79,7 @@ releases:
name: myapp-job
```
#### Priority Rules
### Priority Rules
Resource filtering follows this priority (highest to lowest):
@@ -87,14 +87,14 @@ Resource filtering follows this priority (highest to lowest):
2. **`skipKinds`**: Blacklist resource kinds
3. **`trackKinds`**: Whitelist resource kinds
#### Benefits
### Benefits
- **Real-time feedback**: See deployment progress with detailed status updates
- **Log streaming**: View container logs during deployment
- **Fine-grained control**: Track only the resources you care about
- **Better debugging**: Immediate visibility into deployment issues
#### Helm v4 Legacy Wait Mode
### Helm v4 Legacy Wait Mode
When using Helm v4 with charts that have broken `livenessProbe` configurations without `startupProbe`, the default `--wait=watcher` mode may fail. Helm v4 introduces `--wait=legacy` which uses the simpler polling mechanism compatible with Helm v3's behavior.
@@ -113,16 +113,32 @@ Or via command-line:
helmfile apply --track-mode helm-legacy
```
#### Compatibility
### Compatibility
- **`helm`**: Default mode, uses Helm's built-in `--wait` flag
- **`helm-legacy`**: Uses Helm v4's `--wait=legacy` flag (only available in Helm v4)
- **`kubedog`**: Uses kubedog library for advanced resource tracking
- Kubedog tracking is compatible with Helm 3.x and 4.x
- Kubedog is a compiled dependency and is only used when `trackMode: kubedog` is set
- Works with charts that deploy supported workload kinds (currently `Deployment`, `StatefulSet`, `DaemonSet`, and `Job`); other resource kinds are created by Helm/Helmfile as usual but are ignored by the kubedog tracker
- Works with charts that deploy supported workload kinds (currently `Deployment`, `StatefulSet`, `DaemonSet`, `Job`, and `Canary`); other resource kinds are created by Helm/Helmfile as usual but are ignored by the kubedog tracker
### Import Configuration Parameters into Helmfile
### Advanced Kubedog Settings
```yaml
releases:
- name: myapp
chart: ./charts/myapp
trackMode: kubedog
trackTimeout: 600
trackLogs: true
kubedogQPS: 5.0
kubedogBurst: 10
```
- **`kubedogQPS`**: QPS (queries per second) for the kubedog kubernetes client (default: uses cluster defaults)
- **`kubedogBurst`**: Burst for the kubedog kubernetes client (default: uses cluster defaults)
## Import Configuration Parameters into Helmfile
Helmfile integrates [vals]() to import configuration parameters from following backends:
@@ -136,7 +152,7 @@ See [Vals "Supported Backends"](https://github.com/helmfile/vals#supported-backe
This feature was implemented in https://github.com/roboll/helmfile/pull/906.
If you're curious about how it's designed and how it works, please review the pull request.
### Deploy Kustomizations with Helmfile
## Deploy Kustomizations with Helmfile
You can deploy [kustomize](https://github.com/kubernetes-sigs/kustomize) "kustomization"s with Helmfile.
@@ -212,7 +228,7 @@ After all, Helmfile just installs the temporary chart like standard charts, whic
Please also see [test/advanced/helmfile.yaml](https://github.com/helmfile/helmfile/tree/master/test/advanced/helmfile.yaml) for an example of kustomization support and more.
### Adhoc Kustomization of Helm charts
## Adhoc Kustomization of Helm charts
With Helmfile's integration with Kustomize, not only deploying Kustomization as a Helm chart, you can kustomize charts before installation.
@@ -225,7 +241,7 @@ Currently, Helmfile allows you to set the following fields for kustomizing the c
- `releases[].jsonPatches`
- [`releases[].transformers`](#transformers)
#### `strategicMergePatches`
### `strategicMergePatches`
You can add/update any Kubernetes resource field rendered from a Helm chart by specifying `releases[].strategicMergePatches`:
@@ -269,7 +285,7 @@ There's also `releases[].jsonPatches` that works similarly to `strategicMergePat
Please also see [test/advanced/helmfile.yaml](https://github.com/helmfile/helmfile/tree/master/test/advanced/helmfile.yaml) for an example of patching support and more.
#### `transformers`
### `transformers`
You can set `transformers` to apply [Kustomize's transformers](https://github.com/kubernetes-sigs/kustomize/blob/master/examples/configureBuiltinPlugin.md#configuring-the-builtin-plugins-instead).
@@ -331,7 +347,7 @@ transformers:
Please see https://github.com/kubernetes-sigs/kustomize/blob/master/examples/configureBuiltinPlugin.md#configuring-the-builtin-plugins-instead for more information on how to declare transformers.
### Adding dependencies without forking the chart
## Adding dependencies without forking the chart
With Helmfile, you can add chart dependencies to a Helm chart without forking it.
@@ -418,7 +434,7 @@ dependencies:
Please read https://github.com/roboll/helmfile/issues/1762#issuecomment-816341251 for more details.
#### OCI chart dependencies
### OCI chart dependencies
With Helmfile version v0.146.0 or later, you can add OCI chart to chart dependencies.
@@ -433,7 +449,7 @@ releases:
version: 1.5
```
### Lockfile per environment
## Lockfile per environment
In some cases it can be handy for CI/CD pipelines to be able to roll out updates gradually for environments, such as staging and production while using the same
set of charts. This can be achieved by using `lockFilePath` in combination with environments, such as:
+3 -1
View File
@@ -1,4 +1,6 @@
# helmfile template built-in objects
# Built-in Objects
## helmfile template built-in objects
- `Environment`: The information about the environment. This is set by the
`--environment` flag. It has several objects inside of it:
+380
View File
@@ -0,0 +1,380 @@
# CLI Reference
## CLI Reference
```
Declaratively deploy your Kubernetes manifests, Kustomize configs, and Charts as Helm releases in one shot
V1 mode = false
YAML library = go.yaml.in/yaml/v3
Usage:
helmfile [command]
Available Commands:
apply Apply all resources from state file only when there are changes
build Build all resources from state file
create Create a helmfile deployment project scaffold
cache Cache management
charts DEPRECATED: sync releases from state file (helm upgrade --install)
completion Generate the autocompletion script for the specified shell
delete DEPRECATED: delete releases from state file (helm delete)
deps Update charts based on their requirements
destroy Destroys and then purges releases
diff Diff releases defined in state file
fetch Fetch charts from state file
help Help about any command
init Initialize the helmfile, includes version checking and installation of helm and plug-ins
lint Lint charts from state file (helm lint)
list List releases defined in state file
repos Add chart repositories defined in state file
show-dag It prints a table with 3 columns, GROUP, RELEASE, and DEPENDENCIES. GROUP is the unsigned, monotonically increasing integer starting from 1. All the releases with the same GROUP are deployed concurrently. Everything in GROUP 2 starts being deployed only after everything in GROUP 1 got successfully deployed. RELEASE is the release that belongs to the GROUP. DEPENDENCIES is the list of releases that the RELEASE depends on. It should always be empty for releases in GROUP 1. DEPENDENCIES for a release in GROUP 2 should have some or all dependencies appeared in GROUP 1. It can be "some" because Helmfile simplifies the DAGs of releases into a DAG of groups, so that Helmfile always produce a single DAG for everything written in helmfile.yaml, even when there are technically two or more independent DAGs of releases in it.
status Retrieve status of releases in state file
sync Sync releases defined in state file
template Template releases defined in state file
test Test charts from state file (helm test)
unittest Unit test charts from state file using helm-unittest plugin
version Print the CLI version
write-values Write values files for releases. Similar to `helmfile template`, write values files instead of manifests.
Flags:
--allow-no-matching-release Do not exit with an error code if the provided selector has no matching releases.
-c, --chart string Set chart. Uses the chart set in release by default, and is available in template as {{ .Chart }}
--color Output with color
--debug Enable verbose output for Helm and set log-level to debug, this disables --quiet/-q effect. Overrides "HELMFILE_DEBUG" OS environment variable when specified
--disable-force-update do not force helm repos to update when executing "helm repo add"
--enable-live-output Show live output from the Helm binary Stdout/Stderr into Helmfile own Stdout/Stderr.
It only applies for the Helm CLI commands, Stdout/Stderr for Hooks are still displayed only when it's execution finishes.
-e, --environment string specify the environment name. Overrides "HELMFILE_ENVIRONMENT" OS environment variable when specified. defaults to "default"
-f, --file helmfile.yaml load config from file or directory. defaults to "helmfile.yaml" or "helmfile.yaml.gotmpl" or "helmfile.d" (means "helmfile.d/*.yaml" or "helmfile.d/*.yaml.gotmpl") in this preference. Specify - to load the config from the standard input.
-b, --helm-binary string Path to the helm binary. Overrides "HELMFILE_HELM_BINARY" OS environment variable when specified (default "helm")
-h, --help help for helmfile
-i, --interactive Request confirmation before attempting to modify clusters
--kube-context string Set kubectl context. Overrides "HELMFILE_KUBE_CONTEXT" OS environment variable when specified. Uses current kubectl context by default
-k, --kustomize-binary string Path to the kustomize binary. Overrides "HELMFILE_KUSTOMIZE_BINARY" OS environment variable when specified (default "kustomize")
--log-level string Set log level. Overrides "HELMFILE_LOG_LEVEL" OS environment variable when specified (default "info")
-n, --namespace string Set namespace. Overrides "HELMFILE_NAMESPACE" OS environment variable when specified. Uses the namespace set in the context by default, and is available in templates as {{ .Namespace }}
--no-color Output without color. Overrides "HELMFILE_NO_COLOR" and "NO_COLOR" OS environment variables when specified
-q, --quiet Silence output. Equivalent to log-level warn. Overrides "HELMFILE_QUIET" OS environment variable when specified
-l, --selector stringArray Only run using the releases that match labels. Labels can take the form of foo=bar or foo!=bar.
A release must match all labels in a group in order to be used. Multiple groups can be specified at once.
"--selector tier=frontend,tier!=proxy --selector tier=backend" will match all frontend, non-proxy releases AND all backend releases.
The name of a release can be used as a label: "--selector name=myrelease"
--skip-deps skip running "helm repo update" and "helm dependency build"
--state-values-file stringArray specify state values in a YAML file. Used to override .Values within the helmfile template (not values template).
--state-values-set stringArray set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2). Used to override .Values within the helmfile template (not values template).
--state-values-set-string stringArray set state STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2). Used to override .Values within the helmfile template (not values template).
--sequential-helmfiles Process helmfile.d files sequentially in alphabetical order instead of in parallel
--strip-args-values-on-exit-error Strip the potential secret values of the helm command args contained in a helmfile error message (default true)
-v, --version version for helmfile
Use "helmfile [command] --help" for more information about a command.
```
**Note:** Each command has its own specific flags. Use `helmfile [command] --help` to see command-specific options. For example, `helmfile sync --help` shows operational flags like `--timeout`, `--wait`, and `--wait-for-jobs`.
### init
The `helmfile init` sub-command checks the dependencies required for helmfile operation, such as `helm`, `helm diff plugin`, `helm secrets plugin`, `helm helm-git plugin`, `helm s3 plugin`. When it does not exist or the version is too low, it can be installed automatically.
### cache
The `helmfile cache` sub-command is designed for cache management. Go-getter-backed remote file system are cached by `helmfile`. There is no TTL implemented, if you need to update the cached files or directories, you need to clean individually or run a full cleanup with `helmfile cache cleanup`
#### OCI Chart Cache
OCI charts are cached in the shared cache directory (`~/.cache/helmfile` by default, or `$HELMFILE_CACHE_HOME`). This cache is shared across all helmfile processes.
**Cache Behavior:**
- When a chart exists in the shared cache and is valid, it is reused without re-downloading
- The `--skip-refresh` flag can be used to skip checking for updates to cached charts stored in process-specific temporary directories (it does not affect charts already present in the shared cache)
- When running multiple helmfile processes in parallel (e.g., as an ArgoCD plugin), charts in the shared cache are not refreshed/deleted to prevent race conditions
**Forcing a Cache Refresh:**
To force a refresh of cached OCI charts, run:
```bash
helmfile cache cleanup
```
This will clear the shared cache, allowing the next helmfile command to re-download charts.
#### cache info
Display information about the cache directory.
#### cache cleanup
Remove all cached files from the cache directory.
### sync
The `helmfile sync` sub-command sync your cluster state as described in your `helmfile`. The default helmfile is `helmfile.yaml`, but any YAML file can be passed by specifying a `--file path/to/your/yaml/file` flag.
Under the covers, Helmfile executes `helm upgrade --install` for each `release` declared in the manifest, by optionally decrypting [secrets](#secrets) to be consumed as helm chart values. It also updates specified chart repositories and updates the
dependencies of any referenced local charts.
#### Common sync flags
* `--timeout SECONDS` - Override the default timeout for all releases in this sync operation. This takes precedence over `helmDefaults.timeout` and per-release `timeout` settings.
* `--wait` - Override the default wait behavior for all releases
* `--wait-for-jobs` - Override the default wait-for-jobs behavior for all releases
Examples:
```bash
# Override timeout for all releases to 10 minutes
helmfile sync --timeout 600
# Combine timeout with wait flags
helmfile sync --timeout 900 --wait --wait-for-jobs
# Target specific releases with custom timeout
helmfile sync --selector tier=backend --timeout 1200
```
For Helm 2.9+ you can use a username and password to authenticate to a remote repository.
### deps
The `helmfile deps` sub-command locks your helmfile state and local charts dependencies.
It basically runs `helm dependency update` on your helmfile state file and all the referenced local charts, so that you get a "lock" file per each helmfile state or local chart.
All the other `helmfile` sub-commands like `sync` use chart versions recorded in the lock files, so that e.g. untested chart versions won't suddenly get deployed to the production environment.
For example, the lock file for a helmfile state file named `helmfile.1.yaml` will be `helmfile.1.lock`. The lock file for a local chart would be `requirements.lock`, which is the same as `helm`.
The lock file can be changed using `lockFilePath` in helm state, which makes it possible to for example have a different lock file per environment via templating.
It is recommended to version-control all the lock files, so that they can be used in the production deployment pipeline for extra reproducibility.
To bring in chart updates systematically, it would also be a good idea to run `helmfile deps` regularly, test it, and then update the lock files in the version-control system.
### diff
The `helmfile diff` sub-command executes the [helm-diff](https://github.com/databus23/helm-diff) plugin across all of
the charts/releases defined in the manifest.
To supply the diff functionality Helmfile needs the [helm-diff](https://github.com/databus23/helm-diff) plugin v2.9.0+1 or greater installed. For Helm 2.3+
you should be able to simply execute `helm plugin install https://github.com/databus23/helm-diff`. For more details
please look at their [documentation](https://github.com/databus23/helm-diff#helm-diff-plugin).
### apply
The `helmfile apply` sub-command begins by executing `diff`. If `diff` finds that there is any changes, `sync` is executed. Adding `--interactive` instructs Helmfile to request your confirmation before `sync`.
An expected use-case of `apply` is to schedule it to run periodically, so that you can auto-fix skews between the desired and the current state of your apps running on Kubernetes clusters.
### destroy
The `helmfile destroy` sub-command uninstalls and purges all the releases defined in the manifests.
`helmfile --interactive destroy` instructs Helmfile to request your confirmation before actually deleting releases.
`destroy` basically runs `helm uninstall --purge` on all the targeted releases. If you don't want purging, use `helmfile delete` instead.
If `--skip-charts` flag is not set, destroy would prepare all releases, by fetching charts and templating them.
### delete (DEPRECATED)
The `helmfile delete` sub-command deletes all the releases defined in the manifests.
`helmfile --interactive delete` instructs Helmfile to request your confirmation before actually deleting releases.
Note that `delete` doesn't purge releases. So `helmfile delete && helmfile sync` results in sync failed due to that releases names are not deleted but preserved for future references. If you really want to remove releases for reuse, add `--purge` flag to run it like `helmfile delete --purge`.
If `--skip-charts` flag is not set, destroy would prepare all releases, by fetching charts and templating them.
### secrets
The `secrets` parameter in a `helmfile.yaml` causes the [helm-secrets](https://github.com/jkroepke/helm-secrets) plugin to be executed to decrypt the file.
To supply the secret functionality Helmfile needs the `helm secrets` plugin installed. For Helm 2.3+
you should be able to simply execute `helm plugin install https://github.com/jkroepke/helm-secrets
`.
### test
The `helmfile test` sub-command runs a `helm test` against specified releases in the manifest, default to all
Use `--cleanup` to delete pods upon completion.
### lint
The `helmfile lint` sub-command runs a `helm lint` across all of the charts/releases defined in the manifest. Non local charts will be fetched into a temporary folder which will be deleted once the task is completed.
### unittest
The `helmfile unittest` sub-command runs `helm unittest` (from the [helm-unittest plugin](https://github.com/helm-unittest/helm-unittest)) on releases that have `unitTests` defined. It automatically generates the final merged values files for each release and passes them to `helm unittest`.
This requires the `helm-unittest` plugin to be installed. You can install it with:
```bash
helm plugin install https://github.com/helm-unittest/helm-unittest
```
Releases without `unitTests` defined are skipped. Non-local charts will be fetched into a temporary folder which will be deleted once the task is completed.
Example helmfile configuration:
```yaml
releases:
- name: my-app
chart: ./charts/my-app
values:
- values.yaml
unitTests:
- tests
```
The `unitTests` paths are relative to the chart directory and follow helm-unittest conventions.
If a path does not contain glob characters, it is treated as a directory and `/*_test.yaml` is appended automatically.
You can also specify explicit glob patterns (e.g., `tests/**/*_test.yaml`).
Running `helmfile unittest` will:
1. Merge all values files defined for the release
2. Run `helm unittest ./charts/my-app --values <merged-values> --file tests/*_test.yaml`
You can pass additional flags:
```bash
# Run with additional values
helmfile unittest --values extra-values.yaml
# Run with --set overrides
helmfile unittest --set key=value
# Target specific releases
helmfile unittest --selector name=my-app
# Fail fast on first test failure
helmfile unittest --fail-fast
# Enable colored output (Helm 3 only; ignored on Helm 4 due to flag parsing issues)
helmfile unittest --color
# Enable verbose plugin output
helmfile unittest --debug-plugin
# Pass extra arguments to helm unittest
helmfile unittest --args "--strict"
```
### create
The `helmfile create` sub-command generates a helmfile deployment project scaffold with best-practice directory structure.
```bash
# Create a project in a new directory
helmfile create my-project
# Create a project in the current directory
helmfile create
# Specify a custom output directory
helmfile create my-project --output-dir /path/to/project
# Overwrite existing scaffold files
helmfile create my-project --force
```
This generates:
* `helmfile.yaml` — Main configuration with commented examples for repositories, environments, and releases
* `environments/default.yaml` — Default environment values file
* `values/.gitkeep` — Placeholder for release-specific value files
**Flags:**
| Flag | Default | Description |
|------|---------|-------------|
| `-o`, `--output-dir` | `""` | Output directory (defaults to NAME or current directory) |
| `--force` | false | Overwrite existing scaffold files |
The command validates the project name (no path separators, `.`, `..`, or whitespace-only names). Without `--force`, it atomically checks all target paths before writing to avoid partial scaffolds.
### fetch
The `helmfile fetch` sub-command downloads or copies local charts to a local directory for debug purpose. The local directory
must be specified with `--output-dir`.
### list
The `helmfile list` sub-command lists releases defined in the manifest. Optional `--output` flag accepts `json` to output releases in JSON format.
If `--skip-charts` flag is not set, list would prepare all releases, by fetching charts and templating them.
### version
The `helmfile version` sub-command prints the version of Helmfile.Optional `-o` flag accepts `json` `yaml` `short` to output version in JSON, YAML or short format.
default it will check for the latest version of Helmfile and print a tip if the current version is not the latest. To disable this behavior, set environment variable `HELMFILE_UPGRADE_NOTICE_DISABLED` to any non-empty value.
### show-dag
It prints a table with 3 columns, GROUP, RELEASE, and DEPENDENCIES.
GROUP is the unsigned, monotonically increasing integer starting from 1. All the releases with the same GROUP are deployed concurrently. Everything in GROUP 2 starts being deployed only after everything in GROUP 1 got successfully deployed.
RELEASE is the release that belongs to the GROUP.
DEPENDENCIES is the list of releases that the RELEASE depends on. It should always be empty for releases in GROUP 1. DEPENDENCIES for a release in GROUP 2 should have some or all dependencies appeared in GROUP 1. It can be "some" because Helmfile simplifies the DAGs of releases into a DAG of groups, so that Helmfile always produce a single DAG for everything written in helmfile.yaml, even when there are technically two or more independent DAGs of releases in it.
### print-env
The `helmfile print-env` sub-command prints the parsed environment configuration including merged values (with decrypted secrets). This is useful for debugging environment configuration.
```bash
# Print environment in YAML format (default)
helmfile print-env
# Print environment in JSON format
helmfile print-env --output json
# Print a specific environment
helmfile print-env -e production
```
### status
The `helmfile status` sub-command retrieves the status of releases in the state file by running `helm status` for each release.
### Additional CLI Flags
The following global flags are also available but not shown in the main help output:
| Flag | Default | Description |
|------|---------|-------------|
| `--kubeconfig` | `""` | Use a particular kubeconfig file |
| `--skip-refresh` | false | Skip running `helm repo update` (lighter than `--skip-deps` which also skips dependency build) |
| `--enforce-plugin-verification` | false | Fail plugin installation if verification is not supported |
| `--oci-plain-http` | false | Use plain HTTP for OCI registries (required for local/insecure registries in Helm 4) |
#### fetch flags
| Flag | Default | Description |
|------|---------|-------------|
| `--output-dir` | temp dir | Directory to store charts. If not set, a temporary directory is used and deleted when the command terminates |
| `--output-dir-template` | (default template) | Go text template for generating the output directory. Available fields: `{{ .OutputDir }}`, `{{ .ChartName }}`, `{{ .Release.* }}`, `{{ .Environment.Name }}`, `{{ .Environment.KubeContext }}`, `{{ .Environment.Values.* }}` |
| `--write-output` | false | Write a helmfile.yaml to stdout with chart references updated to point to the downloaded local chart paths. Requires `--output-dir` |
| `--concurrency` | 0 | Maximum number of concurrent helm processes to run, 0 is unlimited |
This is useful for air-gapped environments: download charts with `--output-dir` and `--write-output`, then transfer the output directory and the generated helmfile.yaml to the air-gapped environment.
#### destroy flags
| Flag | Default | Description |
|------|---------|-------------|
| `--skip-charts` | false | Don't prepare charts when destroying releases |
| `--deleteWait` | false | Override helmDefaults.wait, sets `helm uninstall --wait` |
| `--deleteTimeout` | 300 | Time in seconds to wait for helm uninstall |
| `--cascade` | background | Pass cascade to helm exec |
| `--concurrency` | 0 | Maximum number of concurrent helm processes to run, 0 is unlimited |
#### list flags
| Flag | Default | Description |
|------|---------|-------------|
| `--skip-charts` | false | Don't prepare charts when listing releases |
| `--keep-temp-dir` | false | Keep temporary directory after listing |
| `--output` | `""` | Output format: `json` for JSON output |
+529
View File
@@ -0,0 +1,529 @@
# Configuration Reference
This page is a comprehensive reference for all options available in `helmfile.yaml`.
**If you're new to Helmfile**, start with the [Getting Started](index.md#getting-started) tutorial on the home page, then read [Writing Helmfile](writing-helmfile.md) for patterns. Come back here when you need to look up a specific field.
**CAUTION**: This documentation is for the development version of Helmfile. If you are looking for the documentation for any of releases, please switch to the corresponding release tag like [v0.143.4](https://github.com/helmfile/helmfile/tree/v0.143.4).
## Quick Reference
A `helmfile.yaml` has these top-level sections:
| Section | Purpose |
|---------|---------|
| `repositories` | Helm chart repositories to use |
| `releases` | The Helm releases to deploy (the core of helmfile) |
| `helmDefaults` | Default Helm options for all releases |
| `environments` | Environment-specific values (dev, staging, prod) |
| `helmfiles` | Include other helmfile.yaml files (nesting) |
| `bases` | Shared base files merged before this helmfile |
| `values` | Default values available in templates |
| `commonLabels` | Labels applied to all releases |
| `templates` | Reusable release templates |
| `defaultInherit` | Default template(s) for all releases to inherit |
| `hooks` | Global lifecycle hooks |
| `apiVersions` / `kubeVersion` | Kubernetes version capabilities |
## Full Reference
The default name for a helmfile is `helmfile.yaml`:
```yaml
# Chart repositories used from within this state file
#
# Use `helm-s3` and `helm-git` and whatever Helm Downloader plugins
# to use repositories other than the official repository or one backend by chartmuseum.
repositories:
# To use official "stable" charts a.k.a https://github.com/helm/charts/tree/master/stable
- name: stable
url: https://charts.helm.sh/stable
# To use official "incubator" charts a.k.a https://github.com/helm/charts/tree/master/incubator
- name: incubator
url: https://charts.helm.sh/incubator
# helm-git powered repository: You can treat any Git repository as a charts repository
- name: polaris
url: git+https://github.com/reactiveops/polaris@deploy/helm?ref=master
# Advanced configuration: You can setup basic or tls auth and optionally enable helm OCI integration
- name: roboll
url: roboll.io/charts
certFile: optional_client_cert
keyFile: optional_client_key
# username is retrieved from the environment with the format <registryNameUpperCase>_USERNAME for CI usage, here ROBOLL_USERNAME
username: optional_username
# password is retrieved from the environment with the format <registryNameUpperCase>_PASSWORD for CI usage, here ROBOLL_PASSWORD
password: optional_password
oci: true
passCredentials: true
verify: true
keyring: path/to/keyring.gpg
# Advanced configuration: You can use a ca bundle to use an https repo
# with a self-signed certificate
- name: insecure
url: https://charts.my-insecure-domain.com
caFile: optional_ca_crt
# Advanced configuration: You can skip the verification of TLS for an https repo
- name: skipTLS
url: https://ss.my-insecure-domain.com
skipTLSVerify: true
# Advanced configuration: Connect to a repo served over plain http
- name: plainHTTP
url: http://just.http.domain.com
plainHttp: true
# context: kube-context # this directive is deprecated, please consider using helmDefaults.kubeContext
# Path to alternative helm binary (--helm-binary)
# Supports both Helm 3.x and Helm 4.x
helmBinary: path/to/helm
# Path to alternative kustomize binary (--kustomize-binary)
kustomizeBinary: path/to/kustomize
# Path to alternative lock file. The default is <state file name>.lock, i.e for helmfile.yaml it's helmfile.lock.
lockFilePath: path/to/lock.file
# Default values to set for args along with dedicated keys that can be set by contributors, cli args take precedence over these.
# In other words, unset values results in no flags passed to helm.
# See the helm usage (helm SUBCOMMAND -h) for more info on default values when those flags aren't provided.
helmDefaults:
kubeContext: kube-context #dedicated default key for kube-context (--kube-context)
cleanupOnFail: false #dedicated default key for helm flag --cleanup-on-fail
# additional and global args passed to helm (default "")
args:
- "--set k=v"
diffArgs:
- "--suppress-secrets"
syncArgs:
- "--labels=app.kubernetes.io/managed-by=helmfile"
# verify the chart before upgrading (only works with packaged charts not directories) (default false)
verify: true
keyring: path/to/keyring.gpg
# --skip-schema-validation flag to helm 'install', 'upgrade' and 'lint' (default false)
skipSchemaValidation: false
# wait for k8s resources via --wait. (default false)
wait: true
# DEPRECATED: waitRetries is no longer supported as the --wait-retries flag was removed from Helm.
# This configuration is ignored and preserved only for backward compatibility.
# waitRetries: 3
# if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout (default false)
waitForJobs: true
# time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks, and waits on pod/pvc/svc/deployment readiness) (default 300)
timeout: 600
# performs pods restart for the resource if applicable (default false)
recreatePods: true
# forces resource update through delete/recreate if needed (default false)
force: false
# limit the maximum number of revisions saved per release. Use 0 for no limit. (default 10)
historyMax: 10
# automatically create release namespaces if they do not exist (default true)
createNamespace: true
# if used with charts museum allows to pull unstable charts for deployment, for example: if 1.2.3 and 1.2.4-dev versions exist and set to true, 1.2.4-dev will be pulled (default false)
devel: true
# When set to `true`, skips running `helm dep up` and `helm dep build` on this release's chart.
# Useful when the chart is broken, like seen in https://github.com/roboll/helmfile/issues/1547
skipDeps: false
# If set to true, reuses the last release's values and merges them with ones provided in helmfile.
# This attribute, can be overriden in CLI with --reset/reuse-values flag of apply/sync/diff subcommands
reuseValues: false
# propagate `--post-renderer` to helmv3 template and helm install
postRenderer: "path/to/postRenderer"
# propagate `--post-renderer-args` to helmv3 template and helm install. This allows using Powershell
# scripts on Windows as a post renderer
postRendererArgs:
- PowerShell
- "-Command"
- "theScript.ps1"
# cascade `--cascade` to helmv3 delete, available values: background, foreground, or orphan, default: background
cascade: "background"
# insecureSkipTLSVerify is true if the TLS verification should be skipped when fetching remote chart
insecureSkipTLSVerify: false
# plainHttp is true if fetching the remote chart should be done using HTTP
plainHttp: false
# --wait flag for destroy/delete, if set to true, will wait until all resources are deleted before mark delete command as successful
deleteWait: false
# Timeout is the time in seconds to wait for helmfile destroy/delete (default 300)
deleteTimeout: 300
# suppressOutputLineRegex is a list of regex patterns to suppress output lines from helm diff (default []), available in helmfile v0.162.0
suppressOutputLineRegex:
- "version"
# syncReleaseLabels is a list of labels to be added to the release when syncing.
syncReleaseLabels: false
# these labels will be applied to all releases in a Helmfile. Useful in templating if you have a helmfile per environment or customer and don't want to copy the same label to each release
commonLabels:
hello: world
# The desired states of Helm releases.
#
# Helmfile runs various helm commands to converge the current state in the live cluster to the desired state defined here.
releases:
# Published chart example
- name: vault # name of this release
namespace: vault # target namespace
createNamespace: true # automatically create release namespace (default true)
labels: # Arbitrary key value pairs for filtering releases
foo: bar
chart: roboll/vault-secret-manager # the chart being installed to create this release, referenced by `repository/chart` syntax
version: ~1.24.1 # the semver of the chart. range constraint is supported
condition: vault.enabled # The values lookup key for filtering releases. Corresponds to the boolean value of `vault.enabled`, where `vault` is an arbitrary value
missingFileHandler: Warn # set to either "Error" or "Warn". "Error" instructs helmfile to fail when unable to find a values or secrets file. When "Warn", it prints the file and continues.
missingFileHandlerConfig:
# Ignores missing git branch error so that the Debug/Info/Warn handler can treat a missing branch as non-error.
# See https://github.com/helmfile/helmfile/issues/392
ignoreMissingGitBranch: true
# Values files used for rendering the chart
values:
# Value files passed via --values
- vault.yaml
# Inline values, passed via a temporary values file and --values, so that it doesn't suffer from type issues like --set
- address: https://vault.example.com
# Go template available in inline values and values files.
- image:
# The end result is more or less YAML. So do `quote` to prevent number-like strings from accidentally parsed into numbers!
# See https://github.com/roboll/helmfile/issues/608
tag: {{ requiredEnv "IMAGE_TAG" | quote }}
# Otherwise:
# tag: "{{ requiredEnv "IMAGE_TAG" }}"
# tag: !!string {{ requiredEnv "IMAGE_TAG" }}
db:
username: {{ requiredEnv "DB_USERNAME" }}
# value taken from environment variable. Quotes are necessary. Will throw an error if the environment variable is not set. $DB_PASSWORD needs to be set in the calling environment ex: export DB_PASSWORD='password1'
password: {{ requiredEnv "DB_PASSWORD" }}
proxy:
# Interpolate environment variable with a fixed string
domain: {{ requiredEnv "PLATFORM_ID" }}.my-domain.com
scheme: {{ env "SCHEME" | default "https" }}
# Use `values` whenever possible!
# `setString` translates to helm's `--set-string key=val`
setString:
# set a single array value in an array, translates to --set-string bar[0]={1,2}
- name: bar[0]
values:
- 1
- 2
# set a templated value
- name: namespace
value: {{ .Namespace }}
# `set` translates to helm's `--set key=val`, that is known to suffer from type issues like https://github.com/roboll/helmfile/issues/608
set:
# single value loaded from a local file, translates to --set-file foo.config=path/to/file
- name: foo.config
file: path/to/file
# set a single array value in an array, translates to --set bar[0]={1,2}
- name: bar[0]
values:
- 1
- 2
# set a templated value
- name: namespace
value: {{ .Namespace }}
# will attempt to decrypt it using helm-secrets plugin
secrets:
- vault_secret.yaml
# Override helmDefaults options for verify, wait, waitForJobs, timeout, recreatePods, force and reuseValues.
verify: true
keyring: path/to/keyring.gpg
# --skip-schema-validation flag to helm 'install', 'upgrade' and 'lint' (default false)
skipSchemaValidation: false
wait: true
# DEPRECATED: waitRetries is no longer supported - see documentation above
# waitRetries: 3
waitForJobs: true
timeout: 60
recreatePods: true
force: false
reuseValues: false
# set `false` to uninstall this release on sync. (default true)
installed: true
# Defines the strategy to use when updating. Possible value is:
# - "reinstallIfForbidden": Performs an uninstall before the update only if the update is forbidden (e.g., due to permission issues or conflicts).
updateStrategy: ""
# restores previous state in case of failed release (default false)
atomic: true
# when true, cleans up any new resources created during a failed release (default false)
cleanupOnFail: false
# --kube-context to be passed to helm commands
# See https://github.com/roboll/helmfile/issues/642
# (default "", which means the standard kubeconfig, either ~/kubeconfig or the file pointed by $KUBECONFIG environment variable)
kubeContext: kube-context
# passes --disable-validation to helm diff plugin, this requires diff plugin >= 3.1.2
# It may be helpful to deploy charts with helm api v1 CRDS
# https://github.com/roboll/helmfile/pull/1373
disableValidation: false
# passes --disable-validation to helm diff plugin, this requires diff plugin >= 3.1.2
# It is useful when any release contains custom resources for CRDs that is not yet installed onto the cluster.
# https://github.com/roboll/helmfile/pull/1618
disableValidationOnInstall: false
# passes --disable-openapi-validation to helm diff plugin, this requires diff plugin >= 3.1.2
# It may be helpful to deploy charts with helm api v1 CRDS
# https://github.com/roboll/helmfile/pull/1373
disableOpenAPIValidation: false
# limit the maximum number of revisions saved per release. Use 0 for no limit (default 10)
historyMax: 10
# When set to `true`, skips running `helm dep up` and `helm dep build` on this release's chart.
# Useful when the chart is broken, like seen in https://github.com/roboll/helmfile/issues/1547
skipDeps: false
# propagate `--post-renderer` to helmv3 template and helm install
postRenderer: "path/to/postRenderer"
# propagate `--post-renderer-args` to helmv3 template and helm install. This allows using Powershell
# scripts on Windows as a post renderer
postRendererArgs:
- PowerShell
- "-Command"
- "theScript.ps1"
# cascade `--cascade` to helmv3 delete, available values: background, foreground, or orphan, default: background
cascade: "background"
# insecureSkipTLSVerify is true if the TLS verification should be skipped when fetching remote chart
insecureSkipTLSVerify: false
# plainHttp is true if fetching the remote chart should be done using HTTP
plainHttp: false
# suppressDiff skip the helm diff output. Useful for charts which produces large not helpful diff, default: false
suppressDiff: false
# suppressOutputLineRegex is a list of regex patterns to suppress output lines from helm diff (default []), available in helmfile v0.162.0
suppressOutputLineRegex:
- "version"
# syncReleaseLabels is a list of labels to be added to the release when syncing.
syncReleaseLabels: false
# unitTests is a list of test file or directory paths for helm-unittest integration.
# When specified, `helmfile unittest` will run `helm unittest` with the merged values and these test paths.
# Requires the helm-unittest plugin: https://github.com/helm-unittest/helm-unittest
unitTests:
- tests/vault
# Local chart example
- name: grafana # name of this release
namespace: another # target namespace
chart: ../my-charts/grafana # the chart being installed to create this release, referenced by relative path to local helmfile
values:
- "../../my-values/grafana/values.yaml" # Values file (relative path to manifest)
- ./values/{{ requiredEnv "PLATFORM_ENV" }}/config.yaml # Values file taken from path with environment variable. $PLATFORM_ENV must be set in the calling environment.
wait: true
#
# Advanced Configuration: Nested States
#
helmfiles:
- # Path to the helmfile state file being processed BEFORE releases in this state file
path: path/to/subhelmfile.yaml
# Label selector used for filtering releases in the nested state.
# For example, `name=prometheus` in this context is equivalent to processing the nested state like
# helmfile -f path/to/subhelmfile.yaml -l name=prometheus sync
selectors:
- name=prometheus
# Override state values
values:
# Values files merged into the nested state's values
- additional.values.yaml
# One important aspect of using values here is that they first need to be defined in the values section
# of the origin helmfile, so in this example key1 needs to be in the values or environments.NAME.values of path/to/subhelmfile.yaml
# Inline state values merged into the nested state's values
- key1: val1
- # All the nested state files under `helmfiles:` is processed in the order of definition.
# So it can be used for preparation for your main `releases`. An example would be creating CRDs required by `releases` in the parent state file.
path: path/to/mycrd.helmfile.yaml
- # Terraform-module-like URL for importing a remote directory and use a file in it as a nested-state file
# The nested-state file is locally checked-out along with the remote directory containing it.
# Therefore all the local paths in the file are resolved relative to the file
path: git::https://github.com/cloudposse/helmfiles.git@releases/kiam.yaml?ref=0.40.0
- # By default git repositories aren't updated unless the ref is updated.
# Alternatively, refer to a named ref and disable the caching.
path: git::ssh://git@github.com/cloudposse/helmfiles.git@releases/kiam.yaml?ref=main&cache=false
# If set to "Error", return an error when a subhelmfile points to a
# non-existent path. The default behavior is to print a warning and continue.
missingFileHandler: Error
missingFileHandlerConfig:
# Ignores missing git branch error so that the Debug/Info/Warn handler can treat a missing branch as non-error.
# See https://github.com/helmfile/helmfile/issues/392
ignoreMissingGitBranch: true
#
# Advanced Configuration: Environments
#
# The list of environments managed by helmfile.
#
# The default is `environments: {"default": {}}` which implies:
#
# - `{{ .Environment.Name }}` evaluates to "default"
# - `{{ .Values }}` being empty
environments:
# The "default" environment is available and used when `helmfile` is run without `--environment NAME`.
default:
# Everything from the values.yaml is available via `{{ .Values.KEY }}`.
# Suppose `{"foo": {"bar": 1}}` contained in the values.yaml below,
# `{{ .Values.foo.bar }}` is evaluated to `1`.
values:
- environments/default/values.yaml
# Everything from the values.hcl in the `values` block is available via `{{ .Values.KEY }}`.
# More details in its dedicated section
- environments/default/values.hcl
# Each entry in values can be either a file path or inline values.
# The below is an example of inline values, which is merged to the `.Values`
- myChartVer: 1.0.0-dev
# Any environment other than `default` is used only when `helmfile` is run with `--environment NAME`.
# That is, the "production" env below is used when and only when it is run like `helmfile --environment production sync`.
production:
values:
- environments/production/values.yaml
- myChartVer: 1.0.0
# disable vault release processing
- vault:
enabled: false
## `secrets.yaml` is decrypted by `helm-secrets` and available via `{{ .Environment.Values.KEY }}`
secrets:
- environments/production/secrets.yaml
# Instructs helmfile to fail when unable to find a environment values file listed under `environments.NAME.values`.
#
# Possible values are "Error", "Warn", "Info", "Debug". The default is "Error".
#
# Use "Warn", "Info", or "Debug" if you want helmfile to not fail when a values file is missing, while just leaving
# a message about the missing file at the log-level.
missingFileHandler: Error
missingFileHandlerConfig:
# Ignores missing git branch error so that the Debug/Info/Warn handler can treat a missing branch as non-error.
# See https://github.com/helmfile/helmfile/issues/392
ignoreMissingGitBranch: true
# kubeContext to use for this environment
kubeContext: kube-context
#
# Advanced Configuration: Layering
#
# Helmfile merges all the "base" state files and this state file before processing.
#
# Assuming this state file is named `helmfile.yaml`, all the files are merged in the order of:
# environments.yaml <- defaults.yaml <- templates.yaml <- helmfile.yaml
bases:
- environments.yaml
- defaults.yaml
- templates.yaml
#
# Advanced Configuration: API Capabilities
#
# 'helmfile template' renders releases locally without querying an actual cluster,
# and in this case `.Capabilities.APIVersions` cannot be populated.
# When a chart queries for a specific CRD or the Kubernetes version, this can lead to unexpected results.
#
# Note that `Capabilities.KubeVersion` is deprecated in Helm 3 and `helm template` won't populate it.
# All you can do is fix your chart to respect `.Capabilities.APIVersions` instead, rather than trying to figure out
# how to set `Capabilities.KubeVersion` in Helmfile.
#
# Configure a fixed list of API versions to pass to 'helm template' via the --api-versions flag with the below:
apiVersions:
- example/v1
# Set the kubeVersion to render the chart with your desired Kubernetes version.
# The flag --kube-version was deprecated in helm v3 but it was added again.
# For further information https://github.com/helm/helm/issues/7326
kubeVersion: v1.21
```
### Additional helmDefaults fields
The following `helmDefaults` fields are also available but not shown in the example above:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enableDNS` | bool | false | Enable DNS lookups when rendering templates |
| `skipCRDs` | bool | false | Skip CRDs during installation |
| `skipRefresh` | bool | false | Skip running `helm dependency up` |
| `forceConflicts` | bool | false | Force server-side apply changes against conflicts (Helm 4 only) |
| `takeOwnership` | bool | false | Take ownership of existing resources |
| `trackMode` | string | `""` | Default tracking mode for resources. See [Advanced Features](advanced-features.md#resource-tracking-with-kubedog) |
| `disableAutoDetectedKubeVersionForDiff` | bool | false | Disable auto-detected kubeVersion being passed to helm diff |
### Additional release fields
The following per-release fields are also available:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `valuesTemplate` | list | | Like `values` but template expressions are rendered before being passed to Helm |
| `setTemplate` | list | | Like `set` but template expressions are rendered before being passed to Helm |
| `apiVersions` | list | | Per-release API versions (overrides top-level `apiVersions`) |
| `kubeVersion` | string | | Per-release kube version (overrides top-level `kubeVersion`) |
| `valuesPathPrefix` | string | | Prefix for values file paths |
| `verifyTemplate` | string | | Templated verify flag (e.g., `{{ .Values.verify \| default "false" }}`) |
| `waitTemplate` | string | | Templated wait flag |
| `installedTemplate` | string | | Templated installed flag |
| `adopt` | list | | List of resources to adopt (passes `--adopt` to Helm) |
| `forceGoGetter` | bool | false | Force go-getter URL parsing for the chart field. Useful when go-getter URL parsing fails unexpectedly |
| `forceNamespace` | string | | Force namespace on all K8s resources rendered by the chart, even when the template doesn't use `{{ .Namespace }}`. Use with caution |
| `skipRefresh` | bool | false | Per-release skip for `helm dependency up` |
| `disableAutoDetectedKubeVersionForDiff` | bool | false | Disable auto-detected kubeVersion for helm diff on this release |
| `takeOwnership` | bool | false | Take ownership of existing resources for this release |
| `forceConflicts` | bool | false | Force server-side apply against conflicts (Helm 4 only) |
| `description` | string | | Description of the release |
| `enableDNS` | bool | false | Enable DNS lookups when rendering templates |
### Release tracking fields (kubedog)
See [Advanced Features](advanced-features.md#resource-tracking-with-kubedog) for more details:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `trackMode` | string | `""` | Track mode: `helm`, `helm-legacy`, or `kubedog` |
| `trackTimeout` | int | 300 | Tracking timeout in seconds |
| `trackLogs` | bool | false | Enable real-time log streaming |
| `trackKinds` | list | | Whitelist of resource kinds to track |
| `skipKinds` | list | | Blacklist of resource kinds to skip |
| `trackResources` | list | | Specific resources to track (objects with `kind`, `name`, `namespace`) |
| `kubedogQPS` | float | | QPS for kubedog kubernetes client |
| `kubedogBurst` | int | | Burst for kubedog kubernetes client |
### Hook kubectlApply
Hooks also support a `kubectlApply` field for running `kubectl apply` directly:
```yaml
releases:
- name: myapp
chart: mychart
hooks:
- events: ["presync"]
showlogs: true
kubectlApply:
filename: manifests/my-resource.yaml
```
Or with kustomize:
```yaml
hooks:
- events: ["presync"]
showlogs: true
kubectlApply:
kustomize: overlays/default/
```
### Repository additional fields
| Field | Type | Description |
|-------|------|-------------|
| `registryConfig` | string | Path to registry configuration file |
| `managed` | string | Managed repository mode |
### Template Partials
Files matching `_*.tpl` in the same directory as the helmfile are automatically loaded as helper templates. For example, a file named `_helpers.tpl` can define named templates that are reusable across your helmfile:
`_helpers.tpl`:
```
{{- define "myapp.labels" -}}
app: myapp
env: {{ .Environment.Name }}
{{- end -}}
```
`helmfile.yaml`:
```yaml
releases:
- name: myapp
chart: mychart
values:
- labels: {{ include "myapp.labels" . | toYaml | nindent 4 }}
```
+347
View File
@@ -0,0 +1,347 @@
# Environments
## Environment
When you want to customize the contents of `helmfile.yaml` or `values.yaml` files per environment, use this feature.
You can define as many environments as you want under `environments` in `helmfile.yaml`.
`environments` section should be separated from `releases` with `---`.
The environment name defaults to `default`, that is, `helmfile sync` implies the `default` environment.
The selected environment name can be referenced from `helmfile.yaml` and `values.yaml.gotmpl` by `{{ .Environment.Name }}`.
If you want to specify a non-default environment, provide a `--environment NAME` flag to `helmfile` like `helmfile --environment production sync`.
The below example shows how to define a production-only release:
```yaml
environments:
default:
production:
---
releases:
- name: newrelic-agent
installed: {{ eq .Environment.Name "production" | toYaml }}
# snip
- name: myapp
# snip
```
### Environment Values
Helmfile supports 3 values languages :
- Straight yaml
- Go templates to generate straight yaml
- HCL
Environment Values allows you to inject a set of values specific to the selected environment, into `values.yaml` templates.
Use it to inject common values from the environment to multiple values files, to make your configuration DRY.
Suppose you have three files `helmfile.yaml`, `production.yaml` and `values.yaml.gotmpl`:
`helmfile.yaml`
```yaml
environments:
production:
values:
- production.yaml
---
releases:
- name: myapp
values:
- values.yaml.gotmpl
```
`production.yaml`
```yaml
domain: prod.example.com
releaseName: prod
```
`values.yaml.gotmpl`
```yaml
domain: {{ .Values | get "domain" "dev.example.com" }}
```
`helmfile sync` installs `myapp` with the value `domain=dev.example.com`,
whereas `helmfile --environment production sync` installs the app with the value `domain=prod.example.com`.
For even more flexibility, you can now use values declared in the `environments:` section in other parts of your helmfiles:
consider:
`default.yaml`
```yaml
domain: dev.example.com
releaseName: dev
```
```yaml
environments:
default:
values:
- default.yaml
production:
values:
- production.yaml # bare .yaml file, content will be used verbatim
- other.yaml.gotmpl # template directives with potential side-effects like `exec` and `readFile` will be honoured
---
releases:
- name: myapp-{{ .Values.releaseName }} # release name will be one of `dev` or `prod` depending on selected environment
values:
- values.yaml.gotmpl
- name: production-specific-release
# this release would be installed only if selected environment is `production`
installed: {{ eq .Values.releaseName "prod" | toYaml }}
...
```
#### Merge strategy
By default, when several files are listed under `values:`, later files override earlier files. Set `mergeStrategy: fallback` on the environment to flip the precedence so earlier files win and later files only fill missing keys:
```yaml
environments:
production:
mergeStrategy: fallback
values:
- cluster-specific.yaml # wins on every key it defines
- shared-defaults.yaml.gotmpl
```
Under `fallback`, explicit non-nil values in the earlier file (including zero values like `false`, `0`, `""`, and empty list) are preserved against any later file, while maps are deep-merged so later files may still add nested keys. A later `.gotmpl` file can also reference values from earlier files via `.Values`. See [Merge Strategy: override vs fallback](values-and-merging.md#4a-merge-strategy-override-vs-fallback) for the full semantics, including how explicit `null` is handled.
#### HCL specifications
Since Helmfile v0.164.0, HCL language is supported for environment values only.
HCL values supports interpolations and sharing values across files
* Only `.hcl` suffixed files will be interpreted as is
* Helmfile supports 2 different blocks: `values` and `locals`
* `values` block is a shared block where all values are accessible everywhere in all loaded files
* `locals` block can't reference external values apart from the ones in the block itself, and where its defined values are only accessible in its local file
* Only values in `values` blocks are made available to the final root `.Values` (e.g : ` values { myvar = "var" }` is accessed through `{{ .Values.myvar }}`)
* There can only be 1 `locals` block per file
* Helmfile hcl `values` are referenced using the `hv` accessor.
* Helmfile hcl `locals` are referenced using the `local` accessor.
* When the same key is defined multiple times across imported `.hcl` files in `values` blocks, values from later files override those from earlier files (last file loaded wins). Map values are merged per key, while list values are replaced as a whole (i.e. not deep-merged). Mixed-types overrides (e.g. bool -> string) are supported (latest value/type wins).
* All cty [standard library functions](`https://pkg.go.dev/github.com/zclconf/go-cty@v1.14.3/cty/function/stdlib`) are available and custom functions could be created in the future
Consider the following example :
```terraform
# values1.hcl
locals {
hostname = "host1"
}
values {
domain = "DEV.EXAMPLE.COM"
hostnameV1 = "${local.hostname}.${lower(hv.domain)}" # "host1.dev.example.com"
}
```
```terraform
# values2.hcl
locals {
hostname = "host2"
}
values {
hostnameV2 = "${local.hostname}.${hv.domain}" # "host2.DEV.EXAMPLE.COM"
}
```
#### Note on Environment.Values vs Values
The `{{ .Values.foo }}` syntax is the recommended way of using environment values.
Prior to this [pull request](https://github.com/roboll/helmfile/pull/647), environment values were made available through the `{{ .Environment.Values.foo }}` syntax.
This is still working but is **deprecated** and the new `{{ .Values.foo }}` syntax should be used instead.
You can read more infos about the feature proposal [here](https://github.com/roboll/helmfile/issues/640).
### Environment Secrets
Environment Secrets *(not to be confused with Kubernetes Secrets)* are encrypted versions of `Environment Values`.
You can list any number of `secrets.yaml` files created using `helm secrets` or `sops`, so that
Helmfile could automatically decrypt and merge the secrets into the environment values.
First you must have the [helm-secrets](https://github.com/jkroepke/helm-secrets) plugin installed along with a
`.sops.yaml` file to configure the method of encryption (this can be in the same directory as your helmfile or
in the subdirectory containing your secrets files).
Then suppose you have a secret `foo.bar` defined in `environments/production/secrets.yaml`:
```yaml
foo.bar: "mysupersecretstring"
```
You can then encrypt it with `helm secrets enc environments/production/secrets.yaml`
Then reference that encrypted file in `helmfile.yaml`:
```yaml
environments:
production:
secrets:
- environments/production/secrets.yaml
---
releases:
- name: myapp
chart: mychart
values:
- values.yaml.gotmpl
```
Then the environment secret `foo.bar` can be referenced by the below template expression in your `values.yaml.gotmpl`:
```yaml
{{ .Values.foo.bar }}
```
#### Loading remote Environment secrets files
Since Helmfile v0.149.0, you can use `go-getter`-style URLs to refer to remote secrets files, the same way as in values files:
```yaml
environments:
staging:
secrets:
- git::https://{{ env "GITHUB_PAT" }}@github.com/org/repo.git@/environments/staging.secret.yaml?ref=main
- http://$HOSTNAME/artifactory/example-repo-local/test.tgz@environments/staging.secret.yaml
production:
secrets:
- git::https://{{ env "GITHUB_PAT" }}@github.com/org/repo.git@/environments/production.secret.yaml?ref=main
- http://$HOSTNAME/artifactory/example-repo-local/test.tgz@environments/production.secret.yaml
```
### Loading remote Environment values files
Since Helmfile v0.118.8, you can use `go-getter`-style URLs to refer to remote values files:
```yaml
environments:
cluster-azure-us-west:
values:
- git::https://git.company.org/helmfiles/global/azure.yaml?ref=master
- git::https://git.company.org/helmfiles/global/us-west.yaml?ref=master
- git::https://gitlab.com/org/repository-name.git@/config/config.test.yaml?ref=main # Public Gilab Repo
cluster-gcp-europe-west:
values:
- git::https://git.company.org/helmfiles/global/gcp.yaml?ref=master
- git::https://git.company.org/helmfiles/global/europe-west.yaml?ref=master
- git::https://ci:{{ env "CI_JOB_TOKEN" }}@gitlab.com/org/repository-name.git@/config.dev.yaml?ref={{ env "APP_COMMIT_SHA" }} # Private Gitlab Repo
staging:
values:
- git::https://{{ env "GITHUB_PAT" }}@github.com/[$GITHUB_ORGorGITHUB_USER]/repository-name.git@/values.dev.yaml?ref=main #Github Private repo
- http://$HOSTNAME/artifactory/example-repo-local/test.tgz@values.yaml #Artifactory url
---
releases:
- ...
```
Since Helmfile v0.158.0, support more protocols, such as: s3, https, http
```
values:
- s3::https://helm-s3-values-example.s3.us-east-2.amazonaws.com/values.yaml
- s3://helm-s3-values-example/subdir/values.yaml
- https://john:doe@helm-s3-values-example.s3.us-east-2.amazonaws.com/values.yaml
- http://helm-s3-values-example.s3.us-east-2.amazonaws.com/values.yaml
```
For more information about the supported protocols see: [go-getter Protocol-Specific Options](https://github.com/hashicorp/go-getter#protocol-specific-options-1).
This is particularly useful when you co-locate helmfiles within your project repo but want to reuse the definitions in a global repo.
### Environment values precedence
With the introduction of HCL, a new value precedence was introduced over environment values.
Here is the order of precedence from least to greatest (the last one overrides all others)
1. `yaml` / `yaml.gotmpl`
2. `hcl`
3. `yaml` secrets
Example:
---
```yaml
# values1.yaml
domain: "dev.example.com"
```
```terraform
# values2.hcl
values {
domain = "overdev.example.com"
env = "dev"
willBeOverridden = "override_me"
}
```
```terraform
# values3.hcl
values {
env = "local"
}
```
```yaml
# secrets.yml (assuming this one has been encrypted)
willBeOverridden: overridden
```
```
# helmfile.yaml.gotmpl
environments:
default:
values:
- values1.yaml
- values2.hcl
- values3.hcl
secrets:
- secrets.yml
---
releases:
- name: random-release
[...]
values:
domain: "{{ .Values.domain }}" # == "overdev.example.com"
env: "{{ .Values.env }}" # == "local"
willBeOverridden: "{{ .Values.willBeOverridden }}" # == "overridden"
```
### Environment defaults
In addition to `values`, environments support a `defaults` block that provides a separate layer of default values. These are merged **before** `values`, giving `values` higher priority:
```yaml
environments:
default:
defaults:
- cluster: dev
replicas: 1
values:
- replicas: 3
```
The merge order for environment values is:
```
┌─────────────────────────────────────────────────────────────────┐
│ 1. Environment defaults (merged first, lowest priority) │
│ 2. Environment values (yaml/yaml.gotmpl) │
│ 3. Environment values (HCL) │
│ 4. Environment secrets (non-HCL, decrypted) │
│ 5. CLI overrides (--state-values-set, --state-values-file) │
└─────────────────────────────────────────────────────────────────┘
```
In the example above, `{{ .Values.replicas }}` would be `3` (values overrides defaults) and `{{ .Values.cluster }}` would be `dev` (only defined in defaults).
+25 -2
View File
@@ -1,7 +1,30 @@
# Experimental Features
This document describes the experimental features that are available in Helmfile v1.
This document describes the experimental features that are available in Helmfile.
Any experimental feature may be removed or changed in a future release without notice.
- HCL helmfile-values-file support (PR #1423)
Enable experimental features with the environment variable:
```bash
# Enable all experimental features
export HELMFILE_EXPERIMENTAL=true
# Enable a specific feature
export HELMFILE_EXPERIMENTAL=explicit-selector-inheritance
```
## explicit-selector-inheritance
By default, CLI selectors (e.g., `helmfile -l name=myapp sync`) are inherited by sub-helmfiles. This experimental feature changes the behavior so that sub-helmfiles without explicit `selectors` do **not** inherit selectors from their parent or the CLI.
When enabled:
* Sub-helmfiles without `selectors` do not inherit parent/CLI selectors
* Use `selectorsInherited: true` on a sub-helmfile to explicitly opt into inheriting selectors
* `selectors: []` selects all releases (same as current behavior)
See [Selectors and needs](releases.md#selectors) for detailed examples.
## HCL helmfile-values-file support
HCL language is supported for environment values files (`.hcl` suffix). This was introduced as experimental in PR #1423 and is now a stable feature. See [Environments](environments.md#hcl-specifications) for details.
+294
View File
@@ -0,0 +1,294 @@
# Hooks
## Hooks
A Helmfile hook is a per-release extension point that is composed of:
* `events`
* `command`
* `args`
* `showlogs`
* `kubectlApply` (alternative to `command`/`args`)
Helmfile triggers various `events` while it is running.
Once `events` are triggered, associated `hooks` are executed, by running the `command` with `args`. The standard output of the `command` will be displayed if `showlogs` is set and it's value is `true`.
Hooks exec order follows the order of definition in the helmfile state.
Currently supported `events` are:
* `prepare`
* `preapply`
* `presync`
* `preuninstall`
* `postuninstall`
* `postsync`
* `cleanup`
Hooks associated to `prepare` events are triggered after each release in your helmfile is loaded from YAML, before execution.
`prepare` hooks are triggered on the release as long as it is not excluded by the helmfile selector(e.g. `helmfile -l key=value`).
Hooks associated to `presync` events are triggered before each release is synced (installed or upgraded) on the cluster.
This is the ideal event to execute any commands that may mutate the cluster state as it will not be run for read-only operations like `lint`, `diff` or `template`.
`preapply` hooks are triggered before a release is uninstalled, installed, or upgraded as part of `helmfile apply`.
This is the ideal event to hook into when you are going to use `helmfile apply` for every kind of change. Note that preapply hooks will only run if at least one release has changes to apply. Be sure to make each `preapply` hook command idempotent. Otherwise, rerunning `helmfile apply` on a transient failure may end up either breaking your cluster, or the hook that runs for the second time will never succeed.
`preuninstall` hooks are triggered immediately before a release is uninstalled as part of `helmfile apply`, `helmfile sync`, `helmfile delete`, and `helmfile destroy`.
`postuninstall` hooks are triggered immediately after successful uninstall of a release while running `helmfile apply`, `helmfile sync`, `helmfile delete`, `helmfile destroy`.
`postsync` hooks are triggered after each release is synced (installed or upgraded) on the cluster, regardless if the sync was successful or not.
This is the ideal place to execute any commands that may mutate the cluster state as it will not be run for read-only operations like `lint`, `diff` or `template`.
`cleanup` hooks are triggered after each release is processed.
This is the counterpart to `prepare`, as any release on which `prepare` has been triggered gets `cleanup` triggered as well.
The following is an example hook that just prints the contextual information provided to hook:
```yaml
releases:
- name: myapp
chart: mychart
# *snip*
hooks:
- events: ["prepare", "cleanup"]
showlogs: true
command: "echo"
args: ["{{`{{.Environment.Name}}`}}", "{{`{{.Release.Name}}`}}", "{{`{{.HelmfileCommand}}`}}\
"]
```
Let's say you ran `helmfile --environment prod sync`, the above hook results in executing:
```
echo {{Environment.Name}} {{.Release.Name}} {{.HelmfileCommand}}
```
Whereas the template expressions are executed thus the command becomes:
```
echo prod myapp sync
```
Now, replace `echo` with any command you like, and rewrite `args` that actually conforms to the command, so that you can integrate any command that does:
* templating
* linting
* testing
Hooks expose additional template expressions:
`.Event.Name` is the name of the hook event.
`.Event.Error` is the error generated by a failed release, exposed for `postsync` hooks only when a release fails, otherwise its value is `nil`.
You can use the hooks event expressions to send notifications to platforms such as `Slack`, `MS Teams`, etc.
The following example passes arguments to a script which sends a notification:
```yaml
releases:
- name: myapp
chart: mychart
# *snip*
hooks:
- events:
- presync
- postsync
showlogs: true
command: notify.sh
args:
- --event
- '{{`{{ .Event.Name }}`}}'
- --status
- '{{`{{ if .Event.Error }}failure{{ else }}success{{ end }}`}}'
- --environment
- '{{`{{ .Environment.Name }}`}}'
- --namespace
- '{{`{{ .Release.Namespace }}`}}'
- --release
- '{{`{{ .Release.Name }}`}}'
```
For templating, imagine that you created a hook that generates a helm chart on-the-fly by running an external tool like ksonnet, kustomize, or your own template engine.
It will allow you to write your helm releases with any language you like, while still leveraging goodies provided by helm.
### Hooks, Kubectl and Environments
Hooks can also be used in combination with small tasks using `kubectl` directly,
e.g.: in order to install a custom storage class.
In the following example, a specific release depends on a custom storage class.
Further, all enviroments have a default kube context configured where releases are deployed into.
The `.Environment.KubeContext` is used in order to apply / remove the YAML to the correct context depending on the environment.
`environments.yaml`:
```yaml
environments:
dev:
values:
- ../values/default.yaml
- ../values/dev.yaml
kubeContext: dev-cluster
prod:
values:
- ../values/default.yaml
- ../values/prod.yaml
kubeContext: prod-cluster
```
`helmfile.yaml`:
```yaml
bases:
- ./environments.yaml
---
releases:
- name: myService
namespace: my-ns
installed: true
chart: mychart
version: "1.2.3"
values:
- ../services/my-service/values.yaml.gotmpl
hooks:
- events: ["presync"]
showlogs: true
command: "kubectl"
args:
- "apply"
- "-f"
- "./custom-storage-class.yaml"
- "--context"
- "{{`{{.Environment.KubeContext}}`}}"
- events: ["postuninstall"]
showlogs: true
command: "kubectl"
args:
- "delete"
- "-f"
- "./custom-storage-class.yaml"
- "--context"
- "{{`{{.Environment.KubeContext}}`}}"
```
### Global Hooks
In contrast to the per release hooks mentioned above these are run only once at the very beginning and end of the execution of a helmfile command and only the `prepare` and `cleanup` hooks are available respectively.
They use the same syntax as per release hooks, but at the top level of your helmfile:
```yaml
hooks:
- events: ["prepare", "cleanup"]
showlogs: true
command: "echo"
args: ["{{`{{.Environment.Name}}`}}", "{{`{{.HelmfileCommand}}`}}\
"]
```
### Helmfile + Kustomize
Do you prefer `kustomize` to write and organize your Kubernetes apps, but still want to leverage helm's useful features
like rollback, history, and so on? This section is for you!
The combination of `hooks` and [helmify-kustomize](https://gist.github.com/mumoshu/f9d0bd98e0eb77f636f79fc2fb130690)
enables you to integrate [kustomize](https://github.com/kubernetes-sigs/kustomize) into Helmfile.
That is, you can use `kustomize` to build a local helm chart from a kustomize overlay.
Let's assume you have a kustomize project named `foo-kustomize` like this:
```
foo-kustomize/
├── base
│   ├── configMap.yaml
│   ├── deployment.yaml
│   ├── kustomization.yaml
│   └── service.yaml
└── overlays
├── default
│   ├── kustomization.yaml
│   └── map.yaml
├── production
│   ├── deployment.yaml
│   └── kustomization.yaml
└── staging
├── kustomization.yaml
└── map.yaml
5 directories, 10 files
```
Write `helmfile.yaml`:
```yaml
- name: kustomize
chart: ./foo
hooks:
- events: ["prepare", "cleanup"]
command: "../helmify"
args: ["{{`{{if eq .Event.Name \"prepare\"}}build{{else}}clean{{end}}`}}", "{{`{{.Release.Ch\
art}}`}}", "{{`{{.Environment.Name}}`}}"]
```
Run `helmfile --environment staging sync` and see it results in helmfile running `kustomize build foo-kustomize/overlays/staging > foo/templates/all.yaml`.
Voilà! You can mix helm releases that are backed by remote charts, local charts, and even kustomize overlays.
### kubectlApply Hook
Instead of specifying `command` and `args`, you can use the `kubectlApply` field to run `kubectl apply` directly:
```yaml
releases:
- name: myapp
chart: mychart
hooks:
- events: ["presync"]
showlogs: true
kubectlApply:
filename: manifests/custom-resource.yaml
```
Or apply a kustomize overlay:
```yaml
hooks:
- events: ["presync"]
showlogs: true
kubectlApply:
kustomize: overlays/default/
```
The `kubectlApply` field accepts either:
* `filename:` - runs `kubectl apply -f <value>`
* `kustomize:` - runs `kubectl apply -k <value>`
**Note:** `filename` and `kustomize` cannot be used together. When `kubectlApply` is set, the `command` field is ignored with a warning.
### Hook Template Data
Hooks have access to the following template data:
Per-release hooks:
* `{{ .Environment.Name }}` - the environment name
* `{{ .Environment.KubeContext }}` - the environment kube context
* `{{ .Release.Name }}` - the release name
* `{{ .Release.Namespace }}` - the release namespace
* `{{ .Release.Labels }}` - the release labels
* `{{ .Release.Chart }}` - the release chart
* `{{ .Values }}` - state values
* `{{ .HelmfileCommand }}` - the helmfile command name (e.g., `sync`, `apply`)
* `{{ .Event.Name }}` - the hook event name
* `{{ .Event.Error }}` - the error (available in `postsync` hooks when a release fails)
Global hooks:
* `{{ .Environment.Name }}` - the environment name
* `{{ .HelmfileCommand }}` - the helmfile command name
* `{{ .Event.Name }}` - the hook event name
* `{{ .Event.Error }}` - the error
+99 -1814
View File
File diff suppressed because it is too large Load Diff
+136
View File
@@ -0,0 +1,136 @@
# Integrations
## Integrations
* [renovate](https://github.com/renovatebot/renovate) automates chart version updates. See [this PR for more information](https://github.com/renovatebot/renovate/pull/5257).
* For updating container image tags and git tags embedded within helmfile.yaml and values, you can use [renovate's regexManager](https://docs.renovatebot.com/modules/manager/regex/). Please see [this comment in the renovate repository](https://github.com/renovatebot/renovate/issues/6130#issuecomment-624061289) for more information.
* [ArgoCD Integration](#argocd-integration)
* [Azure ACR Integration](#azure-acr-integration)
### ArgoCD Integration
Use [ArgoCD](https://argoproj.github.io/argo-cd/) with `helmfile template` for GitOps.
ArgoCD has support for kustomize/manifests/helm chart by itself. Why bother with Helmfile?
The reasons may vary:
1. You do want to manage applications with ArgoCD, while letting Helmfile manage infrastructure-related components like Calico/Cilium/WeaveNet, Linkerd/Istio, and ArgoCD itself.
* This way, any application deployed by ArgoCD has access to all the infrastructure.
* Of course, you can use ArgoCD's [Sync Waves and Phases](https://argoproj.github.io/argo-cd/user-guide/sync-waves/) for ordering the infrastructure and application installations. But it may be difficult to separate the concern between the infrastructure and apps and annotate K8s resources consistently when you have different teams for managing infra and apps.
2. You want to review the exact K8s manifests being applied on pull-request time, before ArgoCD syncs.
* This is often better than using a kind of `HelmRelease` custom resources that obfuscates exactly what manifests are being applied, which makes reviewing harder.
3. Use Helmfile as the single-pane of glass for all the K8s resources deployed to your cluster(s).
* Helmfile can reduce repetition in K8s manifests across ArgoCD application
For 1, you run `helmfile apply` on CI to deploy ArgoCD and the infrastructure components.
> helmfile config for this phase often reside within the same directory as your Terraform project. So connecting the two with [terraform-provider-helmfile](https://github.com/mumoshu/terraform-provider-helmfile) may be helpful
For 2, another app-centric CI or bot should render/commit manifests by running:
```
helmfile template --output-dir-template $(pwd)/gitops//{{.Release.Name}}
cd gitops
git add .
git commit -m 'some message'
git push origin $BRANCH
```
> Note that `$(pwd)` is necessary when `helmfile.yaml` has one or more sub-helmfiles in nested directories,
> because setting a relative file path in `--output-dir` or `--output-dir-template` results in each sub-helmfile render
> to the directory relative to the specified path.
so that they can be deployed by Argo CD as usual.
The CI or bot can optionally submit a PR to be review by human, running:
```
hub pull-request -b main -l gitops -m 'some description'
```
Recommendations:
* Do create ArgoCD `Application` custom resource per Helm/Helmfile release, each point to respective sub-directory generated by `helmfile template --output-dir-template`
* If you don't directly push it to the main Git branch and instead go through a pull-request, do lint rendered manifests on your CI, so that you can catch easy mistakes earlier/before ArgoCD finally deploys it
* See [this ArgoCD issue](https://github.com/argoproj/argo-cd/issues/2143#issuecomment-570478329) for why you may want this, and see [this helmfile issue](https://github.com/roboll/helmfile/pull/1357) for how `--output-dir-template` works.
### Azure ACR Integration
Azure offers helm repository [support for Azure Container Registry](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-helm-repos) as a preview feature.
To use this you must first `az login` and then `az acr helm repo add -n <MyRegistry>`. This will extract a token for the given ACR and configure `helm` to use it, e.g. `helm repo update` should work straight away.
To use `helmfile` with ACR, on the other hand, you must either include a username/password in the repository definition for the ACR in your `helmfile.yaml` or use the `--skip-deps` switch, e.g. `helmfile template --skip-deps`.
An ACR repository definition in `helmfile.yaml` looks like this:
```yaml
repositories:
- name: <MyRegistry>
url: https://<MyRegistry>.azurecr.io/helm/v1/repo
```
## OCI Registries
In order to use OCI chart registries firstly they must be marked in the repository list as OCI enabled, e.g.
```yaml
repositories:
- name: myOCIRegistry
url: myregistry.azurecr.io
oci: true
```
It is important not to include a scheme for the URL as helm requires that these are not present for OCI registries
Secondly the credentials for the OCI registry can either be specified within `helmfile.yaml` similar to
```yaml
repositories:
- name: myOCIRegistry
url: myregistry.azurecr.io
oci: true
username: spongebob
password: squarepants
```
or for CI scenarios these can be sourced from the environment with the format `<registryName>_USERNAME` and `<registryName_PASSWORD>`, e.g.
```shell
export MYOCIREGISTRY_USERNAME=spongebob
export MYOCIREGISTRY_PASSWORD=squarepants
```
If `<registryName>` contains hyphens, the environment variable to be read is the hyphen replaced by an underscore., e.g.
```yaml
repositories:
- name: my-oci-registry
url: myregistry.azurecr.io
oci: true
```
```shell
export MY_OCI_REGISTRY_USERNAME=spongebob
export MY_OCI_REGISTRY_PASSWORD=squarepants
```
### OCI Chart Caching
OCI charts are automatically cached in the shared cache directory (`~/.cache/helmfile` by default, or the directory specified by `HELMFILE_CACHE_HOME`). This improves performance by avoiding redundant downloads.
**Multi-Process Safety:** When running multiple helmfile processes in parallel (e.g., as an ArgoCD plugin), charts in the shared cache are not deleted or refreshed to prevent race conditions where one process might delete a chart that another is using. To force a cache refresh, run `helmfile cache cleanup` first.
See the [cache](cli.md#cache) section for more details on cache management.
## Attribution
We use:
* [semtag](https://github.com/pnikosis/semtag) for automated semver tagging. I greatly appreciate the author(pnikosis)'s effort on creating it and their kindness to share it!
+188
View File
@@ -0,0 +1,188 @@
# Releases & DAG
## DAG-aware installation/deletion ordering with `needs`
`needs` controls the order of the installation/deletion of the release:
```yaml
releases:
- name: somerelease
needs:
- [[KUBECONTEXT/]NAMESPACE/]anotherelease
```
Be aware that you have to specify the kubecontext and namespace name if you configured one for the release(s).
All the releases listed under `needs` are installed before(or deleted after) the release itself.
For the following example, `helmfile [sync|apply]` installs releases in this order:
1. logging
2. servicemesh
3. myapp1 and myapp2
```yaml
- name: myapp1
chart: charts/myapp
needs:
- servicemesh
- logging
- name: myapp2
chart: charts/myapp
needs:
- servicemesh
- logging
- name: servicemesh
chart: charts/istio
needs:
- logging
- name: logging
chart: charts/fluentd
```
Note that all the releases in a same group is installed concurrently. That is, myapp1 and myapp2 are installed concurrently.
On `helmfile [delete|destroy]`, deletions happen in the reverse order.
That is, `myapp1` and `myapp2` are deleted first, then `servicemesh`, and finally `logging`.
### Selectors and `needs`
When using selectors/labels, `needs` are ignored by default. This behaviour can be overruled with a few parameters:
| Parameter | default | Description |
|---|---|---|
| `--skip-needs` | `true` | `needs` are ignored (default behavior). |
| `--include-needs` | `false` | The direct `needs` of the selected release(s) will be included. |
| `--include-transitive-needs` | `false` | The direct and transitive `needs` of the selected release(s) will be included. |
Let's look at an example to illustrate how the different parameters work:
```yaml
releases:
- name: serviceA
chart: my/chart
needs:
- serviceB
- name: serviceB
chart: your/chart
needs:
- serviceC
- name: serviceC
chart: her/chart
- name: serviceD
chart: his/chart
```
| Command | Included Releases Order | Explanation |
|---|---|---|
| `helmfile -l name=serviceA sync` | - `serviceA` | By default no needs are included. |
| `helmfile -l name=serviceA sync --include-needs` | - `serviceB`<br>- `serviceA` | `serviceB` is now part of the release as it is a direct need of `serviceA`. |
| `helmfile -l name=serviceA sync --include-transitive-needs` | - `serviceC`<br>- `serviceB`<br>- `serviceA` | `serviceC` is now also part of the release as it is a direct need of `serviceB` and therefore a transitive need of `serviceA`. |
Note that `--include-transitive-needs` will override any potential exclusions done by selectors or conditions. So even if you explicitly exclude a release via a selector it will still be part of the deployment in case it is a direct or transitive need of any of the specified releases.
## Separating helmfile.yaml into multiple independent files
Once your `helmfile.yaml` got to contain too many releases,
split it into multiple yaml files.
Recommended granularity of helmfile.yaml files is "per microservice" or "per team".
And there are two ways to organize your files.
* Single directory
* Glob patterns
### Single directory
`helmfile -f path/to/directory` loads and runs all the yaml files under the specified directory, each file as an independent helmfile.yaml.
The default helmfile directory is `helmfile.d`, that is,
in case helmfile is unable to locate `helmfile.yaml`, it tries to locate `helmfile.d/*.yaml`.
By default, multiple files in `helmfile.d` are processed in **parallel** for better performance. If you need files to be processed **sequentially in alphabetical order** (e.g., for dependency ordering where databases must be deployed before applications), use the `--sequential-helmfiles` flag.
For example, you can use a `<two digit number>-<microservice>.yaml` naming convention to control the sync order when using `--sequential-helmfiles`:
* `helmfile.d`/
* `00-database.yaml`
* `01-backend.yaml`
* `02-frontend.yaml`
```bash
# Process files sequentially in alphabetical order
helmfile --sequential-helmfiles sync
```
> **Note:** When processing multiple helmfile.d files, both parallel and sequential modes resolve paths without changing the process working directory, so relative environment variables like `KUBECONFIG` work correctly.
### Glob patterns
In case you want more control over how multiple `helmfile.yaml` files are organized, use `helmfiles:` configuration key in the `helmfile.yaml`:
Suppose you have multiple microservices organized in a Git repository that looks like:
* `myteam/` (sometimes it is equivalent to a k8s ns, that is `kube-system` for `clusterops` team)
* `apps/`
* `filebeat/`
* `helmfile.yaml` (no `charts/` exists because it depends on the stable/filebeat chart hosted on the official helm charts repository)
* `README.md` (each app managed by my team has a dedicated README maintained by the owners of the app)
* `metricbeat/`
* `helmfile.yaml`
* `README.md`
* `elastalert-operator/`
* `helmfile.yaml`
* `README.md`
* `charts/`
* `elastalert-operator/`
* `<the content of the local helm chart>`
The benefits of this structure is that you can run `git diff` to locate in which directory=microservice a git commit has changes.
It allows your CI system to run a workflow for the changed microservice only.
A downside of this is that you don't have an obvious way to sync all microservices at once. That is, you have to run:
```bash
for d in apps/*; do helmfile -f $d diff; if [ $? -eq 2 ]; then helmfile -f $d sync; fi; done
```
At this point, you'll start writing a `Makefile` under `myteam/` so that `make sync-all` will do the job.
It does work, but you can rely on the Helmfile feature instead.
Put `myteam/helmfile.yaml` that looks like:
```yaml
helmfiles:
- apps/*/helmfile.yaml
```
So that you can get rid of the `Makefile` and the bash snippet.
Just run `helmfile sync` inside `myteam/`, and you are done.
All the files are sorted alphabetically per group = array item inside `helmfiles:`, so that you have granular control over ordering, too.
#### selectors
When composing helmfiles you can use selectors from the command line as well as explicit selectors inside the parent helmfile to filter the releases to be used.
```yaml
helmfiles:
- apps/*/helmfile.yaml
- path: apps/a-helmfile.yaml
selectors: # list of selectors
- name=prometheus
- tier=frontend
- path: apps/b-helmfile.yaml # no selector, so all releases are used
selectors: []
- path: apps/c-helmfile.yaml # parent selector to be used or cli selector for the initial helmfile
selectorsInherited: true
```
* When a subhelmfile has explicit `selectors`, those selectors determine which releases from that subhelmfile are considered; parent and CLI selectors are not combined with them for release filtering.
* When CLI selectors are provided (e.g. `helmfile -l name=b sync`) and a subhelmfile has explicit selectors that are provably incompatible with them (same key, different value), that subhelmfile may be **skipped entirely** without loading or rendering it. For example, with `-l name=b`, a subhelmfile with `selectors: [name=a]` will be skipped since no release could match both. This optimization does not apply when `selectorsInherited: true` is set or when no CLI selectors are provided. Use `--debug` to see log messages about skipped subhelmfiles.
* When not selector is specified there are 2 modes for the selector inheritance because we would like to change the current inheritance behavior (see [issue #344](https://github.com/roboll/helmfile/issues/344) ).
* Legacy mode, sub-helmfiles without selectors inherit selectors from their parent helmfile. The initial helmfiles inherit from the command line selectors.
* explicit mode, sub-helmfile without selectors do not inherit from their parent or the CLI selector. If you want them to inherit from their parent selector then use `selectorsInherited: true`. To enable this explicit mode you need to set the following environment variable `HELMFILE_EXPERIMENTAL=explicit-selector-inheritance` (see [experimental](experimental-features.md)).
* Using `selector: []` will select all releases regardless of the parent selector or cli for the initial helmfile
* using `selectorsInherited: true` make the sub-helmfile selects releases with the parent selector or the cli for the initial helmfile. You cannot specify an explicit selector while using `selectorsInherited: true`
+1 -1
View File
@@ -2,7 +2,7 @@ Babel==2.17.0
click==8.1.2
ghp-import==2.0.2
gitdb==4.0.9
GitPython==3.1.41
GitPython==3.1.50
importlib-metadata==4.11.3
Jinja2==3.1.6
Markdown==3.8.1
+244
View File
@@ -0,0 +1,244 @@
# Templating
Helmfile uses [Go templates](https://godoc.org/text/template) for templating your helmfile.yaml. While go ships several built-in functions, we have added all of the functions in the [Sprig library](https://godoc.org/github.com/Masterminds/sprig).
We also added the following functions:
* [`env`](templating_funcs.md#env)
* [`requiredEnv`](templating_funcs.md#requiredenv)
* [`exec`](templating_funcs.md#exec)
* [`envExec`](templating_funcs.md#envexec)
* [`readFile`](templating_funcs.md#readfile)
* [`readDir`](templating_funcs.md#readdir)
* [`readDirEntries`](templating_funcs.md#readdirentries)
* [`toYaml`](templating_funcs.md#toyaml)
* [`fromYaml`](templating_funcs.md#fromyaml)
* [`setValueAtPath`](templating_funcs.md#setvalueatpath)
* [`get`](templating_funcs.md#get) (Sprig's original `get` is available as `sprigGet`)
* [`getOrNil`](templating_funcs.md#getornil)
* [`tpl`](templating_funcs.md#tpl)
* [`required`](templating_funcs.md#required)
* [`fetchSecretValue`](templating_funcs.md#fetchsecretvalue)
* [`expandSecretRefs`](templating_funcs.md#expandsecretrefs)
* [`include`](templating_funcs.md#include)
More details on each function can be found at the ["Template Functions" page in our documentation](templating_funcs.md).
## Using environment variables
Environment variables can be used in most places for templating the helmfile. Currently this is supported for `name`, `namespace`, `value` (in set), `values` and `url` (in repositories).
Examples:
```yaml
repositories:
- name: your-private-git-repo-hosted-charts
url: https://{{ requiredEnv "GITHUB_TOKEN"}}@raw.githubusercontent.com/kmzfs/helm-repo-in-github/master/
```
```yaml
releases:
- name: {{ requiredEnv "NAME" }}-vault
namespace: {{ requiredEnv "NAME" }}
chart: roboll/vault-secret-manager
values:
- db:
username: {{ requiredEnv "DB_USERNAME" }}
password: {{ requiredEnv "DB_PASSWORD" }}
set:
- name: proxy.domain
value: {{ requiredEnv "PLATFORM_ID" }}.my-domain.com
- name: proxy.scheme
value: {{ env "SCHEME" | default "https" }}
```
### Note
If you wish to treat your enviroment variables as strings always, even if they are boolean or numeric values you can use `{{ env "ENV_NAME" | quote }}` or `"{{ env "ENV_NAME" }}"`. These approaches also work with `requiredEnv`.
### Useful internal Helmfile environment variables
Helmfile uses some OS environment variables to override default behaviour:
* `HELMFILE_DISABLE_INSECURE_FEATURES` - disable insecure features, expecting `true` lower case
* `HELMFILE_DISABLE_RUNNER_UNIQUE_ID` - disable unique logging ID, expecting any non-empty value
* `HELMFILE_SKIP_INSECURE_TEMPLATE_FUNCTIONS` - disable insecure template functions, expecting `true` lower case
* `HELMFILE_USE_HELM_STATUS_TO_CHECK_RELEASE_EXISTENCE` - expecting non-empty value to use `helm status` to check release existence, instead of `helm list` which is the default behaviour
* `HELMFILE_EXPERIMENTAL` - enable experimental features, expecting `true` lower case
* `HELMFILE_ENVIRONMENT` - specify [Helmfile environment](environments.md), it has lower priority than CLI argument `--environment`
* `HELMFILE_KUBE_CONTEXT` - specify the kubectl context, it has lower priority than CLI argument `--kube-context`
* `HELMFILE_NAMESPACE` - specify the namespace, it has lower priority than CLI argument `--namespace`
* `HELMFILE_HELM_BINARY` - specify the path to the helm binary, it has lower priority than CLI argument `--helm-binary`
* `HELMFILE_KUSTOMIZE_BINARY` - specify the path to the kustomize binary, it has lower priority than CLI argument `--kustomize-binary`
* `HELMFILE_LOG_LEVEL` - specify the log level, it has lower priority than CLI argument `--log-level`
* `HELMFILE_DEBUG` - enable debug output, expecting `true` lower case. The same as `--debug` CLI flag
* `HELMFILE_QUIET` - silence output (equivalent to log-level warn), expecting `true` lower case. The same as `--quiet`/`-q` CLI flag
* `HELMFILE_NO_COLOR` - disable colored output, expecting `true` lower case. The same as `--no-color` CLI flag. `NO_COLOR` (any non-empty value, per [no-color.org](https://no-color.org/)) is also honored
* `HELMFILE_TEMPDIR` - specify directory to store temporary files
* `HELMFILE_UPGRADE_NOTICE_DISABLED` - expecting any non-empty value to skip the check for the latest version of Helmfile in [helmfile version](cli.md#version)
* `HELMFILE_GO_YAML_V3` - use *go.yaml.in/yaml/v3* instead of *go.yaml.in/yaml/v2*. It's `false` by default in Helmfile v0.x, and `true` in Helmfile v1.x.
* `HELMFILE_CACHE_HOME` - specify directory to store cached files for remote operations
* `HELMFILE_FILE_PATH` - specify the path to the helmfile.yaml file
* `HELMFILE_INTERACTIVE` - enable interactive mode, expecting `true` lower case. The same as `--interactive` CLI flag
* `HELMFILE_RENDER_YAML` - force helmfile.yaml to be rendered as a Go template regardless of file extension, expecting `true` lower case. Useful for migrating from v0 to v1 without renaming files to `.gotmpl`
* `HELMFILE_AWS_SDK_LOG_LEVEL` - configure AWS SDK logging level for vals library. Valid values: `off` (default, secure, case-insensitive), `minimal`, `standard`, `verbose`, or custom comma-separated values like `request,response`. See issue #2270 for details
* `HELMFILE_VALS_FAIL_ON_MISSING_KEY_IN_MAP` - enable strict mode for vals secret references. When set to `true` (or any value accepted by Go's `strconv.ParseBool` like `TRUE`, `1`), vals will fail when a referenced key does not exist in the secret map. Invalid values will cause an error when vals is initialized (when secret refs are first evaluated). Default is `false` (when unset or empty) for backward compatibility. See issue #1563 for details
## Templates
You can use go's text/template expressions in `helmfile.yaml` and `values.yaml.gotmpl` (templated helm values files). `values.yaml` references will be used verbatim. In other words:
* for value files ending with `.gotmpl`, template expressions will be rendered
* for plain value files (ending in `.yaml`), content will be used as-is
In addition to built-in ones, the following custom template functions are available:
* `readFile` reads the specified local file and generate a golang string
* `readDir` reads the files within provided directory path. (folders are excluded)
* `readDirEntries` Returns a list of [https://pkg.go.dev/os#DirEntry](DirEntry) within provided directory path
* `fromYaml` reads a golang string and generates a map
* `setValueAtPath PATH NEW_VALUE` traverses a golang map, replaces the value at the PATH with NEW_VALUE
* `toYaml` marshals a map into a string
* `get` returns the value of the specified key if present in the `.Values` object, otherwise will return the default value defined in the function
### Values Files Templates
You can reference a template of values file in your `helmfile.yaml` like below:
```yaml
releases:
- name: myapp
chart: mychart
values:
- values.yaml.gotmpl
```
Every values file whose file extension is `.gotmpl` is considered as a template file.
Suppose `values.yaml.gotmpl` was something like:
```yaml
{{ readFile "values.yaml" | fromYaml | setValueAtPath "foo.bar" "FOO_BAR" | toYaml }}
```
And `values.yaml` was:
```yaml
foo:
bar: ""
```
The resulting, temporary values.yaml that is generated from `values.yaml.gotmpl` would become:
```yaml
foo:
# Notice `setValueAtPath "foo.bar" "FOO_BAR"` in the template above
bar: FOO_BAR
```
## Refactoring `helmfile.yaml` with values files templates
One of expected use-cases of values files templates is to keep `helmfile.yaml` small and concise.
See the example `helmfile.yaml` below:
```yaml
releases:
- name: {{ requiredEnv "NAME" }}-vault
namespace: {{ requiredEnv "NAME" }}
chart: roboll/vault-secret-manager
values:
- db:
username: {{ requiredEnv "DB_USERNAME" }}
password: {{ requiredEnv "DB_PASSWORD" }}
set:
- name: proxy.domain
value: {{ requiredEnv "PLATFORM_ID" }}.my-domain.com
- name: proxy.scheme
value: {{ env "SCHEME" | default "https" }}
```
The `values` and `set` sections of the config file can be separated out into a template:
`helmfile.yaml`:
```yaml
releases:
- name: {{ requiredEnv "NAME" }}-vault
namespace: {{ requiredEnv "NAME" }}
chart: roboll/vault-secret-manager
values:
- values.yaml.gotmpl
```
`values.yaml.gotmpl`:
```yaml
db:
username: {{ requiredEnv "DB_USERNAME" }}
password: {{ requiredEnv "DB_PASSWORD" }}
proxy:
domain: {{ requiredEnv "PLATFORM_ID" }}.my-domain.com
scheme: {{ env "SCHEME" | default "https" }}
```
## Importing values from any source
The `exec` template function that is available in `values.yaml.gotmpl` is useful for importing values from any source
that is accessible by running a command:
A usual usage of `exec` would look like this:
```yaml
mysetting: |
{{ exec "./mycmd" (list "arg1" "arg2" "--flag1") | indent 2 }}
```
Or even with a pipeline:
```yaml
mysetting: |
{{ yourinput | exec "./mycmd-consume-stdin" (list "arg1" "arg2") | indent 2 }}
```
The possibility is endless. Try importing values from your golang app, bash script, jsonnet, or anything!
Then `envExec` same as `exec`, but it can receive a dict as the envs.
A usual usage of `envExec` would look like this:
```yaml
mysetting: |
{{ envExec (dict "envkey" "envValue") "./mycmd" (list "arg1" "arg2" "--flag1") | indent 2 }}
```
## Using .env files
Helmfile itself doesn't have an ability to load .env files. But you can write some bash script to achieve the goal:
```console
set -a; . .env; set +a; helmfile sync
```
Please see #203 for more context.
## Running Helmfile interactively
`helmfile --interactive [apply|destroy|delete|sync]` requests confirmation from you before actually modifying your cluster.
Use it when you're running `helmfile` manually on your local machine or a kind of secure administrative hosts.
For your local use-case, aliasing it like `alias hi='helmfile --interactive'` would be convenient.
Another way to use it is to set the environment variable `HELMFILE_INTERACTIVE=true` to enable the interactive mode by default.
Anything other than `true` will disable the interactive mode. The precedence has the `--interactive` flag.
## Running Helmfile without an Internet connection
Once you download all required charts into your machine, you can run `helmfile sync --skip-deps` to deploy your apps.
With the `--skip-deps` option, you can skip running "helm repo update" and "helm dependency build".
## `bash` and `zsh` completion
helmfile completion --help
+86 -127
View File
@@ -116,6 +116,54 @@ environments:
- secrets.yaml # Merged last (step 3) - highest priority
```
### 4a. Merge Strategy: `override` vs `fallback`
By default, when an environment lists multiple files under `values:`, **later files override earlier files** (the historical helmfile behavior, equivalent to `mergeStrategy: override`).
You can flip this per environment so that **earlier files take precedence** and later files only fill in missing keys:
```yaml
environments:
production:
mergeStrategy: fallback
values:
- cluster-specific.yaml # wins on every key it defines
- shared-defaults.yaml # only fills gaps
```
Under `fallback`:
- An explicit non-nil value in an earlier file is preserved against any later file, including the zero values `false`, `0`, `""`, and empty list. An explicit `enabled: false` in `cluster-specific.yaml` is *not* silently overwritten by `enabled: true` from `shared-defaults.yaml`.
- Maps are deep-merged. An earlier map does not block later files from adding nested keys it didn't set; only the keys an earlier file explicitly defines win on conflict.
- An explicit `null` in an earlier file falls through to a later file's value, matching how `MergeMaps` treats nil from the override side elsewhere in helmfile.
- Within a single `values:` entry that expands to multiple files (e.g. via a glob), the **first** file in the expansion wins.
- A later `.gotmpl` values file can reference values from earlier files via `.Values`, so derived defaults work natively:
```yaml
# cluster-specific.yaml
cluster:
domain: prod.example.com
```
```yaml
# shared-defaults.yaml.gotmpl
service:
domain: "service.{{ .Values.cluster.domain }}"
```
→ `service.domain: service.prod.example.com`. Under `mergeStrategy: override` this cross-file template reference is not available.
Valid values: `override` (default) and `fallback`. Any other value is rejected at load time.
#### Interaction with `.hcl` values files
`.hcl` files are evaluated as a single unit so that HCL `locals` and `values` blocks can reference each other across files. Helmfile collects every `.hcl` entry from the `values:` list, renders them together, and merges the combined result after the YAML pass. Two consequences worth knowing:
- `mergeStrategy` does not reshuffle the position of HCL within the list. HCL's combined output is always merged after the YAML pass. Under `override` it overrides YAML on conflicts (the historical behavior); under `fallback` it fills gaps only.
- Among multiple `.hcl` files, the precedence is HCL's own (last-file-wins within HCL), independent of `mergeStrategy`. The strategy applies at the YAML-vs-HCL boundary, not within HCL.
If you need first-file-wins precedence between specific HCL files, restructure them into one HCL file (or split the values into YAML).
### 5. CLI Overrides
The highest priority values come from CLI flags:
@@ -412,133 +460,6 @@ Here's the complete data flow when running `helmfile sync`:
- Defaults & Values: `ArrayMergeStrategySparse` (auto-detect nil values)
- CLIOverrides: `ArrayMergeStrategyMerge` (always element-by-element)
## Technical Details
### Secret Handling Intern
Helmfile processes secrets in a special way:
**Non-HCL secrets (.yaml, .yaml.gotmpl):**
1. Decrypted using helm-secrets plugin
2. Parsed into values immediately (during load phase)
3. Stored separately from regular values
4. **Mrged last** (highest priority) after all environment values are loaded
**HCL secrets (.hcl):**
1. Decrypted using helm-secrets plugin
2. Decrypted file paths added to values file list
3. Processed in step 2 (HCL loading phase)
4. Can reference values from other HCL files (using `hv.` accessor)
This separation allows:
- Secrets to override regular values without being re-decrypted multiple times
- HCL secrets to participate in HCL's cross-file referencing system
### Multiple Helmfiles and State files
When using multi-part helmfiles (multiple YAML documents separated by `---`):
```yaml
# Part 1: base.yaml
helmDefaults:
wait: true
timeout: 300
---
# Part 2: environments.yaml
environments:
default:
production:
```
Each part is processed in order:
and the results are merged with later parts taking precedence.
## Technical Details
### Environment Structure Internals
The `Environment` struct has three key fields that affect merging:
```go
type Environment struct {
Name string
KubeContext string
Values map[string]any // Environment values + secrets
Defaults map[string]any // Root-level values: block
CLIOverrides map[string]any // CLI --state-values-set
}
```
### Final Merge Process (GetMergedValues)
When you access `.Values` in templates, Helmfile calls `GetMergedValues()` which merges in this order:
```go
func (e *Environment) GetMergedValues() (map[string]any, error) {
vals := map[string]any{}
vals = maputil.MergeMaps(vals, e.Defaults) // 1. Defaults (root-level values:)
vals = maputil.MergeMaps(vals, e.Values) // 2. Values (environment values + secrets)
vals = maputil.MergeMaps(vals, e.CLIOverrides, // 3. CLIOverrides (highest priority)
maputil.MergeOptions{ArrayStrategy: maputil.ArrayMergeStrategyMerge})
return vals, nil
}
```
**Important:** CLIOverrides uses `ArrayMergeStrategyMerge` (element-by-element merging), while Defaults and Values use the default strategy (sparse auto-detection).
### Merging Library: mergo
Helmfile uses the [mergo](https://github.com/imdario/mergo) library for deep merging with these key features:
1. **Deep merge for maps**: Nested maps are merged recursively
2. **WithOverride option**: Later values override earlier values
3. **Type-safe**: Preserves value types during merge
Example from code:
```go
// In loadEnvValues()
if err := mergo.Merge(&valuesVals, &secretVals, mergo.WithOverride); err != nil {
return nil, err
}
```
### Array Merge Strategies Implementation
The `maputil.MergeMaps` function supports three array merge strategies:
```go
type ArrayMergeStrategy int
const (
ArrayMergeStrategySparse ArrayMergeStrategy = iota // Auto-detect based on nil values
ArrayMergeStrategyReplace ArrayMergeStrategy = iota // Always replace arrays
ArrayMergeStrategyMerge ArrayMergeStrategy = iota // Always merge element-by-element
)
```
**Sparse Strategy (Default for most cases):**
```go
func mergeSlices(base, override []any, strategy ArrayMergeStrategy) []any {
if strategy == ArrayMergeStrategySparse {
isSparse := false
for _, v := range override {
if v == nil {
isSparse = true
break
}
}
if !isSparse {
return override // Replace entirely
}
// Otherwise merge element-by-element
}
}
```
This means:
- `[1, 2, 3]` merged with `[4, 5]` → result: `[4, 5]` (replaced, no nils)
- `[null, 2]` merged with `[1, 2, 3]` → result: `[1, 2, 3]` (merged, has nil)
## Common Patterns
### Pattern 1: Global Defaults with Environment Overrides
@@ -633,6 +554,44 @@ environments:
## Technical Implementation Details
### Secret Handling
Helmfile processes secrets in a special way:
**Non-HCL secrets (.yaml, .yaml.gotmpl):**
1. Decrypted using helm-secrets plugin
2. Parsed into values immediately (during load phase)
3. Stored separately from regular values
4. **Merged last** (highest priority) after all environment values are loaded
**HCL secrets (.hcl):**
1. Decrypted using helm-secrets plugin
2. Decrypted file paths added to values file list
3. Processed in step 2 (HCL loading phase)
4. Can reference values from other HCL files (using `hv.` accessor)
This separation allows:
- Secrets to override regular values without being re-decrypted multiple times
- HCL secrets to participate in HCL's cross-file referencing system
### Multiple Helmfiles and State files
When using multi-part helmfiles (multiple YAML documents separated by `---`):
```yaml
# Part 1: base.yaml
helmDefaults:
wait: true
timeout: 300
---
# Part 2: environments.yaml
environments:
default:
production:
```
Each part is processed in order and the results are merged with later parts taking precedence.
### Environment Structure
The `Environment` struct is the core data structure that holds all values:
+44 -2
View File
@@ -1,6 +1,6 @@
# The Helmfile Best Practices Guide
# Writing Helmfile
This guide covers the Helmfile's considered patterns for writing advanced helmfiles. It focuses on how helmfile should be structured and executed.
This guide covers patterns and best practices for writing helmfiles. It focuses on how helmfile should be structured and executed.
**Before diving into advanced patterns, we strongly recommend reading [Values Merging and Data Flow](values-and-merging.md)** to understand how Helmfile merges values from various sources. This foundational knowledge is essential for writing effective helmfiles.
@@ -159,6 +159,48 @@ See [issue helmfile/helmfile#435](https://github.com/helmfile/helmfile/issues/43
You might also find [issue roboll/helmfile#428](https://github.com/roboll/helmfile/issues/428) useful for more context on how we originally designed the release template and what it's supposed to solve.
### Default Template Inheritance
When all releases share the same template, specifying `inherit` on each one becomes repetitive. Use `defaultInherit` to apply a template to all releases automatically:
```yaml
templates:
default:
namespace: kube-system
missingFileHandler: Warn
values:
- config/{{`{{ .Release.Name }}`}}/values.yaml
defaultInherit: default
releases:
- name: heapster
chart: stable/heapster
version: 0.3.2
# inherits from "default" automatically
- name: kubernetes-dashboard
chart: stable/kubernetes-dashboard
version: 0.10.0
inherit:
- template: default
except:
- values
```
`defaultInherit` accepts a single template name or a list:
```yaml
# Single template
defaultInherit: default
# Multiple templates (merged in order)
defaultInherit:
- ns-template
- defaults
```
If a release already inherits from the same template explicitly, the default is not duplicated. Use `except` in the release's `inherit` to exclude specific fields when needed.
## Layering Release Values
Please note, that it is not possible to layer `values` sections. If `values` is defined in the release and in the release template, only the `values` defined in the release will be considered. The same applies to `secrets` and `set`.
+121 -116
View File
@@ -1,54 +1,63 @@
module github.com/helmfile/helmfile
go 1.25.4
go 1.26.2
require (
dario.cat/mergo v1.0.2
github.com/Masterminds/semver/v3 v3.4.0
github.com/Masterminds/semver/v3 v3.5.0
github.com/Masterminds/sprig/v3 v3.3.0
github.com/aws/aws-sdk-go-v2/config v1.32.12
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2
github.com/aws/aws-sdk-go-v2/config v1.32.18
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/go-test/deep v1.1.1
github.com/gofrs/flock v0.13.0
github.com/golang/mock v1.6.0
github.com/google/go-cmp v0.7.0
github.com/gookit/color v1.6.1
github.com/gosuri/uitable v0.0.4
github.com/hashicorp/go-cty-funcs v0.1.0
github.com/hashicorp/go-getter/v2 v2.2.3
github.com/hashicorp/hcl/v2 v2.24.0
github.com/helmfile/chartify v0.26.2
github.com/helmfile/vals v0.43.7
github.com/helmfile/chartify v0.26.4
github.com/helmfile/vals v0.44.0
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939
github.com/tj/assert v0.0.3
github.com/variantdev/dag v1.1.0
github.com/werf/kubedog-for-werf-helm v0.0.0-20241217155728-9d45c48b82b6
github.com/zclconf/go-cty v1.18.0
github.com/werf/kubedog v0.13.1-0.20260217150136-ed58edf34eac
github.com/zclconf/go-cty v1.18.1
github.com/zclconf/go-cty-yaml v1.2.0
go.szostok.io/version v1.2.0
go.uber.org/zap v1.27.1
go.uber.org/zap v1.28.0
go.yaml.in/yaml/v2 v2.4.4
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/sync v0.20.0
golang.org/x/term v0.41.0
golang.org/x/term v0.43.0
gopkg.in/yaml.v3 v3.0.1
helm.sh/helm/v3 v3.20.1
helm.sh/helm/v4 v4.1.3
k8s.io/apimachinery v0.35.3
k8s.io/client-go v0.35.3
helm.sh/helm/v3 v3.21.0
helm.sh/helm/v4 v4.2.0
k8s.io/apimachinery v0.36.0
k8s.io/client-go v0.36.0
)
replace (
// kubedog's flagger dependency still imports k8s.io/api/autoscaling/v2beta2,
// while Helm 4.2.0 requires k8s.io/apimachinery/content.IsPathSegmentName from v0.36+.
k8s.io/api => k8s.io/api v0.35.4
k8s.io/apimachinery => k8s.io/apimachinery v0.36.0
k8s.io/client-go => k8s.io/client-go v0.35.4
)
require (
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/storage v1.61.0 // indirect
cloud.google.com/go/iam v1.7.0 // indirect
cloud.google.com/go/storage v1.62.1 // indirect
filippo.io/age v1.3.1 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.23 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.7 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
@@ -58,31 +67,31 @@ require (
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/fatih/color v1.19.0
github.com/fujiwara/tfstate-lookup v1.10.0 // indirect
github.com/fujiwara/tfstate-lookup v1.11.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/googleapis/gax-go/v2 v2.21.0 // indirect
github.com/goware/prefixer v0.0.0-20160118172347-395022866408 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-slug v0.16.4 // indirect
github.com/hashicorp/go-slug v0.16.8 // indirect
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
github.com/hashicorp/go-tfe v1.84.0 // indirect
github.com/hashicorp/go-tfe v1.99.0 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e // indirect
github.com/hashicorp/vault/api v1.22.0 // indirect
github.com/hashicorp/vault/api v1.23.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/itchyny/gojq v0.12.16 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/lib/pq v1.11.2 // indirect
github.com/itchyny/gojq v0.12.19 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/lib/pq v1.12.3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
@@ -97,15 +106,15 @@ require (
github.com/spf13/cast v1.7.0 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
go.uber.org/atomic v1.9.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/api v0.271.0 // indirect
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
google.golang.org/api v0.276.0 // indirect
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/ini.v1 v1.67.1 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
@@ -114,21 +123,21 @@ require (
require (
al.essio.dev/pkg/shellescape v1.6.0 // indirect
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth v0.20.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/kms v1.26.0 // indirect
cloud.google.com/go/longrunning v0.8.0 // indirect
cloud.google.com/go/kms v1.29.0 // indirect
cloud.google.com/go/longrunning v0.9.0 // indirect
cloud.google.com/go/monitoring v1.24.3 // indirect
cloud.google.com/go/secretmanager v1.16.0 // indirect
cloud.google.com/go/secretmanager v1.19.0 // indirect
filippo.io/edwards25519 v1.1.1 // indirect
filippo.io/hpke v0.4.0 // indirect
github.com/1Password/connect-sdk-go v1.5.3 // indirect
github.com/1password/onepassword-sdk-go v0.3.1 // indirect
github.com/AlecAivazis/survey/v2 v2.3.6 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 // indirect
@@ -137,17 +146,17 @@ require (
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/DelineaXPM/tss-sdk-go/v3 v3.0.1 // indirect
github.com/DelineaXPM/tss-sdk-go/v3 v3.0.2 // indirect
github.com/DopplerHQ/cli v0.5.11-0.20230908185655-7aef4713e1a4 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/ProtonMail/go-crypto v1.4.1 // indirect
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/antchfx/jsonquery v1.3.6 // indirect
github.com/antchfx/jsonquery v1.3.7 // indirect
github.com/antchfx/xpath v1.3.6 // indirect
github.com/apparentlymart/go-cidr v1.1.0 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
@@ -155,46 +164,46 @@ require (
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/avelino/slugify v0.0.0-20180501145920-855f152bd774 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.4 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 // indirect
github.com/aws/aws-sdk-go-v2/service/kms v1.50.2 // indirect
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.3 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect
github.com/aws/aws-sdk-go-v2/service/ssm v1.68.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.7 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.17 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect
github.com/aws/aws-sdk-go-v2/service/kms v1.51.0 // indirect
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.6 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect
github.com/aws/aws-sdk-go-v2/service/ssm v1.68.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 // indirect
github.com/aws/smithy-go v1.25.1 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
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/chanced/caps v1.0.2 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/containerd/containerd v1.7.30 // indirect
github.com/containerd/errdefs v0.3.0 // indirect
github.com/containerd/containerd v1.7.32 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/cyberark/conjur-api-go v0.13.16 // indirect
github.com/cyberark/conjur-api-go v0.13.19 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/danieljoos/wincred v1.2.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
@@ -202,36 +211,36 @@ require (
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
github.com/extism/go-sdk v1.7.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fluxcd/cli-utils v0.37.2-flux.1 // indirect
github.com/fluxcd/cli-utils v1.2.0 // indirect
github.com/fluxcd/flagger v1.36.1 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e // indirect
github.com/getsops/sops/v3 v3.12.1 // indirect
github.com/getsops/sops/v3 v3.12.2 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.24.3 // indirect
github.com/go-openapi/analysis v0.25.0 // indirect
github.com/go-openapi/errors v0.22.7 // indirect
github.com/go-openapi/jsonpointer v0.22.5 // indirect
github.com/go-openapi/jsonreference v0.21.5 // indirect
github.com/go-openapi/loads v0.23.3 // indirect
github.com/go-openapi/runtime v0.29.3 // indirect
github.com/go-openapi/runtime v0.29.4 // indirect
github.com/go-openapi/spec v0.22.4 // indirect
github.com/go-openapi/strfmt v0.26.0 // indirect
github.com/go-openapi/strfmt v0.26.1 // indirect
github.com/go-openapi/swag v0.24.1 // indirect
github.com/go-openapi/swag/cmdutils v0.24.0 // indirect
github.com/go-openapi/swag/conv v0.25.5 // indirect
github.com/go-openapi/swag/fileutils v0.25.5 // indirect
github.com/go-openapi/swag/conv v0.26.0 // indirect
github.com/go-openapi/swag/fileutils v0.26.0 // indirect
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
github.com/go-openapi/swag/jsonutils v0.26.0 // indirect
github.com/go-openapi/swag/loading v0.25.5 // indirect
github.com/go-openapi/swag/mangling v0.25.5 // indirect
github.com/go-openapi/swag/netutils v0.24.0 // indirect
github.com/go-openapi/swag/stringutils v0.25.5 // indirect
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
github.com/go-openapi/swag/stringutils v0.26.0 // indirect
github.com/go-openapi/swag/typeutils v0.26.0 // indirect
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
github.com/go-openapi/validate v0.25.2 // indirect
github.com/go-resty/resty/v2 v2.13.1 // indirect
@@ -242,24 +251,23 @@ require (
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-jsonnet v0.21.0 // indirect
github.com/google/go-jsonnet v0.22.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/hashicorp/go-safetemp v1.0.0 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/go-version v1.8.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcp-sdk-go v0.167.0 // indirect
github.com/hashicorp/hcp-sdk-go v0.172.0 // indirect
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.187 // indirect
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect
github.com/infisical/go-sdk v0.6.8 // indirect
github.com/itchyny/timefmt-go v0.1.6 // indirect
github.com/infisical/go-sdk v0.7.1 // indirect
github.com/itchyny/timefmt-go v0.1.8 // indirect
github.com/jmoiron/sqlx v1.4.0 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
@@ -288,7 +296,6 @@ require (
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/zerolog v1.26.1 // indirect
github.com/rubenv/sql-migrate v1.8.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
@@ -310,49 +317,47 @@ require (
github.com/werf/logboek v0.6.1 // 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/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yandex-cloud/go-genproto v0.60.0 // indirect
github.com/yandex-cloud/go-genproto v0.75.0 // indirect
github.com/yandex-cloud/go-sdk v0.31.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/zalando/go-keyring v0.2.6 // indirect
go.mongodb.org/mongo-driver v1.17.6 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/metric v1.41.0 // indirect
go.opentelemetry.io/otel/sdk v1.41.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/tools v0.41.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/tools v0.43.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/gookit/color.v1 v1.1.6 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/api v0.35.3 // indirect
k8s.io/apiextensions-apiserver v0.35.1 // indirect
k8s.io/apiserver v0.35.1 // indirect
k8s.io/cli-runtime v0.35.1 // indirect
k8s.io/component-base v0.35.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
k8s.io/kubectl v0.35.1 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
k8s.io/api v0.36.0 // indirect
k8s.io/apiextensions-apiserver v0.36.0 // indirect
k8s.io/apiserver v0.36.0 // indirect
k8s.io/cli-runtime v0.36.0 // indirect
k8s.io/component-base v0.36.0 // indirect
k8s.io/klog/v2 v2.140.0 // indirect
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
k8s.io/kubectl v0.36.0 // indirect
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
oras.land/oras-go/v2 v2.6.0 // indirect
sigs.k8s.io/controller-runtime v0.23.1 // indirect
sigs.k8s.io/controller-runtime v0.24.0 // indirect
sigs.k8s.io/kustomize/api v0.21.1 // indirect
sigs.k8s.io/kustomize/kyaml v0.21.1 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
)
+297 -295
View File
@@ -7,26 +7,26 @@ cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU=
cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58=
cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw=
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
cloud.google.com/go/iam v1.7.0 h1:JD3zh0C6LHl16aCn5Akff0+GELdp1+4hmh6ndoFLl8U=
cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY=
cloud.google.com/go/kms v1.29.0 h1:bAW1C5FQf+6GhPkywQzPlsULALCG7c16qpXLFGV9ivY=
cloud.google.com/go/kms v1.29.0/go.mod h1:YIyXZym11R5uovJJt4oN5eUL3oPmirF3yKeIh6QAf4U=
cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA=
cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak=
cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY=
cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E=
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
cloud.google.com/go/secretmanager v1.16.0 h1:19QT7ZsLJ8FSP1k+4esQvuCD7npMJml6hYzilxVyT+k=
cloud.google.com/go/secretmanager v1.16.0/go.mod h1://C/e4I8D26SDTz1f3TQcddhcmiC3rMEl0S1Cakvs3Q=
cloud.google.com/go/storage v1.61.0 h1:8NGccs4oDZTqV1nBlom0CVJewloINXYW5Z0LoFqaVeI=
cloud.google.com/go/storage v1.61.0/go.mod h1:IvExELZv/uJe/DAzLgPeKNT8dm5+DM5gO0H1bkubD6Y=
cloud.google.com/go/secretmanager v1.19.0 h1:dm9BK06xl+hrxp2unT2psjZeypPj5c6uPiABb6fmicE=
cloud.google.com/go/secretmanager v1.19.0/go.mod h1:9OmSuOeiiUicANglrbdKWSnT3gYkRcXuUQDk7dDW0zU=
cloud.google.com/go/storage v1.62.1 h1:Os0G3XbUbjZumkpDUf2Y0rLoXJTCF1kU2kWUujKYXD8=
cloud.google.com/go/storage v1.62.1/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA=
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
@@ -46,14 +46,14 @@ 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/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw=
github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
@@ -72,11 +72,11 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEK
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ=
github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk=
github.com/Azure/go-autorest/autorest/adal v0.9.23 h1:Yepx8CvFxwNKpH6ja7RZ+sKX+DWYNldbLiALMC3BTz8=
github.com/Azure/go-autorest/autorest/adal v0.9.23/go.mod h1:5pcMqFkdPhviJdlEy3kC/v1ZLnQl0MH6XA5YCcMhy4c=
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc=
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0=
github.com/Azure/go-autorest/autorest/azure/cli v0.4.7 h1:Q9R3utmFg9K1B4OYtAZ7ZUUvIUdzQt7G2MN5Hi/d670=
github.com/Azure/go-autorest/autorest/azure/cli v0.4.7/go.mod h1:bVrAueELJ0CKLBpUHDIvD516TwmHmzqwCpvONWRsw3s=
github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
@@ -96,12 +96,12 @@ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/DelineaXPM/tss-sdk-go/v3 v3.0.1 h1:4JBJukbaTjv2gJogF3MxZkrt7i+ayRhM//FgdJTKJ3Q=
github.com/DelineaXPM/tss-sdk-go/v3 v3.0.1/go.mod h1:VmyoHQ25FhSVHTI3/ptQNOviNEMfCy2ALAf/3E4Eqxg=
github.com/DelineaXPM/tss-sdk-go/v3 v3.0.2 h1:8wRzxlo6fujNoDbnp6PnawY3moxqQelxpJGzTHG7Qoo=
github.com/DelineaXPM/tss-sdk-go/v3 v3.0.2/go.mod h1:VmyoHQ25FhSVHTI3/ptQNOviNEMfCy2ALAf/3E4Eqxg=
github.com/DopplerHQ/cli v0.5.11-0.20230908185655-7aef4713e1a4 h1:s7/zwMi5w+KnlumDVbX1+P6mNAk5o7Wvx0VmvrQ7Bm0=
github.com/DopplerHQ/cli v0.5.11-0.20230908185655-7aef4713e1a4/go.mod h1:ipnA9Lpn5YM+FDSQZ7VWNjcuVurchInoGKm+v7O0sGs=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
@@ -114,8 +114,8 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
@@ -126,17 +126,16 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63n
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY=
github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/antchfx/jsonquery v1.3.6 h1:TaSfeAh7n6T11I74bsZ1FswreIfrbJ0X+OyLflx6mx4=
github.com/antchfx/jsonquery v1.3.6/go.mod h1:fGzSGJn9Y826Qd3pC8Wx45avuUwpkePsACQJYy+58BU=
github.com/antchfx/xpath v1.3.2/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/antchfx/jsonquery v1.3.7 h1:LUoue12xcCj6Q41kYUSAS0UJ+9s3XyxbP5uh7x8aMsw=
github.com/antchfx/jsonquery v1.3.7/go.mod h1:oGh95SRUXZfnma1B7Q0p1rhgDeSgghub4W+JwnUYv2o=
github.com/antchfx/xpath v1.3.6 h1:s0y+ElRRtTQdfHP609qFu0+c6bglDv20pqOViQjjdPI=
github.com/antchfx/xpath v1.3.6/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=
@@ -153,52 +152,50 @@ github.com/avelino/slugify v0.0.0-20180501145920-855f152bd774 h1:HrMVYtly2IVqg9E
github.com/avelino/slugify v0.0.0-20180501145920-855f152bd774/go.mod h1:5wi5YYOpfuAKwL5XLFYopbgIl/v7NZxaJpa/4X6yFKE=
github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ=
github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk=
github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k=
github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=
github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.0 h1:MpkX8EjkwuvyuX9B7+Zgk5M4URb2WQ84Y6jM81n5imw=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.0/go.mod h1:4V9Pv5sFfMPWQF0Q0zYN6BlV/504dFGaTeogallRqQw=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 h1:SwGMTMLIlvDNyhMteQ6r8IJSBPlRdXX5d4idhIGbkXA=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 h1:qtJZ70afD3ISKWnoX3xB0J2otEqu3LqicRcDBqsj0hQ=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 h1:siU1A6xjUZ2N8zjTHSXFhB9L/2OY8Dqs0xXiLjF30jA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw=
github.com/aws/aws-sdk-go-v2/service/kms v1.50.2 h1:UOHOXigIzDRaEU03CBQcZ5uW7FNC7E+vwfhsQWXl5RQ=
github.com/aws/aws-sdk-go-v2/service/kms v1.50.2/go.mod h1:nAa5gmcmAmjXN3tGuhPSHLXFeWv+7nzKhjZzh8F7MH0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2 h1:MRNiP6nqa20aEl8fQ6PJpEq11b2d40b16sm4WD7QgMU=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2/go.mod h1:FrNA56srbsr3WShiaelyWYEo70x80mXnVZ17ZZfbeqg=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.3 h1:9bb0dEq1WzA0ZxIGG2EmwEgxfMAJpHyusxwbVN7f6iM=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.3/go.mod h1:2z9eg35jfuRtdPE4Ci0ousrOU9PBhDBilXA1cwq9Ptk=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=
github.com/aws/aws-sdk-go-v2/service/ssm v1.68.2 h1:idKv7B7NjmTDd05YHQYMMEFNeD0rWxs/kVX4lsjEiDo=
github.com/aws/aws-sdk-go-v2/service/ssm v1.68.2/go.mod h1:1NiL45h4A60CO/hu/UdNyG5AD3VEsdpaQx1l5KtpurA=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10/go.mod h1:qqY157uZoqm5OXq/amuaBJyC9hgBCBQnsaWnPe905GY=
github.com/aws/aws-sdk-go-v2/config v1.32.18 h1:Hcia46bxhGgF3BaSnG8nSNCWmqTK6bj9xN9/FJ3WK6Q=
github.com/aws/aws-sdk-go-v2/config v1.32.18/go.mod h1:zEjCAYmxqDadH1WX8CdBvmLKhUEUVFgKRQG38zjDmrY=
github.com/aws/aws-sdk-go-v2/credentials v1.19.17 h1:gP2nkGsS+KMvF/jfFz2Vv2qiiOqWKyPACSzPsqHgoW8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.17/go.mod h1:Bsew3S/moG5iT77giPj1q8wb/s0RE5/QfH+ASjYtuQc=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.2 h1:1i1SUOTLk0TbMh7+eJYxgv1r1f47BfR69LL6yaELoI0=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.2/go.mod h1:bo7DhmS/OyVeAJTC768nEk92YKWskqJ4gn0gB5e59qQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 h1:ieLCO1JxUWuxTZ1cRd0GAaeX7O6cIxnwk7tc1LsQhC4=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15/go.mod h1:e3IzZvQ3kAWNykvE0Tr0RDZCMFInMvhku3qNpcIQXhM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 h1:03xatSQO4+AM1lTAbnRg5OK528EUg744nW7F73U8DKw=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23/go.mod h1:M8l3mwgx5ToK7wot2sBBce/ojzgnPzZXUV445gTSyE8=
github.com/aws/aws-sdk-go-v2/service/kms v1.51.0 h1:696UM+NwOrETBCLQJyCAGtVmmZmziBT59yMwgg6Fvrw=
github.com/aws/aws-sdk-go-v2/service/kms v1.51.0/go.mod h1:GBO/aaEi47QldDVoqw2CsM2UZQDoqDiFIMJD/ztHPs0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 h1:etqBTKY581iwLL/H/S2sVgk3C9lAsTJFeXWFDsDcWOU=
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.6 h1:XR42AXidhYs4HwH0I+yElLXVt7zb2hAyNHQJe6Blv7w=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.6/go.mod h1:nOTsSVQlAsgwVRdtZYtECSnsInF8IUhrpnclCPat7Fs=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI=
github.com/aws/aws-sdk-go-v2/service/ssm v1.68.5 h1:TY5Vh7uXQgJVuc6ahI6toLcRajG1aYSDCP3a0xsPvmo=
github.com/aws/aws-sdk-go-v2/service/ssm v1.68.5/go.mod h1:UkzShnbxHRIIL2cHi/7fBGLUAZIVTEADQjaA53bWWCE=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.0 h1:nDARhv/oF55bcxF7rCI/4PDxOKnVXVWwDuDwCs2I2SQ=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.0/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg=
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk=
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio=
github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -209,8 +206,8 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
github.com/bshuster-repo/logrus-logstash-hook v1.1.0 h1:o2FzZifLg+z/DN1OFmzTWzZZx/roaqt8IPZCIVco8r4=
github.com/bshuster-repo/logrus-logstash-hook v1.1.0/go.mod h1:Q2aXOe7rNuPgbBtPCOzYyWDvKX7+FpxE5sRdvcPoui0=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
@@ -220,35 +217,39 @@ 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/chanced/caps v1.0.2 h1:RELvNN4lZajqSXJGzPaU7z8B4LK2+o2Oc/upeWdgMOA=
github.com/chanced/caps v1.0.2/go.mod h1:SJhRzeYLKJ3OmzyQXhdZ7Etj7lqqWoPtQ1zcSJRtQjs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/containerd/containerd v1.7.30 h1:/2vezDpLDVGGmkUXmlNPLCCNKHJ5BbC5tJB5JNzQhqE=
github.com/containerd/containerd v1.7.30/go.mod h1:fek494vwJClULlTpExsmOyKCMUAbuVjlFsJQc4/j44M=
github.com/containerd/containerd v1.7.32 h1:S54xuVcPxeLaYgaRABtpJ2VyVUVsy0IGf7qHBs+sbY8=
github.com/containerd/containerd v1.7.32/go.mod h1:jdwD6s/BhV4XVJGrvtziNPVA+83n66TwptVaPKprq4E=
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
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/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
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.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
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/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyberark/conjur-api-go v0.13.16 h1:4PT/cja78hIIyUy1EOQPhppedVMY76BfzmWR20c8kog=
github.com/cyberark/conjur-api-go v0.13.16/go.mod h1:BQmiYeA8hJmGSduF+wgfXY4Ktdky30+cevXm+tzr63k=
github.com/cyberark/conjur-api-go v0.13.19 h1:QPUmBJJ4ik2FwHlNHVdRND6t3sLL2bqmZDPdikfzSlc=
github.com/cyberark/conjur-api-go v0.13.19/go.mod h1:POjKgJwnniONTnWySx6Yq87hYsdPD6Ohwm7x1UGio7E=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
@@ -261,22 +262,20 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM=
github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU=
github.com/distribution/distribution/v3 v3.1.1 h1:KUbk7C8CfaLXy8kbf/hGq9cad/wCoLB6dbWH6DMbmX0=
github.com/distribution/distribution/v3 v3.1.1/go.mod h1:d7lXwZpph0bVcOj4Aqn0nMrWHIwRQGdiV5TLeI+/w6Y=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/cli v28.0.4+incompatible h1:pBJSJeNd9QeIWPjRcV91RVJihd/TXB77q1ef64XEu4A=
github.com/docker/cli v28.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v28.0.4+incompatible h1:JNNkBctYKurkw6FrHfKqY0nKIDf5nrbxjVBtS+cdcok=
github.com/docker/docker v28.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
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-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
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/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-events v0.0.0-20250808211157-605354379745 h1:yOn6Ze6IbYI/KAw2lw/83ELYvZh6hvsygTVkD0dzMC4=
github.com/docker/go-events v0.0.0-20250808211157-605354379745/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -285,8 +284,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE=
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
@@ -311,30 +310,30 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fluxcd/cli-utils v0.37.2-flux.1 h1:tQ588ghtRN+E+kHq415FddfqA9v4brn/1WWgrP6rQR0=
github.com/fluxcd/cli-utils v0.37.2-flux.1/go.mod h1:LcWSu1NYET8d8U7O326RhEm5JkQXCMK6ITu4G1CT02c=
github.com/fluxcd/cli-utils v1.2.0 h1:1o07pXTMxJ/XJ1GpAbLtjdXwfCUMq4Ku1OcnvJHLohI=
github.com/fluxcd/cli-utils v1.2.0/go.mod h1:d5HdTDdR5sCbsIbgtOQ7x7srKYwYeZORU6CD2yn4j/M=
github.com/fluxcd/flagger v1.36.1 h1:X2PumtNwZz9YSGaOtZLFm2zAKLgHhFkbNv8beg7ifyc=
github.com/fluxcd/flagger v1.36.1/go.mod h1:qmtLsxheVDTI8XeCaXUxW5UCmfcSKnY9fizG9NmW/Fk=
github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0=
github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fujiwara/tfstate-lookup v1.10.0 h1:RtVV1PUO+C7hqmRKhecS/ed6XOZ4KDiJmbHPFvX7ZJE=
github.com/fujiwara/tfstate-lookup v1.10.0/go.mod h1:hBwXB7lKy4kPEM+szko7rzAq04TGpBzzNY8Wm8hG6UE=
github.com/fujiwara/tfstate-lookup v1.11.0 h1:Td8nSGyicw90vHhQhMEDs/ouDYPswQyS2sHgxgH44Tc=
github.com/fujiwara/tfstate-lookup v1.11.0/go.mod h1:USksi9piDklLjGmdisD2g/hScJLUHfJMfLygyQk5FnA=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e h1:y/1nzrdF+RPds4lfoEpNhjfmzlgZtPqyO3jMzrqDQws=
github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e/go.mod h1:awFzISqLJoZLm+i9QQ4SgMNHDqljH6jWV0B36V5MrUM=
github.com/getsops/sops/v3 v3.12.1 h1:DZzLNJx6EH4SZvMjI1Y814WIcOQNUtOP3WgDsHNqQTU=
github.com/getsops/sops/v3 v3.12.1/go.mod h1:Bs/geuL5shRiXi194TQaGFiLvzVpA6U8tTYRd84qdvM=
github.com/getsops/sops/v3 v3.12.2 h1:4ctEFDNpAAubW8EMICytX8+BFDBSFJkrKvQ9ahSs0a4=
github.com/getsops/sops/v3 v3.12.2/go.mod h1:BACmHQl0J8nPNXBDSJKRT5oUdZx36CkbohGDj9+bD9M=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -342,8 +341,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-openapi/analysis v0.24.3 h1:a1hrvMr8X0Xt69KP5uVTu5jH62DscmDifrLzNglAayk=
github.com/go-openapi/analysis v0.24.3/go.mod h1:Nc+dWJ/FxZbhSow5Yh3ozg5CLJioB+XXT6MdLvJUsUw=
github.com/go-openapi/analysis v0.25.0 h1:EnjAq1yO8wEO9HbPmY8vLPEIkdZuuFhCAKBPvCB7bCs=
github.com/go-openapi/analysis v0.25.0/go.mod h1:5WFTRE43WLkPG9r9OtlMfqkkvUTYLVVCIxLlEpyF8kE=
github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA=
github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w=
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
@@ -352,42 +351,42 @@ github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe
github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ=
github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA=
github.com/go-openapi/runtime v0.29.3 h1:h5twGaEqxtQg40ePiYm9vFFH1q06Czd7Ot6ufdK0w/Y=
github.com/go-openapi/runtime v0.29.3/go.mod h1:8A1W0/L5eyNJvKciqZtvIVQvYO66NlB7INMSZ9bw/oI=
github.com/go-openapi/runtime v0.29.4 h1:k2lDxrGoSAJRdhFG2tONKMpkizY/4X1cciSdtzk4Jjo=
github.com/go-openapi/runtime v0.29.4/go.mod h1:K0k/2raY6oqXJnZAgWJB2i/12QKrhUKpZcH4PfV9P18=
github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=
github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=
github.com/go-openapi/strfmt v0.26.0 h1:SDdQLyOEqu8W96rO1FRG1fuCtVyzmukky0zcD6gMGLU=
github.com/go-openapi/strfmt v0.26.0/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y=
github.com/go-openapi/strfmt v0.26.1 h1:7zGCHji7zSYDC2tCXIusoxYQz/48jAf2q+sF6wXTG+c=
github.com/go-openapi/strfmt v0.26.1/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y=
github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8=
github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A=
github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I=
github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8=
github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=
github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=
github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk=
github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc=
github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I=
github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE=
github.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU=
github.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc=
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=
github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo=
github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA=
github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y=
github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=
github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=
github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw=
github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY=
github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w=
github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM=
github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=
github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=
github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=
github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=
github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg=
github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE=
github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4=
github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE=
github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=
github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.1 h1:NZOrZmIb6PTv5LTFxr5/mKV/FjbUzGE7E6gLz7vFoOQ=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.1/go.mod h1:r7dwsujEHawapMsxA69i+XMGZrQ5tRauhLAjV/sxg3Q=
github.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw=
github.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE=
github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4=
github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0=
github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
@@ -415,10 +414,7 @@ github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
@@ -451,12 +447,13 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/go-jsonnet v0.21.0 h1:43Bk3K4zMRP/aAZm9Po2uSEjY6ALCkYUVIcz9HLGMvA=
github.com/google/go-jsonnet v0.21.0/go.mod h1:tCGAu8cpUpEZcdGMmdOu37nh8bGgqubhI5v2iSk3KJQ=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/go-jsonnet v0.22.0 h1:o0bOAIE+9SIfRZ7FXQPuta0mHLLE0AwbY/L5GTH5CH8=
github.com/google/go-jsonnet v0.22.0/go.mod h1:pLhKpu0/ODjL2Zev4y+CmCoHKAgONT1gSLQyriuYh9w=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
@@ -470,10 +467,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI=
github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4=
github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=
github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=
github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU=
github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
@@ -484,8 +483,8 @@ github.com/goware/prefixer v0.0.0-20160118172347-395022866408 h1:Y9iQJfEqnN3/Nce
github.com/goware/prefixer v0.0.0-20160118172347-395022866408/go.mod h1:PE1ycukgRPJ7bJ9a1fdfQ9j8i/cEcRAoLZzbxYpNB/s=
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.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/hashicorp/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=
@@ -509,16 +508,16 @@ github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRct
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
github.com/hashicorp/go-slug v0.16.4 h1:kI0mOUVjbBsyocwO29pZIQzzkBnfQNdU4eqlUpNdNVA=
github.com/hashicorp/go-slug v0.16.4/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ=
github.com/hashicorp/go-slug v0.16.8 h1:f4/sDZqRsxx006HrE6e9BE5xO9lWXydKhVoH6Kb0v1M=
github.com/hashicorp/go-slug v0.16.8/go.mod h1:hB4mUcVHl4RPu0205s0fwmB9i31MxQgeafGkko3FD+Y=
github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
github.com/hashicorp/go-tfe v1.84.0 h1:aq4zLtr0beMjoe1bjMPkv9tW4wWTix37SBX8ct8vJWw=
github.com/hashicorp/go-tfe v1.84.0/go.mod h1:6dUFMBKh0jkxlRsrw7bYD2mby0efdwE4dtlAuTogIzA=
github.com/hashicorp/go-tfe v1.99.0 h1:JJToLgw5swACi3Z6Hap3zFS8UfA40R/PdUVQU3xh7Hk=
github.com/hashicorp/go-tfe v1.99.0/go.mod h1:JIqznMwZd8flUhPif5d2sprKcFkD4sWJSIQ6E8iAuIA=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw=
@@ -529,16 +528,16 @@ github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y
github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
github.com/hashicorp/hcp-sdk-go v0.167.0 h1:t2v+mm3gN1z4qvdJ7g9RuDdXDvIExMtjV1Fvzn2LuVc=
github.com/hashicorp/hcp-sdk-go v0.167.0/go.mod h1:v2vbpNIrmgUTelW4Z+ur+aQuSPxeaVK3xytFdpEXvSg=
github.com/hashicorp/hcp-sdk-go v0.172.0 h1:j4VrSN2yd8prFb8Y0gQWQbTpsV5uVPgYEUozOGfPOOc=
github.com/hashicorp/hcp-sdk-go v0.172.0/go.mod h1:v2vbpNIrmgUTelW4Z+ur+aQuSPxeaVK3xytFdpEXvSg=
github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e h1:xwy/1T0cxHWaLx2MM0g4BlaQc1BXn/9835mPrBqwSPU=
github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM=
github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0=
github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM=
github.com/helmfile/chartify v0.26.2 h1:DM9t0fcKpBdAFAKOccrBZ9HZbDfsyQiNUCydmwVjB7E=
github.com/helmfile/chartify v0.26.2/go.mod h1:a5faVrDFlVzi3cQakbtjDnIfonskkIsvms5Vj7aKYdw=
github.com/helmfile/vals v0.43.7 h1:mW+9Mo0IqHZH86oNwR2LVkgdyROzUTw2yI8t9UB/luY=
github.com/helmfile/vals v0.43.7/go.mod h1:tMjwmA9xn4v3elFJPrV+BKqImXectuWn7gc7bHP+tgM=
github.com/hashicorp/vault/api v1.23.0 h1:gXgluBsSECfRWTSW9niY2jwg2e9mMJc4WoHNv4g3h6A=
github.com/hashicorp/vault/api v1.23.0/go.mod h1:zransKiB9ftp+kgY8ydjnvCU7Wk8i9L0DYWpXeMj9ko=
github.com/helmfile/chartify v0.26.4 h1:pIzVe+mqBiBMlJEH3qUVKgFQKV/m4vGOVccdYWY4VbI=
github.com/helmfile/chartify v0.26.4/go.mod h1:jnMhinkuwSMfgPPNb3JYges/13xkXPEdUVnh1eGxTOQ=
github.com/helmfile/vals v0.44.0 h1:9Yf5JDIl3JUHE1XWR9GopurvAbuXowCSsgUShB4aWcI=
github.com/helmfile/vals v0.44.0/go.mod h1:siAvy7f4VPPCrgLGzDOW21ZbvR6Tbf9g7oGRme9fMH4=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8=
@@ -551,12 +550,12 @@ github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/infisical/go-sdk v0.6.8 h1:OB0d4v9Nm+ioA5it1SQaOGGv5qXWEwfYsxRqZZkxHMk=
github.com/infisical/go-sdk v0.6.8/go.mod h1:A6l7EhwCkPw8tmJjgA09KtueEHYko+VdGCEupK8hL08=
github.com/itchyny/gojq v0.12.16 h1:yLfgLxhIr/6sJNVmYfQjTIv0jGctu6/DgDoivmxTr7g=
github.com/itchyny/gojq v0.12.16/go.mod h1:6abHbdC2uB9ogMS38XsErnfqJ94UlngIJGlRAIj4jTM=
github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q=
github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg=
github.com/infisical/go-sdk v0.7.1 h1:26upmNiIuXJgZEQdH8ThLZ18EIGdg9ifMm+fBGXSmP0=
github.com/infisical/go-sdk v0.7.1/go.mod h1:yEfXF+3YDDXiJ9zzJUSzW6me6XXPPEDK52fSU6JfpCA=
github.com/itchyny/gojq v0.12.19 h1:ttXA0XCLEMoaLOz5lSeFOZ6u6Q3QxmG46vfgI4O0DEs=
github.com/itchyny/gojq v0.12.19/go.mod h1:5galtVPDywX8SPSOrqjGxkBeDhSxEW1gSxoy7tn1iZY=
github.com/itchyny/timefmt-go v0.1.8 h1:1YEo1JvfXeAHKdjelbYr/uCuhkybaHCeTkH8Bo791OI=
github.com/itchyny/timefmt-go v0.1.8/go.mod h1:5E46Q+zj7vbTgWY8o5YkMeYb4I6GeWLFnetPy5oBrAI=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
@@ -568,8 +567,8 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -582,8 +581,8 @@ github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhR
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
@@ -597,8 +596,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
@@ -622,6 +621,10 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w=
github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.2.2 h1:Pt4hRMCAIlyjL3cr8M5TrXCwKzguebPAc2do2ur7dEM=
github.com/moby/moby/client v0.2.2/go.mod h1:2EkIPVNCqR05CMIzL1mfA07t0HvVUUOl85pasRz/GmQ=
github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
@@ -643,8 +646,8 @@ github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q=
github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
github.com/openbao/openbao/api/v2 v2.5.1 h1:Br79D6L20SbAa5P7xqENxmvv8LyI4HoKosPy7klhn4o=
github.com/openbao/openbao/api/v2 v2.5.1/go.mod h1:Dh5un77tqGgMbmlVEqjqN+8/dMyUohnkaQVg/wXW0Ig=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -688,8 +691,8 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho=
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U=
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc=
@@ -697,9 +700,6 @@ github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnA
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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
@@ -780,8 +780,8 @@ github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
github.com/variantdev/dag v1.1.0 h1:xodYlSng33KWGvIGMpKUyLcIZRXKiNUx612mZJqYrDg=
github.com/variantdev/dag v1.1.0/go.mod h1:pH1TQsNSLj2uxMo9NNl9zdGy01Wtn+/2MT96BrKmVyE=
github.com/werf/kubedog-for-werf-helm v0.0.0-20241217155728-9d45c48b82b6 h1:lpgQPTCp+wNJfTqJWtR6A5gRA4e4m/eRJFV7V18XCoA=
github.com/werf/kubedog-for-werf-helm v0.0.0-20241217155728-9d45c48b82b6/go.mod h1:PA9xGVKX9Il6sCgvPrcB3/FahRme3bXRz4BuylvAssc=
github.com/werf/kubedog v0.13.1-0.20260217150136-ed58edf34eac h1:kGp4G79ZiV61SxyLeh6kErzv+u5YBaAxp23oL6T/IJo=
github.com/werf/kubedog v0.13.1-0.20260217150136-ed58edf34eac/go.mod h1:gu4EY4hxtiYVDy5o6WE2lRZS0YWqrOV0HS//GTYyrUE=
github.com/werf/logboek v0.6.1 h1:oEe6FkmlKg0z0n80oZjLplj6sXcBeLleCkjfOOZEL2g=
github.com/werf/logboek v0.6.1/go.mod h1:Gez5J4bxekyr6MxTmIJyId1F61rpO+0/V4vjCIEIZmk=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
@@ -789,7 +789,6 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
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=
@@ -800,8 +799,8 @@ github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yandex-cloud/go-genproto v0.60.0 h1:hIpd742/g6XAe3cpZ8PK08ItwmuBRdHRMSJC8N3bQ9M=
github.com/yandex-cloud/go-genproto v0.60.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
github.com/yandex-cloud/go-genproto v0.75.0 h1:5XhK9CZtYqY6i3u62LETNgF1FwXN/D3EoxivJNbxyw4=
github.com/yandex-cloud/go-genproto v0.75.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
github.com/yandex-cloud/go-sdk v0.31.0 h1:iPixKMu7t64xziWRIEW3pKkq3kGuvgNmiwH/Vl1FcqY=
github.com/yandex-cloud/go-sdk v0.31.0/go.mod h1:C27Pqw9umTq3vi3ZM8tfmc5Rb0rt6Fxnl7nimQT1aM0=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
@@ -812,8 +811,8 @@ github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
github.com/zclconf/go-cty v1.18.0 h1:pJ8+HNI4gFoyRNqVE37wWbJWVw43BZczFo7KUoRczaA=
github.com/zclconf/go-cty v1.18.0/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg=
github.com/zclconf/go-cty v1.18.1 h1:yEGE8M4iIZlyKQURZNb2SnEyZlZHUcBCnx6KF81KuwM=
github.com/zclconf/go-cty v1.18.1/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
github.com/zclconf/go-cty-yaml v1.2.0 h1:GDyL4+e/Qe/S0B7YaecMLbVvAR/Mp21CXMOSiCTOi1M=
@@ -823,54 +822,54 @@ go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUps
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 h1:I/7S/yWobR3QHFLqHsJ8QOndoiFsj1VgHpQiq43KlUI=
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0/go.mod h1:jPF6gn3y1E+nozCAEQj3c6NZ8KY+tvAgSVfvoOJUFac=
go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 h1:dkBzNEAIKADEaFnuESzcXvpd09vxvDZsOjx11gjUqLk=
go.opentelemetry.io/contrib/bridges/prometheus v0.67.0/go.mod h1:Z5RIwRkZgauOIfnG5IpidvLpERjhTninpP1dTG2jTl4=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g=
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
go.opentelemetry.io/otel/exporters/prometheus v0.62.0 h1:krvC4JMfIOVdEuNPTtQ0ZjCiXrybhv+uOHMfHRmnvVo=
go.opentelemetry.io/otel/exporters/prometheus v0.62.0/go.mod h1:fgOE6FM/swEnsVQCqCnbOfRV4tOnWPg7bVeo4izBuhQ=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 h1:ivlbaajBWJqhcCPniDqDJmRwj4lc6sRT+dCAVKNmxlQ=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0/go.mod h1:u/G56dEKDDwXNCVLsbSrllB2o8pbtFLUC4HpR66r2dc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8=
go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4=
go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes=
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8=
go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90=
go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI=
go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4=
go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8=
go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y=
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 h1:4fnRcNpc6YFtG3zsFw9achKn3XgmxPxuMuqIL5rE8e8=
go.opentelemetry.io/contrib/exporters/autoexport v0.67.0/go.mod h1:qTvIHMFKoxW7HXg02gm6/Wofhq5p3Ib/A/NNt1EoBSQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/otel/exporters/prometheus v0.65.0 h1:jOveH/b4lU9HT7y+Gfamf18BqlOuz2PWEvs8yM7Q6XE=
go.opentelemetry.io/otel/exporters/prometheus v0.65.0/go.mod h1:i1P8pcumauPtUI4YNopea1dhzEMuEqWP1xoUZDylLHo=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 h1:GJkybS+crDMdExT/BUNCEgfrmfboztcS6PhvSo88HKM=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0/go.mod h1:NuAyxRYIG2lKX3YQkB+83StTxM7s52PUUkRRiC0wnYI=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU=
go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4=
go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko=
go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.szostok.io/version v1.2.0 h1:8eMMdfsonjbibwZRLJ8TnrErY8bThFTQsZYV16mcXms=
go.szostok.io/version v1.2.0/go.mod h1:EiU0gPxaXb6MZ+apSN0WgDO6F4JXyC99k9PIXf2k2E8=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
@@ -879,8 +878,8 @@ 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.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
@@ -894,22 +893,23 @@ golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.4.2/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.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -926,8 +926,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
@@ -961,21 +961,23 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -986,8 +988,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
@@ -1001,40 +1003,40 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
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.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-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=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY=
google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.276.0 h1:nVArUtfLEihtW+b0DdcqRGK1xoEm2+ltAihyztq7MKY=
google.golang.org/api v0.276.0/go.mod h1:Fnag/EWUPIcJXuIkP1pjoTgS5vdxlk3eeemL7Do6bvw=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0=
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -1054,38 +1056,38 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/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=
helm.sh/helm/v3 v3.20.1 h1:T8PodUaH1UwNvE+imUA2mIKjJItY8g7CVvLVP5g4NzI=
helm.sh/helm/v3 v3.20.1/go.mod h1:Fl1kBaWCpkUrM6IYXPjQ3bdZQfFrogKArqptvueZ6Ww=
helm.sh/helm/v4 v4.1.3 h1:Abfmb+oJUtxoaXDyB2Jhw1zRk3hT6aFfHta+AXb8Lno=
helm.sh/helm/v4 v4.1.3/go.mod h1:5dSo8rRgn3OTkDAc/k0Ipw5/Q+BlqKIKZwa0XwSiINI=
helm.sh/helm/v3 v3.21.0 h1:9TRbaXQH+BIKLLDYlu++JsyWodS5kBBOLF7C7HY5+cs=
helm.sh/helm/v3 v3.21.0/go.mod h1:5IvU6Ae6ruB/vasVHhnC1IU5RvqFM349vLYS1BiHqeY=
helm.sh/helm/v4 v4.2.0 h1:J+0TmTtPK2NuS6z9Z2WOcIX0nGGJylokEZLt0fi0X4U=
helm.sh/helm/v4 v4.2.0/go.mod h1:sDQRGAct/I/ogTvOX8QqE/8bBWuLH4BHbB3QFL5G3do=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ=
k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4=
k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w=
k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ=
k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8=
k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/apiserver v0.35.1 h1:potxdhhTL4i6AYAa2QCwtlhtB1eCdWQFvJV6fXgJzxs=
k8s.io/apiserver v0.35.1/go.mod h1:BiL6Dd3A2I/0lBnteXfWmCFobHM39vt5+hJQd7Lbpi4=
k8s.io/cli-runtime v0.35.1 h1:uKcXFe8J7AMAM4Gm2JDK4mp198dBEq2nyeYtO+JfGJE=
k8s.io/cli-runtime v0.35.1/go.mod h1:55/hiXIq1C8qIJ3WBrWxEwDLdHQYhBNRdZOz9f7yvTw=
k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg=
k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c=
k8s.io/component-base v0.35.1 h1:XgvpRf4srp037QWfGBLFsYMUQJkE5yMa94UsJU7pmcE=
k8s.io/component-base v0.35.1/go.mod h1:HI/6jXlwkiOL5zL9bqA3en1Ygv60F03oEpnuU1G56Bs=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/kubectl v0.35.1 h1:zP3Er8C5i1dcAFUMh9Eva0kVvZHptXIn/+8NtRWMxwg=
k8s.io/kubectl v0.35.1/go.mod h1:cQ2uAPs5IO/kx8R5s5J3Ihv3VCYwrx0obCXum0CvnXo=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/api v0.35.4 h1:P7nFYKl5vo9AGUp1Z+Pmd3p2tA7bX2wbFWCvDeRv988=
k8s.io/api v0.35.4/go.mod h1:yl4lqySWOgYJJf9RERXKUwE9g2y+CkuwG+xmcOK8wXU=
k8s.io/apiextensions-apiserver v0.36.0 h1:Wt7E8J+VBCbj4FjiBfDTK/neXDDjyJVJc7xfuOHImZ0=
k8s.io/apiextensions-apiserver v0.36.0/go.mod h1:kGDjH0msuiIB3tgsYRV0kS9GqpMYMUsQ3GHv7TApyug=
k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ=
k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc=
k8s.io/apiserver v0.36.0 h1:Jg5OFAENUACByUCg15CmhZAYrr5ZyJ+jodyA1mHl3YE=
k8s.io/apiserver v0.36.0/go.mod h1:mHvwdHf+qKEm+1/hYm756SV+oREOKSPnsjagOpx6Vho=
k8s.io/cli-runtime v0.36.0 h1:HNxciQpQMMOKS0/GiUXcKDyA6J2FDILJj9NmP2BZrTg=
k8s.io/cli-runtime v0.36.0/go.mod h1:KObkknK9Ro5LYX+1RdiKc7C8CvGg4aX+V/Zv+E8WPHA=
k8s.io/client-go v0.35.4 h1:DN6fyaGuzK64UvnKO5fOA6ymSjvfGAnCAHAR0C66kD8=
k8s.io/client-go v0.35.4/go.mod h1:2Pg9WpsS4NeOpoYTfHHfMxBG8zFMSAUi4O/qoiJC3nY=
k8s.io/component-base v0.36.0 h1:hFjEktssxiJhrK1zfybkH4kJOi8iZuF+mIDCqS5+jRo=
k8s.io/component-base v0.36.0/go.mod h1:JZvIfcNHk+uck+8LhJzhSBtydWXaZNQwX2OdL+Mnwsk=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
k8s.io/kubectl v0.36.0 h1:hEGr8NvIm2Wjqs2Xy48Uzmvo6lpHdGKlLyMvau2gTms=
k8s.io/kubectl v0.36.0/go.mod h1:iDe8aV5BEi45W8k+5n71I2pJ/nwE0PHDu+/2cejzYoo=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=
sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE=
sigs.k8s.io/controller-runtime v0.23.1/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0=
sigs.k8s.io/controller-runtime v0.24.0 h1:Ck6N2LdS8Lovy1o25BB4r1xjvLEKUl1s2o9kU+KWDE4=
sigs.k8s.io/controller-runtime v0.24.0/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs=
@@ -1094,7 +1096,7 @@ sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7
sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs=
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8=
sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
+15 -8
View File
@@ -16,17 +16,24 @@ docs_dir: docs
nav:
- Home: index.md
- Getting Started:
- Paths Overview: paths.md
- Templating Funcs: templating_funcs.md
- HCL Funcs: hcl_funcs.md
- Built-in Objects: builtin-objects.md
- Core Concepts:
- Writing Helmfile: writing-helmfile.md
- Values and Merging: values-and-merging.md
- Advanced Features:
- Best Practices Guide: writing-helmfile.md
- Environments: environments.md
- Releases & DAG: releases.md
- Configuration:
- helmfile.yaml Reference: configuration.md
- Templating: templating.md
- Template Functions: templating_funcs.md
- Built-in Objects: builtin-objects.md
- HCL Functions: hcl_funcs.md
- Paths Overview: paths.md
- CLI Reference: cli.md
- Advanced:
- Advanced Features: advanced-features.md
- Hooks: hooks.md
- Secrets: remote-secrets.md
- Shared Configuration Across Teams: shared-configuration-across-teams.md
- Shared Configuration: shared-configuration-across-teams.md
- Integrations: integrations.md
- Experimental Features: experimental-features.md
- About:
- Users: users.md
+275 -110
View File
@@ -162,10 +162,11 @@ func (a *App) Diff(c DiffConfigProvider) error {
includeCRDs := !c.SkipCRDs()
prepErr := run.withPreparedCharts("diff", state.ChartPrepareOptions{
prepErr := run.WithPreparedCharts("diff", state.ChartPrepareOptions{
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
SkipRefresh: c.SkipRefresh(),
SkipDeps: c.SkipDeps(),
SkipSchemaValidation: c.SkipSchemaValidation(),
IncludeCRDs: &includeCRDs,
Validate: c.Validate(),
Concurrency: c.Concurrency(),
@@ -234,10 +235,11 @@ func (a *App) Template(c TemplateConfigProvider) error {
// https://github.com/helmfile/helmfile/issues/1749
run.helm.SetExtraArgs()
prepErr := run.withPreparedCharts("template", state.ChartPrepareOptions{
prepErr := run.WithPreparedCharts("template", state.ChartPrepareOptions{
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
SkipRefresh: c.SkipRefresh(),
SkipDeps: c.SkipDeps(),
SkipSchemaValidation: c.SkipSchemaValidation(),
IncludeCRDs: &includeCRDs,
SkipCleanup: c.SkipCleanup(),
Validate: c.Validate(),
@@ -263,7 +265,7 @@ func (a *App) Template(c TemplateConfigProvider) error {
func (a *App) WriteValues(c WriteValuesConfigProvider) error {
return a.ForEachState(func(run *Run) (ok bool, errs []error) {
prepErr := run.withPreparedCharts("write-values", state.ChartPrepareOptions{
prepErr := run.WithPreparedCharts("write-values", state.ChartPrepareOptions{
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
SkipRefresh: c.SkipRefresh(),
SkipDeps: c.SkipDeps(),
@@ -317,7 +319,7 @@ func (a *App) Lint(c LintConfigProvider) error {
var lintErrs []error
// `helm lint` on helm v2 and v3 does not support remote charts, that we need to set `forceDownload=true` here
prepErr := run.withPreparedCharts("lint", state.ChartPrepareOptions{
prepErr := run.WithPreparedCharts("lint", state.ChartPrepareOptions{
ForceDownload: true,
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
SkipRefresh: c.SkipRefresh(),
@@ -360,7 +362,7 @@ func (a *App) Unittest(c UnittestConfigProvider) error {
var unittestErrs []error
// helm unittest needs local charts, so force download
prepErr := run.withPreparedCharts("unittest", state.ChartPrepareOptions{
prepErr := run.WithPreparedCharts("unittest", state.ChartPrepareOptions{
ForceDownload: true,
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
SkipRefresh: c.SkipRefresh(),
@@ -397,8 +399,50 @@ func (a *App) Unittest(c UnittestConfigProvider) error {
}
func (a *App) Fetch(c FetchConfigProvider) error {
return a.ForEachState(func(run *Run) (ok bool, errs []error) {
prepErr := run.withPreparedCharts("pull", state.ChartPrepareOptions{
if c.WriteOutput() && c.OutputDir() == "" {
return fmt.Errorf("--output-dir is required when --write-output is set")
}
if c.WriteOutput() {
// Force sequential processing to ensure YAML documents are emitted in order
// without interleaving when multiple helmfile state files are processed.
// Restore the original value when Fetch returns so the App instance is not
// permanently mutated (important for tests and library usage).
prev := a.SequentialHelmfiles
a.SequentialHelmfiles = true
defer func() { a.SequentialHelmfiles = prev }()
}
// processedStateFileCount tracks how many state files have been processed when
// --write-output is set; used to detect multi-file inputs early and return
// a clear error instead of silently producing semantically incorrect YAML.
var processedStateFileCount int
// yamlOutput buffers the generated YAML document so that nothing is written to
// stdout until ForEachState completes successfully. This prevents partial/corrupted
// output reaching stdout when a later state file (or chart download error) causes
// the operation to fail.
var yamlOutput strings.Builder
err := a.ForEachState(func(run *Run) (ok bool, errs []error) {
if c.WriteOutput() {
processedStateFileCount++
if processedStateFileCount > 1 {
return false, []error{fmt.Errorf(
"--write-output requires a single helmfile state file, but multiple were found; " +
"use -f to specify a single helmfile instead of a directory or a helmfile with nested helmfiles: entries",
)}
}
// Disable live output to avoid Helm progress/status lines being streamed
// to stdout and corrupting the YAML document emitted by --write-output.
// Restore the original value when this callback returns so the cached helm
// exec instance is not permanently mutated (important for tests and library usage).
run.helm.SetEnableLiveOutput(false)
defer run.helm.SetEnableLiveOutput(a.EnableLiveOutput)
}
prepErr := run.WithPreparedCharts("pull", state.ChartPrepareOptions{
ForceDownload: true,
SkipRefresh: c.SkipRefresh(),
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
@@ -407,6 +451,27 @@ func (a *App) Fetch(c FetchConfigProvider) error {
OutputDirTemplate: c.OutputDirTemplate(),
Concurrency: c.Concurrency(),
}, func() []error {
if c.WriteOutput() {
for i := range run.state.Releases {
rel := &run.state.Releases[i]
if rel.ChartPath != "" {
rel.Chart = rel.ChartPath
rel.ChartPath = ""
}
}
stateYaml, yamlErr := run.state.ToYaml()
if yamlErr != nil {
return []error{yamlErr}
}
sourceFile, pathErr := run.state.FullFilePath()
if pathErr != nil {
return []error{pathErr}
}
fmt.Fprintf(&yamlOutput, "---\n# Source: %s\n\n%s", sourceFile, stateYaml)
}
return nil
})
@@ -414,18 +479,29 @@ func (a *App) Fetch(c FetchConfigProvider) error {
errs = append(errs, prepErr)
}
return
return ok, errs
}, false, false, SetFilter(true))
if err == nil && c.WriteOutput() {
fmt.Print(yamlOutput.String())
}
return err
}
func (a *App) Sync(c SyncConfigProvider) error {
return a.ForEachState(func(run *Run) (ok bool, errs []error) {
var any bool
mut := &sync.Mutex{}
err := a.ForEachState(func(run *Run) (ok bool, errs []error) {
includeCRDs := !c.SkipCRDs()
prepErr := run.withPreparedCharts("sync", state.ChartPrepareOptions{
prepErr := run.WithPreparedCharts("sync", state.ChartPrepareOptions{
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
SkipRefresh: c.SkipRefresh(),
SkipDeps: c.SkipDeps(),
SkipSchemaValidation: c.SkipSchemaValidation(),
Wait: c.Wait(),
WaitRetries: c.WaitRetries(),
WaitForJobs: c.WaitForJobs(),
@@ -435,7 +511,14 @@ func (a *App) Sync(c SyncConfigProvider) error {
Validate: c.Validate(),
Concurrency: c.Concurrency(),
}, func() []error {
ok, errs = a.sync(run, c)
matched, updated, es := a.SyncState(run, c)
mut.Lock()
any = any || updated
mut.Unlock()
ok = matched
errs = es
return errs
})
@@ -445,6 +528,18 @@ func (a *App) Sync(c SyncConfigProvider) error {
return
}, c.IncludeNeeds(), c.IncludeTransitiveNeeds())
if err != nil {
return err
}
if ec, ok := c.(interface{ DetailedExitcode() bool }); ok && ec.DetailedExitcode() && any {
code := 2
return &Error{msg: "", code: &code}
}
return nil
}
func (a *App) Apply(c ApplyConfigProvider) error {
@@ -459,10 +554,11 @@ func (a *App) Apply(c ApplyConfigProvider) error {
err := a.ForEachState(func(run *Run) (ok bool, errs []error) {
includeCRDs := !c.SkipCRDs()
prepErr := run.withPreparedCharts("apply", state.ChartPrepareOptions{
prepErr := run.WithPreparedCharts("apply", state.ChartPrepareOptions{
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
SkipRefresh: c.SkipRefresh(),
SkipDeps: c.SkipDeps(),
SkipSchemaValidation: c.SkipSchemaValidation(),
Wait: c.Wait(),
WaitRetries: c.WaitRetries(),
WaitForJobs: c.WaitForJobs(),
@@ -505,7 +601,7 @@ func (a *App) Apply(c ApplyConfigProvider) error {
func (a *App) Status(c StatusesConfigProvider) error {
return a.ForEachState(func(run *Run) (ok bool, errs []error) {
err := run.withPreparedCharts("status", state.ChartPrepareOptions{
err := run.WithPreparedCharts("status", state.ChartPrepareOptions{
SkipRepos: true,
SkipDeps: true,
Concurrency: c.Concurrency(),
@@ -525,7 +621,7 @@ func (a *App) Status(c StatusesConfigProvider) error {
func (a *App) Destroy(c DestroyConfigProvider) error {
return a.ForEachState(func(run *Run) (ok bool, errs []error) {
if !c.SkipCharts() {
err := run.withPreparedCharts("destroy", state.ChartPrepareOptions{
err := run.WithPreparedCharts("destroy", state.ChartPrepareOptions{
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
SkipRefresh: c.SkipRefresh(),
SkipDeps: c.SkipDeps(),
@@ -554,7 +650,7 @@ func (a *App) Test(c TestConfigProvider) error {
"or set helm.sh/hook-delete-policy\n")
}
err := run.withPreparedCharts("test", state.ChartPrepareOptions{
err := run.WithPreparedCharts("test", state.ChartPrepareOptions{
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
SkipRefresh: c.SkipRefresh(),
SkipDeps: c.SkipDeps(),
@@ -575,7 +671,7 @@ func (a *App) Test(c TestConfigProvider) error {
func (a *App) PrintDAGState(c DAGConfigProvider) error {
var err error
return a.ForEachState(func(run *Run) (ok bool, errs []error) {
err = run.withPreparedCharts("show-dag", state.ChartPrepareOptions{
err = run.WithPreparedCharts("show-dag", state.ChartPrepareOptions{
SkipRepos: true,
SkipDeps: true,
Concurrency: 2,
@@ -592,7 +688,7 @@ func (a *App) PrintDAGState(c DAGConfigProvider) error {
func (a *App) PrintState(c StateConfigProvider) error {
return a.ForEachState(func(run *Run) (_ bool, errs []error) {
err := run.withPreparedCharts("build", state.ChartPrepareOptions{
err := run.WithPreparedCharts("build", state.ChartPrepareOptions{
SkipRepos: true,
SkipDeps: true,
Concurrency: 2,
@@ -665,7 +761,7 @@ func (a *App) ListReleases(c ListConfigProvider) error {
var listErr error
if !c.SkipCharts() {
prepErr := run.withPreparedCharts("list", state.ChartPrepareOptions{
prepErr := run.WithPreparedCharts("list", state.ChartPrepareOptions{
SkipRepos: true,
SkipDeps: true,
Concurrency: 2,
@@ -1015,6 +1111,11 @@ func (a *App) processStateFileParallel(relPath string, defOpts LoadOpts, converg
func (a *App) processNestedHelmfiles(st *state.HelmState, absd, file string, defOpts, opts LoadOpts, converge func(*state.HelmState) (bool, []error), sharedCtx *Context) (bool, error) {
anyMatched := false
for i, m := range st.Helmfiles {
if subhelmfileSelectorsConflict(a.Selectors, m, a.Logger) {
a.Logger.Debugf("skipping subhelmfile %q: CLI selectors %v conflict with subhelmfile selectors %v", m.Path, a.Selectors, m.Selectors)
continue
}
optsForNestedState := LoadOpts{
CalleePath: filepath.Join(absd, file),
Environment: m.Environment,
@@ -1040,6 +1141,24 @@ func (a *App) processNestedHelmfiles(st *state.HelmState, absd, file string, def
return anyMatched, nil
}
// subhelmfileSelectorsConflict returns true when the subhelmfile has explicit
// selectors that are provably incompatible with the CLI selectors,
// meaning no release could satisfy both. In that case the subhelmfile can be
// safely skipped without loading or evaluating it.
// Only CLI selectors (not inherited parent selectors) are used for comparison,
// so this optimization is restricted to cases where the user explicitly
// provided selectors via the command line (e.g. -l name=b).
func subhelmfileSelectorsConflict(cliSelectors []string, m state.SubHelmfileSpec, logger *zap.SugaredLogger) bool {
if len(cliSelectors) == 0 || len(m.Selectors) == 0 || m.SelectorsInherited {
return false
}
compatible, err := state.SelectorsAreCompatible(cliSelectors, m.Selectors)
if err != nil {
logger.Debugf("selector compatibility check failed for subhelmfile %q: %v", m.Path, err)
}
return !compatible
}
func (a *App) visitStatesWithContext(fileOrDir string, defOpts LoadOpts, converge func(*state.HelmState) (bool, []error), sharedCtx *Context) error {
noMatchInHelmfiles := true
@@ -1370,14 +1489,12 @@ func (a *App) visitStatesWithSelectorsAndRemoteSupportWithContext(fileOrDir stri
for _, v := range a.ValuesFiles {
envvals = append(envvals, v)
}
if len(a.Set) > 0 {
envvals = append(envvals, a.Set)
}
if len(envvals) > 0 {
opts.Environment.OverrideValues = envvals
opts.Environment.OverrideValuesAreCLI = true
}
if len(a.Set) > 0 {
opts.Environment.OverrideCLISetValues = []any{a.Set}
}
a.remote = remote.NewRemote(a.Logger, "", a.fs)
@@ -1632,42 +1749,63 @@ func (a *App) getSelectedReleases(r *Run, includeNeeds bool, includeTransitiveNe
return selected, deduplicated, nil
}
func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, bool, []error) {
// GetPlannedAndSelectedReleasesWithNeeds returns the planned releases and the selected releases used for planning.
// The planned releases include dependency releases only when includeNeeds is true and skipNeeds is false.
func (a *App) GetPlannedAndSelectedReleasesWithNeeds(r *Run, skipNeeds bool, includeNeeds bool, includeTransitiveNeeds bool) ([]state.ReleaseSpec, []state.ReleaseSpec, error) {
st := r.state
helm := r.helm
helm.SetExtraArgs(GetArgs(c.Args(), r.state)...)
selectedReleases, selectedAndNeededReleases, err := a.getSelectedReleases(r, c.IncludeNeeds(), c.IncludeTransitiveNeeds())
selectedReleases, selectedAndNeededReleases, err := a.getSelectedReleases(r, includeNeeds, includeTransitiveNeeds)
if err != nil {
return false, false, []error{err}
return nil, nil, err
}
if len(selectedReleases) == 0 {
return false, false, nil
return nil, nil, nil
}
// This is required when you're trying to deduplicate releases by the selector.
// Without this, `PlanReleases` conflates duplicates and return both in `batches`,
// even if we provided `SelectedReleases: selectedReleases`.
// See https://github.com/roboll/helmfile/issues/1818 for more context.
originalReleases := st.Releases
st.Releases = selectedAndNeededReleases
defer func() {
st.Releases = originalReleases
}()
plan, err := st.PlanReleases(state.PlanOptions{Reverse: false, SelectedReleases: selectedReleases, SkipNeeds: c.SkipNeeds(), IncludeNeeds: c.IncludeNeeds(), IncludeTransitiveNeeds: c.IncludeTransitiveNeeds()})
batches, err := st.PlanReleases(state.PlanOptions{Reverse: false, SelectedReleases: selectedReleases, SkipNeeds: skipNeeds, IncludeNeeds: includeNeeds, IncludeTransitiveNeeds: includeTransitiveNeeds})
if err != nil {
return nil, nil, err
}
var releasesWithNeeds []state.ReleaseSpec
for _, rs := range batches {
for _, r := range rs {
releasesWithNeeds = append(releasesWithNeeds, r.ReleaseSpec)
}
}
return releasesWithNeeds, selectedAndNeededReleases, nil
}
func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, bool, []error) {
st := r.state
helm := r.helm
helm.SetExtraArgs(GetArgs(c.Args(), r.state)...)
releasesWithNeeds, selectedAndNeededReleases, err := a.GetPlannedAndSelectedReleasesWithNeeds(r, c.SkipNeeds(), c.IncludeNeeds(), c.IncludeTransitiveNeeds())
if err != nil {
return false, false, []error{err}
}
var toApplyWithNeeds []state.ReleaseSpec
for _, rs := range plan {
for _, r := range rs {
toApplyWithNeeds = append(toApplyWithNeeds, r.ReleaseSpec)
}
if len(releasesWithNeeds) == 0 {
return false, false, nil
}
// Do build deps and prepare only on selected releases so that we won't waste time
// on running various helm commands on unnecessary releases
st.Releases = toApplyWithNeeds
st.Releases = releasesWithNeeds
// helm must be 2.11+ and helm-diff should be provided `--detailed-exitcode` in order for `helmfile apply` to work properly
detailedExitCode := true
@@ -1693,27 +1831,27 @@ func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, bool, []error) {
DetectedKubeVersion: detectedKubeVersion,
}
infoMsg, releasesToBeUpdated, releasesToBeDeleted, errs := r.diff(false, detailedExitCode, c, diffOpts)
if len(errs) > 0 {
return false, false, errs
infoMsg, releasesToUpdate, releasesToDelete, diffErrs := r.diff(false, detailedExitCode, c, diffOpts)
if len(diffErrs) > 0 {
return false, false, diffErrs
}
var toDelete []state.ReleaseSpec
for _, r := range releasesToBeDeleted {
for _, r := range releasesToDelete {
toDelete = append(toDelete, r)
}
var toUpdate []state.ReleaseSpec
for _, r := range releasesToBeUpdated {
for _, r := range releasesToUpdate {
toUpdate = append(toUpdate, r)
}
releasesWithNoChange := map[string]state.ReleaseSpec{}
for _, r := range toApplyWithNeeds {
for _, r := range releasesWithNeeds {
release := r
id := state.ReleaseToID(&release)
_, uninstalled := releasesToBeDeleted[id]
_, updated := releasesToBeUpdated[id]
_, uninstalled := releasesToDelete[id]
_, updated := releasesToUpdate[id]
if !uninstalled && !updated {
releasesWithNoChange[id] = release
}
@@ -1735,19 +1873,18 @@ Do you really want to apply?
a.Logger.Debug(infoMsgStr)
}
var applyErrs []error
affectedReleases := state.AffectedReleases{}
var errs []error
// Traverse DAG of all the releases so that we don't suffer from false-positive missing dependencies
st.Releases = selectedAndNeededReleases
if len(releasesToBeUpdated) == 0 && len(releasesToBeDeleted) == 0 {
if len(releasesToUpdate) == 0 && len(releasesToDelete) == 0 {
return true, false, nil
}
affectedReleases := state.AffectedReleases{}
if !interactive || interactive && r.askForConfirmation(confMsg) {
if _, preapplyErrors := withDAG(st, helm, a.Logger, state.PlanOptions{Purpose: "invoking preapply hooks for", Reverse: true, SelectedReleases: toApplyWithNeeds, SkipNeeds: true}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error {
if _, preapplyErrors := withDAG(st, helm, a.Logger, state.PlanOptions{Purpose: "invoking preapply hooks for", Reverse: true, SelectedReleases: releasesWithNeeds, SkipNeeds: true}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error {
for _, r := range subst.Releases {
release := r
if _, err := st.TriggerPreapplyEvent(&release, "apply"); err != nil {
@@ -1761,13 +1898,13 @@ Do you really want to apply?
}
// We deleted releases by traversing the DAG in reverse order
if len(releasesToBeDeleted) > 0 {
if len(releasesToDelete) > 0 {
_, deletionErrs := withDAG(st, helm, a.Logger, state.PlanOptions{Reverse: true, SelectedReleases: toDelete, SkipNeeds: true}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error {
var rs []state.ReleaseSpec
for _, r := range subst.Releases {
release := r
if r2, ok := releasesToBeDeleted[state.ReleaseToID(&release)]; ok {
if r2, ok := releasesToDelete[state.ReleaseToID(&release)]; ok {
rs = append(rs, r2)
}
}
@@ -1778,18 +1915,18 @@ Do you really want to apply?
}))
if len(deletionErrs) > 0 {
applyErrs = append(applyErrs, deletionErrs...)
errs = append(errs, deletionErrs...)
}
}
// We upgrade releases by traversing the DAG
if len(releasesToBeUpdated) > 0 {
_, updateErrs := withDAG(st, helm, a.Logger, state.PlanOptions{SelectedReleases: toUpdate, Reverse: false, SkipNeeds: true, IncludeTransitiveNeeds: c.IncludeTransitiveNeeds()}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error {
if len(releasesToUpdate) > 0 {
_, updateErrs := withDAG(st, helm, a.Logger, state.PlanOptions{SelectedReleases: toUpdate, SkipNeeds: true, IncludeTransitiveNeeds: c.IncludeTransitiveNeeds()}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error {
var rs []state.ReleaseSpec
for _, r := range subst.Releases {
release := r
if r2, ok := releasesToBeUpdated[state.ReleaseToID(&release)]; ok {
if r2, ok := releasesToUpdate[state.ReleaseToID(&release)]; ok {
rs = append(rs, r2)
}
}
@@ -1816,13 +1953,14 @@ Do you really want to apply?
TrackMode: c.TrackMode(),
TrackTimeout: c.TrackTimeout(),
TrackLogs: c.TrackLogs(),
TrackFailOnError: c.TrackFailOnError(),
Description: c.Description(),
}
return subst.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), syncOpts)
}))
if len(updateErrs) > 0 {
applyErrs = append(applyErrs, updateErrs...)
errs = append(errs, updateErrs...)
}
}
}
@@ -1835,11 +1973,11 @@ Do you really want to apply?
a.Logger.Warnf("warn: %v\n", err)
}
}
if releasesToBeDeleted == nil && releasesToBeUpdated == nil {
if releasesToDelete == nil && releasesToUpdate == nil {
return true, false, nil
}
return true, true, applyErrs
return true, true, errs
}
func (a *App) delete(r *Run, purge bool, c DestroyConfigProvider) (bool, []error) {
@@ -2113,44 +2251,25 @@ func (a *App) status(r *Run, c StatusesConfigProvider) (bool, []error) {
return true, errs
}
func (a *App) sync(r *Run, c SyncConfigProvider) (bool, []error) {
func (a *App) SyncState(r *Run, c SyncConfigProvider) (bool, bool, []error) {
st := r.state
helm := r.helm
selectedReleases, selectedAndNeededReleases, err := a.getSelectedReleases(r, c.IncludeNeeds(), c.IncludeTransitiveNeeds())
releasesWithNeeds, selectedAndNeededReleases, err := a.GetPlannedAndSelectedReleasesWithNeeds(r, c.SkipNeeds(), c.IncludeNeeds(), c.IncludeTransitiveNeeds())
if err != nil {
return false, []error{err}
return false, false, []error{err}
}
if len(selectedReleases) == 0 {
return false, nil
}
// This is required when you're trying to deduplicate releases by the selector.
// Without this, `PlanReleases` conflates duplicates and return both in `batches`,
// even if we provided `SelectedReleases: selectedReleases`.
// See https://github.com/roboll/helmfile/issues/1818 for more context.
st.Releases = selectedAndNeededReleases
batches, err := st.PlanReleases(state.PlanOptions{Reverse: false, SelectedReleases: selectedReleases, IncludeNeeds: c.IncludeNeeds(), IncludeTransitiveNeeds: c.IncludeTransitiveNeeds(), SkipNeeds: c.SkipNeeds()})
if err != nil {
return false, []error{err}
}
var toSyncWithNeeds []state.ReleaseSpec
for _, rs := range batches {
for _, r := range rs {
toSyncWithNeeds = append(toSyncWithNeeds, r.ReleaseSpec)
}
if len(releasesWithNeeds) == 0 {
return false, false, nil
}
// Do build deps and prepare only on selected releases so that we won't waste time
// on running various helm commands on unnecessary releases
st.Releases = toSyncWithNeeds
st.Releases = releasesWithNeeds
toDelete, err := st.DetectReleasesToBeDeletedForSync(helm, toSyncWithNeeds)
toDelete, err := st.DetectReleasesToBeDeletedForSync(helm, releasesWithNeeds)
if err != nil {
return false, []error{err}
return false, false, []error{err}
}
releasesToDelete := map[string]state.ReleaseSpec{}
@@ -2161,7 +2280,7 @@ func (a *App) sync(r *Run, c SyncConfigProvider) (bool, []error) {
}
var toUpdate []state.ReleaseSpec
for _, r := range toSyncWithNeeds {
for _, r := range releasesWithNeeds {
release := r
if _, deleted := releasesToDelete[state.ReleaseToID(&release)]; !deleted {
if r.Desired() {
@@ -2181,7 +2300,7 @@ func (a *App) sync(r *Run, c SyncConfigProvider) (bool, []error) {
}
releasesWithNoChange := map[string]state.ReleaseSpec{}
for _, r := range toSyncWithNeeds {
for _, r := range releasesWithNeeds {
release := r
id := state.ReleaseToID(&release)
_, uninstalled := releasesToDelete[id]
@@ -2191,13 +2310,6 @@ func (a *App) sync(r *Run, c SyncConfigProvider) (bool, []error) {
}
}
for id := range releasesWithNoChange {
r := releasesWithNoChange[id]
if _, err := st.TriggerCleanupEvent(&r, "sync"); err != nil {
a.Logger.Warnf("warn: %v\n", err)
}
}
names := []string{}
for _, r := range releasesToUpdate {
names = append(names, fmt.Sprintf(" %s (%s) UPDATED", r.Name, r.Chart))
@@ -2208,9 +2320,57 @@ func (a *App) sync(r *Run, c SyncConfigProvider) (bool, []error) {
// Make the output deterministic for testing purpose
sort.Strings(names)
infoMsg := fmt.Sprintf(`Affected releases are:
interactive := c.Interactive()
var infoMsg string
var errs []error
r.helm.SetExtraArgs(GetArgs(c.Args(), r.state)...)
operationsAttempted := false
if interactive {
if diffC, ok := c.(DiffConfigProvider); ok {
detectedKubeVersion := a.detectKubeVersion(st)
diffOpts := &state.DiffOpts{
Context: diffC.Context(),
Output: diffC.DiffOutput(),
Color: diffC.Color(),
NoColor: diffC.NoColor(),
Set: diffC.Set(),
DiffArgs: diffC.DiffArgs(),
SkipDiffOnInstall: diffC.SkipDiffOnInstall(),
ReuseValues: diffC.ReuseValues(),
ResetValues: diffC.ResetValues(),
PostRenderer: diffC.PostRenderer(),
PostRendererArgs: diffC.PostRendererArgs(),
SkipSchemaValidation: diffC.SkipSchemaValidation(),
SuppressOutputLineRegex: diffC.SuppressOutputLineRegex(),
TakeOwnership: diffC.TakeOwnership(),
DetectedKubeVersion: detectedKubeVersion,
}
infoMsgPtr, _, _, diffErrs := r.diff(false, diffC.DetailedExitcode(), diffC, diffOpts)
if len(diffErrs) > 0 {
return false, false, diffErrs
}
if infoMsgPtr != nil {
infoMsg = *infoMsgPtr
} else {
infoMsg = fmt.Sprintf(`Affected releases are:
%s
`, strings.Join(names, "\n"))
}
} else {
infoMsg = fmt.Sprintf(`Affected releases are:
%s
`, strings.Join(names, "\n"))
}
} else {
infoMsg = fmt.Sprintf(`Affected releases are:
%s
`, strings.Join(names, "\n"))
a.Logger.Debug(infoMsg)
}
confMsg := fmt.Sprintf(`%s
Do you really want to sync?
@@ -2218,15 +2378,6 @@ Do you really want to sync?
`, infoMsg)
interactive := c.Interactive()
if !interactive {
a.Logger.Debug(infoMsg)
}
var errs []error
r.helm.SetExtraArgs(GetArgs(c.Args(), r.state)...)
// Traverse DAG of all the releases so that we don't suffer from false-positive missing dependencies
st.Releases = selectedAndNeededReleases
@@ -2234,6 +2385,7 @@ Do you really want to sync?
if !interactive || interactive && r.askForConfirmation(confMsg) {
if len(releasesToDelete) > 0 {
operationsAttempted = true
_, deletionErrs := withDAG(st, helm, a.Logger, state.PlanOptions{Reverse: true, SelectedReleases: toDelete, SkipNeeds: true}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error {
var rs []state.ReleaseSpec
@@ -2255,6 +2407,7 @@ Do you really want to sync?
}
if len(releasesToUpdate) > 0 {
operationsAttempted = true
_, syncErrs := withDAG(st, helm, a.Logger, state.PlanOptions{SelectedReleases: toUpdate, SkipNeeds: true, IncludeTransitiveNeeds: c.IncludeTransitiveNeeds()}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error {
var rs []state.ReleaseSpec
@@ -2267,7 +2420,7 @@ Do you really want to sync?
subst.Releases = rs
opts := &state.SyncOpts{
syncOpts := &state.SyncOpts{
Set: c.Set(),
SkipCRDs: c.SkipCRDs(),
Wait: c.Wait(),
@@ -2286,9 +2439,10 @@ Do you really want to sync?
TrackMode: c.TrackMode(),
TrackTimeout: c.TrackTimeout(),
TrackLogs: c.TrackLogs(),
TrackFailOnError: c.TrackFailOnError(),
Description: c.Description(),
}
return subst.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), opts)
return subst.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), syncOpts)
}))
if len(syncErrs) > 0 {
@@ -2296,8 +2450,19 @@ Do you really want to sync?
}
}
}
affectedReleases.DisplayAffectedReleases(c.Logger())
return true, errs
for id := range releasesWithNoChange {
r := releasesWithNoChange[id]
if _, err := st.TriggerCleanupEvent(&r, "sync"); err != nil {
a.Logger.Warnf("warn: %v\n", err)
}
}
changesApplied := operationsAttempted && len(errs) == 0
return true, changesApplied, errs
}
func (a *App) template(r *Run, c TemplateConfigProvider) (bool, []error) {
+219 -1
View File
@@ -14,6 +14,187 @@ import (
"github.com/helmfile/helmfile/pkg/helmexec"
)
func TestSyncInteractive(t *testing.T) {
type testcase struct {
interactive bool
confirm bool
error string
files map[string]string
selectors []string
lists map[exectest.ListKey]string
diffs map[exectest.DiffKey]error
wantDiffs int
upgraded []exectest.Release
deleted []exectest.Release
}
check := func(t *testing.T, tc testcase) {
t.Helper()
wantUpgrades := tc.upgraded
wantDeletes := tc.deleted
var helm = &exectest.Helm{
FailOnUnexpectedList: true,
FailOnUnexpectedDiff: true,
Lists: tc.lists,
Diffs: tc.diffs,
DiffMutex: &sync.Mutex{},
ChartsMutex: &sync.Mutex{},
ReleasesMutex: &sync.Mutex{},
}
bs := runWithLogCapture(t, "debug", func(t *testing.T, logger *zap.SugaredLogger) {
t.Helper()
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Errorf("unexpected error creating vals runtime: %v", err)
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},
valsRuntime: valsRuntime,
}, tc.files)
if tc.selectors != nil {
app.Selectors = tc.selectors
}
// Use ForEachState to gain access to the Run so we can inject Ask
forEachErr := app.ForEachState(func(run *Run) (bool, []error) {
run.Ask = func(msg string) bool {
return tc.confirm
}
ok, _, errs := app.SyncState(run, applyConfig{
concurrency: 1,
interactive: tc.interactive,
skipNeeds: true,
logger: logger,
})
return ok, errs
}, false, false)
var gotErr string
if forEachErr != nil {
gotErr = forEachErr.Error()
}
if d := cmp.Diff(tc.error, gotErr); d != "" {
t.Fatalf("unexpected error: want (-), got (+): %s", d)
}
if len(wantUpgrades) > len(helm.Releases) {
t.Fatalf("insufficient number of upgrades: got %d, want %d", len(helm.Releases), len(wantUpgrades))
}
for relIdx := range wantUpgrades {
if wantUpgrades[relIdx].Name != helm.Releases[relIdx].Name {
t.Errorf("releases[%d].name: got %q, want %q", relIdx, helm.Releases[relIdx].Name, wantUpgrades[relIdx].Name)
}
for flagIdx := range wantUpgrades[relIdx].Flags {
if wantUpgrades[relIdx].Flags[flagIdx] != helm.Releases[relIdx].Flags[flagIdx] {
t.Errorf("releases[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Releases[relIdx].Flags[flagIdx], wantUpgrades[relIdx].Flags[flagIdx])
}
}
}
if len(helm.Diffed) != tc.wantDiffs {
t.Fatalf("unexpected number of diffs: got %d, want %d", len(helm.Diffed), tc.wantDiffs)
}
if len(wantDeletes) > len(helm.Deleted) {
t.Fatalf("insufficient number of deletes: got %d, want %d", len(helm.Deleted), len(wantDeletes))
}
})
_ = bs
}
t.Run("non-interactive: sync proceeds without diff", func(t *testing.T) {
check(t, testcase{
interactive: false,
confirm: false,
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: my-release
chart: incubator/raw
namespace: default
`,
},
upgraded: []exectest.Release{
{Name: "my-release", Flags: []string{"--kube-context", "default", "--namespace", "default"}},
},
lists: map[exectest.ListKey]string{
{Filter: "^my-release$", Flags: listFlags("default", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
my-release 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default
`,
},
})
})
t.Run("interactive with diff: user confirms", func(t *testing.T) {
check(t, testcase{
interactive: true,
confirm: true,
wantDiffs: 1,
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: my-release
chart: incubator/raw
namespace: default
`,
},
upgraded: []exectest.Release{
{Name: "my-release", Flags: []string{"--kube-context", "default", "--namespace", "default"}},
},
diffs: map[exectest.DiffKey]error{
{Name: "my-release", Chart: "incubator/raw", Flags: "--kube-context default --namespace default --reset-values"}: helmexec.ExitError{Code: 2},
},
lists: map[exectest.ListKey]string{
{Filter: "^my-release$", Flags: listFlags("default", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
my-release 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default
`,
},
})
})
t.Run("interactive with diff: user rejects", func(t *testing.T) {
check(t, testcase{
interactive: true,
confirm: false,
wantDiffs: 1,
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: my-release
chart: incubator/raw
namespace: default
`,
},
upgraded: []exectest.Release{},
diffs: map[exectest.DiffKey]error{
{Name: "my-release", Chart: "incubator/raw", Flags: "--kube-context default --namespace default --reset-values"}: helmexec.ExitError{Code: 2},
},
lists: map[exectest.ListKey]string{
{Filter: "^my-release$", Flags: listFlags("default", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
my-release 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default
`,
},
})
})
}
func TestSync(t *testing.T) {
type fields struct {
skipNeeds bool
@@ -27,7 +208,9 @@ func TestSync(t *testing.T) {
concurrency int
timeout int
skipDiffOnInstall bool
detailedExitcode bool
error string
errorCode int
files map[string]string
selectors []string
lists map[exectest.ListKey]string
@@ -88,6 +271,7 @@ func TestSync(t *testing.T) {
skipNeeds: tc.fields.skipNeeds,
includeNeeds: tc.fields.includeNeeds,
includeTransitiveNeeds: tc.fields.includeTransitiveNeeds,
detailedExitcode: tc.detailedExitcode,
})
var gotErr string
@@ -99,6 +283,16 @@ func TestSync(t *testing.T) {
t.Fatalf("unexpected error: want (-), got (+): %s", d)
}
if tc.errorCode >= 0 {
var gotCode int
if appErr, ok := syncErr.(*Error); ok && appErr != nil {
gotCode = appErr.Code()
}
if tc.errorCode != gotCode {
t.Fatalf("unexpected error code: got %d, want %d", gotCode, tc.errorCode)
}
}
if len(wantUpgrades) > len(helm.Releases) {
t.Fatalf("insufficient number of upgrades: got %d, want %d", len(helm.Releases), len(wantUpgrades))
}
@@ -109,7 +303,7 @@ func TestSync(t *testing.T) {
}
for flagIdx := range wantUpgrades[relIdx].Flags {
if wantUpgrades[relIdx].Flags[flagIdx] != helm.Releases[relIdx].Flags[flagIdx] {
t.Errorf("releaes[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Releases[relIdx].Flags[flagIdx], wantUpgrades[relIdx].Flags[flagIdx])
t.Errorf("releases[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Releases[relIdx].Flags[flagIdx], wantUpgrades[relIdx].Flags[flagIdx])
}
}
}
@@ -499,6 +693,30 @@ releases:
lists: map[exectest.ListKey]string{
{Filter: "^my-release$", Flags: listFlags("default", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
my-release 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default
`,
},
})
})
t.Run("detailed-exitcode returns exit code 2 on successful sync", func(t *testing.T) {
check(t, testcase{
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: my-release
chart: incubator/raw
namespace: default
`,
},
detailedExitcode: true,
errorCode: 2,
concurrency: 1,
upgraded: []exectest.Release{
{Name: "my-release", Flags: []string{"--kube-context", "default", "--namespace", "default"}},
},
lists: map[exectest.ListKey]string{
{Filter: "^my-release$", Flags: listFlags("default", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
my-release 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default
`,
},
})
+236
View File
@@ -527,3 +527,239 @@ releases:
})
})
}
func TestTemplate_DefaultInherit(t *testing.T) {
type testcase struct {
error string
templated []exectest.Release
}
check := func(t *testing.T, tc testcase) {
t.Helper()
var helm = &exectest.Helm{
FailOnUnexpectedList: true,
FailOnUnexpectedDiff: true,
DiffMutex: &sync.Mutex{},
ChartsMutex: &sync.Mutex{},
ReleasesMutex: &sync.Mutex{},
}
_ = runWithLogCapture(t, "debug", func(t *testing.T, logger *zap.SugaredLogger) {
t.Helper()
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Errorf("unexpected error creating vals runtime: %v", err)
}
files := map[string]string{
"/path/to/helmfile.yaml": `
templates:
default:
namespace: default-ns
labels:
managed: "true"
defaultInherit: default
releases:
- name: app1
chart: incubator/raw
- name: app2
chart: incubator/raw
inherit:
- template: default
except:
- labels
`,
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: &ffs.FileSystem{Glob: filepath.Glob},
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},
valsRuntime: valsRuntime,
}, files)
tmplErr := app.Template(applyConfig{
concurrency: 1,
logger: logger,
})
var gotErr string
if tmplErr != nil {
gotErr = tmplErr.Error()
}
if d := cmp.Diff(tc.error, gotErr); d != "" {
t.Fatalf("unexpected error: want (-), got (+): %s", d)
}
require.Equal(t, tc.templated, helm.Templated)
})
}
t.Run("default inherit applies template to all releases", func(t *testing.T) {
check(t, testcase{
templated: []exectest.Release{
{Name: "app1", Flags: []string{"--kube-context", "default", "--namespace", "default-ns"}},
{Name: "app2", Flags: []string{"--kube-context", "default", "--namespace", "default-ns"}},
},
})
})
}
func TestTemplate_DefaultInherit_Multiple(t *testing.T) {
type testcase struct {
error string
templated []exectest.Release
}
check := func(t *testing.T, tc testcase) {
t.Helper()
var helm = &exectest.Helm{
FailOnUnexpectedList: true,
FailOnUnexpectedDiff: true,
DiffMutex: &sync.Mutex{},
ChartsMutex: &sync.Mutex{},
ReleasesMutex: &sync.Mutex{},
}
_ = runWithLogCapture(t, "debug", func(t *testing.T, logger *zap.SugaredLogger) {
t.Helper()
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Errorf("unexpected error creating vals runtime: %v", err)
}
files := map[string]string{
"/path/to/helmfile.yaml": `
templates:
ns:
namespace: from-ns-template
override:
namespace: from-ctx-template
defaultInherit:
- ns
- override
releases:
- name: app1
chart: incubator/raw
`,
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: &ffs.FileSystem{Glob: filepath.Glob},
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},
valsRuntime: valsRuntime,
}, files)
tmplErr := app.Template(applyConfig{
concurrency: 1,
logger: logger,
})
var gotErr string
if tmplErr != nil {
gotErr = tmplErr.Error()
}
if d := cmp.Diff(tc.error, gotErr); d != "" {
t.Fatalf("unexpected error: want (-), got (+): %s", d)
}
require.Equal(t, tc.templated, helm.Templated)
})
}
t.Run("multiple default inherits are applied in order", func(t *testing.T) {
check(t, testcase{
templated: []exectest.Release{
{Name: "app1", Flags: []string{"--kube-context", "default", "--namespace", "from-ctx-template"}},
},
})
})
}
func TestTemplate_DefaultInherit_NonExistent(t *testing.T) {
type testcase struct {
error string
}
check := func(t *testing.T, tc testcase) {
t.Helper()
var helm = &exectest.Helm{
FailOnUnexpectedList: true,
FailOnUnexpectedDiff: true,
DiffMutex: &sync.Mutex{},
ChartsMutex: &sync.Mutex{},
ReleasesMutex: &sync.Mutex{},
}
_ = runWithLogCapture(t, "debug", func(t *testing.T, logger *zap.SugaredLogger) {
t.Helper()
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Errorf("unexpected error creating vals runtime: %v", err)
}
files := map[string]string{
"/path/to/helmfile.yaml": `
defaultInherit: nonexistent
releases:
- name: app1
chart: incubator/raw
`,
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: &ffs.FileSystem{Glob: filepath.Glob},
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},
valsRuntime: valsRuntime,
}, files)
tmplErr := app.Template(applyConfig{
concurrency: 1,
logger: logger,
})
var gotErr string
if tmplErr != nil {
gotErr = tmplErr.Error()
}
if d := cmp.Diff(tc.error, gotErr); d != "" {
t.Fatalf("unexpected error: want (-), got (+): %s", d)
}
})
}
t.Run("fail due to non-existent template in defaultInherit", func(t *testing.T) {
check(t, testcase{
error: `in ./helmfile.yaml: failed executing release templates in "helmfile.yaml": release "app1" tried to inherit inexistent release template "nonexistent"`,
})
})
}
+542 -7
View File
@@ -731,8 +731,8 @@ releases:
}{
{label: "duplicatedOK=yes", expectedReleases: []string{"zipkin", "prometheus", "bar", "bar", "grafana", "postgresql"}, expectErr: false},
{label: "name=zipkin", expectedReleases: []string{"zipkin", "prometheus", "grafana", "postgresql"}, expectErr: false},
{label: "name=grafana", expectedReleases: []string{"zipkin", "prometheus", "grafana", "grafana", "postgresql"}, expectErr: false},
{label: "name=doesnotexists", expectedReleases: []string{"zipkin", "prometheus", "grafana", "postgresql"}, expectErr: false},
{label: "name=grafana", expectedReleases: []string{"grafana", "grafana", "postgresql"}, expectErr: false},
{label: "name=doesnotexists", expectedReleases: []string{"grafana", "postgresql"}, expectErr: false},
}
runFilterSubHelmFilesTests(legacyTestcases, files, t, "1st EmbeddedSelectors")
@@ -744,7 +744,7 @@ releases:
errMsg string
}{
{label: "duplicatedOK=yes", expectedReleases: []string{"zipkin", "prometheus", "grafana", "bar", "bar", "grafana", "postgresql"}, expectErr: false},
{label: "name=doesnotexists", expectedReleases: []string{"zipkin", "prometheus", "grafana", "bar", "bar", "grafana", "postgresql"}, expectErr: false},
{label: "name=doesnotexists", expectedReleases: []string{"grafana", "bar", "bar", "grafana", "postgresql"}, expectErr: false},
}
t.Setenv(envvar.Experimental, ExperimentalSelectorExplicit)
@@ -860,6 +860,89 @@ releases:
runFilterSubHelmFilesTests(desiredTestcases, files, t, "2nd inherits")
}
func TestSubhelmfileSelectorsConflict(t *testing.T) {
tests := []struct {
name string
cliSelectors []string
spec state.SubHelmfileSpec
conflict bool
}{
{
name: "no CLI selectors",
cliSelectors: nil,
spec: state.SubHelmfileSpec{Path: "a.yaml", Selectors: []string{"name=a"}},
conflict: false,
},
{
name: "no subhelmfile selectors",
cliSelectors: []string{"name=b"},
spec: state.SubHelmfileSpec{Path: "a.yaml", Selectors: nil},
conflict: false,
},
{
name: "selectorsInherited true",
cliSelectors: []string{"name=b"},
spec: state.SubHelmfileSpec{Path: "a.yaml", Selectors: []string{"name=a"}, SelectorsInherited: true},
conflict: false,
},
{
name: "conflicting selectors",
cliSelectors: []string{"name=b"},
spec: state.SubHelmfileSpec{Path: "a.yaml", Selectors: []string{"name=a"}},
conflict: true,
},
{
name: "matching selectors",
cliSelectors: []string{"name=b"},
spec: state.SubHelmfileSpec{Path: "b.yaml", Selectors: []string{"name=b"}},
conflict: false,
},
{
name: "different keys no conflict",
cliSelectors: []string{"name=b"},
spec: state.SubHelmfileSpec{Path: "a.yaml", Selectors: []string{"env=prod"}},
conflict: false,
},
{
name: "empty CLI selectors slice",
cliSelectors: []string{},
spec: state.SubHelmfileSpec{Path: "a.yaml", Selectors: []string{"name=a"}},
conflict: false,
},
{
name: "empty subhelmfile selectors slice",
cliSelectors: []string{"name=b"},
spec: state.SubHelmfileSpec{Path: "a.yaml", Selectors: []string{}},
conflict: false,
},
{
name: "all pairs conflict with multiple subhelmfile selectors",
cliSelectors: []string{"name=b"},
spec: state.SubHelmfileSpec{Path: "a.yaml", Selectors: []string{"name=a", "name=c"}},
conflict: true,
},
{
name: "one matching pair among multiple subhelmfile selectors",
cliSelectors: []string{"name=b"},
spec: state.SubHelmfileSpec{Path: "a.yaml", Selectors: []string{"name=a", "name=b"}},
conflict: false,
},
{
name: "selectorsInherited false with conflicting selectors still conflicts",
cliSelectors: []string{"name=b"},
spec: state.SubHelmfileSpec{Path: "a.yaml", Selectors: []string{"name=a"}, SelectorsInherited: false},
conflict: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := subhelmfileSelectorsConflict(tt.cliSelectors, tt.spec, newAppTestLogger())
assert.Equal(t, tt.conflict, got)
})
}
}
func runFilterSubHelmFilesTests(testcases []struct {
label string
expectedReleases []string
@@ -1297,6 +1380,67 @@ x:
}
}
// TestStateValuesFileArrayReplace verifies that arrays in --state-values-file
// replace environment default arrays entirely rather than merging element-by-element.
// Regression test for https://github.com/helmfile/helmfile/issues/2536
func TestStateValuesFileArrayReplace(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml.gotmpl": `
environments:
default:
values:
- env-defaults.yaml
---
releases:
- name: {{ .Values.list | join "," }}
chart: stable/noop
`,
// Environment default defines a list with two elements
"/path/to/env-defaults.yaml": `
list:
- first
- second
`,
// --state-values-file overrides the list with a single element
// This should REPLACE the list, not merge element-by-element.
"/path/to/overrides.yaml": `
list:
- second
`,
}
actual := []state.ReleaseSpec{}
collectReleases := func(run *Run) (bool, []error) {
actual = append(actual, run.state.Releases...)
return false, []error{}
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Selectors: []string{},
Env: "default",
ValuesFiles: []string{"overrides.yaml"},
FileOrDir: "helmfile.yaml.gotmpl",
}, files)
expectNoCallsToHelm(app)
err := app.ForEachState(collectReleases, false, false, SetFilter(true))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(actual) != 1 {
t.Fatalf("expected 1 release, got %d", len(actual))
}
// list should be ["second"] (replaced), not ["second","second"] (merged by position)
if actual[0].Name != "second" {
t.Errorf("expected release name %q (list replaced), got %q (list was merged)", "second", actual[0].Name)
}
}
func TestVisitDesiredStatesWithReleasesFiltered_ChartAtAbsPath(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
@@ -2241,6 +2385,8 @@ type configImpl struct {
enforceNeedsAreInstalled bool
skipCharts bool
kubeVersion string
postRenderer string
postRendererArgs []string
}
func (c configImpl) Selectors() []string {
@@ -2328,11 +2474,11 @@ func (c configImpl) SkipCharts() bool {
}
func (c configImpl) PostRenderer() string {
return ""
return c.postRenderer
}
func (c configImpl) PostRendererArgs() []string {
return nil
return c.postRendererArgs
}
func (c configImpl) KubeVersion() string {
@@ -2351,6 +2497,10 @@ func (c configImpl) EnforceNeedsAreInstalled() bool {
return c.enforceNeedsAreInstalled
}
func (c configImpl) WriteOutput() bool {
return false
}
type applyConfig struct {
args string
cascade string
@@ -2403,6 +2553,7 @@ type applyConfig struct {
trackMode string
trackTimeout int
trackLogs bool
trackFailOnError bool
// template-only options
includeCRDs, skipTests bool
@@ -2629,6 +2780,10 @@ func (a applyConfig) TrackLogs() bool {
return a.trackLogs
}
func (a applyConfig) TrackFailOnError() bool {
return a.trackFailOnError
}
func (a applyConfig) Description() string {
return ""
}
@@ -2683,8 +2838,9 @@ func MockExecer(logger *zap.SugaredLogger, kubeContext string) (helmexec.Interfa
// mocking helmexec.Interface
type mockHelmExec struct {
templated []mockTemplates
repos []mockRepo
templated []mockTemplates
repos []mockRepo
enableLiveOutput bool
}
type mockTemplates struct {
@@ -2724,6 +2880,7 @@ func (helm *mockHelmExec) SetHelmBinary(bin string) {
}
func (helm *mockHelmExec) SetEnableLiveOutput(enableLiveOutput bool) {
helm.enableLiveOutput = enableLiveOutput
}
func (helm *mockHelmExec) SetDisableForceUpdate(forceUpdate bool) {
@@ -2951,6 +3108,222 @@ releases:
}
}
// newPostRendererTestApp creates an App and mockHelmExec wired together from a helmfile YAML content map,
// suitable for testing post-renderer flag propagation.
func newPostRendererTestApp(t *testing.T, files map[string]string) (*App, *mockHelmExec) {
t.Helper()
helm := &mockHelmExec{}
var buffer bytes.Buffer
syncWriter := testhelper.NewSyncWriter(&buffer)
logger := helmexec.NewLogger(syncWriter, "debug")
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Fatalf("unexpected error creating vals runtime: %v", err)
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},
valsRuntime: valsRuntime,
}, files)
return app, helm
}
// hasFlagWithValue reports whether flags contains either "--flagName value" as adjacent entries
// or "--flagName=value" as a single entry.
func hasFlagWithValue(flags []string, flagName, value string) bool {
for i, f := range flags {
if f == flagName && i+1 < len(flags) && flags[i+1] == value {
return true
}
if f == flagName+"="+value {
return true
}
}
return false
}
func TestTemplate_HelmDefaultsPostRendererArgs(t *testing.T) {
tests := []struct {
name string
content string
}{
{
name: "single-doc",
content: `
helmDefaults:
postRenderer: foo
postRendererArgs:
- --arg1
- --arg2
releases:
- name: myrelease
chart: stable/mychart
`,
},
{
name: "multi-doc",
content: `
helmDefaults:
postRenderer: foo
postRendererArgs:
- --arg1
- --arg2
---
releases:
- name: myrelease
chart: stable/mychart
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
app, helm := newPostRendererTestApp(t, map[string]string{"/path/to/helmfile.yaml": tt.content})
if err := app.Template(configImpl{}); err != nil {
t.Fatalf("%v", err)
}
if len(helm.templated) != 1 {
t.Fatalf("expected 1 release, got %d", len(helm.templated))
}
flags := helm.templated[0].flags
if !hasFlagWithValue(flags, "--post-renderer", "foo") {
t.Errorf("expected --post-renderer foo in flags, got %v", flags)
}
if !hasFlagWithValue(flags, "--post-renderer-args", "--arg1") {
t.Errorf("expected --post-renderer-args=--arg1 or --post-renderer-args --arg1 in flags, got %v", flags)
}
if !hasFlagWithValue(flags, "--post-renderer-args", "--arg2") {
t.Errorf("expected --post-renderer-args=--arg2 or --post-renderer-args --arg2 in flags, got %v", flags)
}
})
}
}
func TestTemplate_CLIPostRendererArgsOverridesHelmDefaults(t *testing.T) {
tests := []struct {
name string
content string
}{
{
name: "single-doc",
content: `
helmDefaults:
postRenderer: foo
postRendererArgs:
- --default-arg
releases:
- name: myrelease
chart: stable/mychart
`,
},
{
name: "multi-doc",
content: `
helmDefaults:
postRenderer: foo
postRendererArgs:
- --default-arg
---
releases:
- name: myrelease
chart: stable/mychart
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
app, helm := newPostRendererTestApp(t, map[string]string{"/path/to/helmfile.yaml": tt.content})
// CLI provides --post-renderer-args which should override helmDefaults.postRendererArgs
if err := app.Template(configImpl{postRendererArgs: []string{"--cli-arg"}}); err != nil {
t.Fatalf("%v", err)
}
if len(helm.templated) != 1 {
t.Fatalf("expected 1 release, got %d", len(helm.templated))
}
flags := helm.templated[0].flags
if !hasFlagWithValue(flags, "--post-renderer-args", "--cli-arg") {
t.Errorf("expected --post-renderer-args=--cli-arg or --post-renderer-args --cli-arg in flags (CLI should override helmDefaults), got %v", flags)
}
if hasFlagWithValue(flags, "--post-renderer-args", "--default-arg") {
t.Errorf("unexpected --post-renderer-args --default-arg in flags (CLI should override helmDefaults), got %v", flags)
}
})
}
}
func TestTemplate_ReleasePostRendererArgsOverridesCLI(t *testing.T) {
tests := []struct {
name string
content string
}{
{
name: "single-doc",
content: `
helmDefaults:
postRenderer: foo
releases:
- name: myrelease
chart: stable/mychart
postRendererArgs:
- --release-arg
`,
},
{
name: "multi-doc",
content: `
helmDefaults:
postRenderer: foo
---
releases:
- name: myrelease
chart: stable/mychart
postRendererArgs:
- --release-arg
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
app, helm := newPostRendererTestApp(t, map[string]string{"/path/to/helmfile.yaml": tt.content})
// Release explicitly sets postRendererArgs, CLI also provides --post-renderer-args flag; release should win
if err := app.Template(configImpl{postRendererArgs: []string{"--cli-arg"}}); err != nil {
t.Fatalf("%v", err)
}
if len(helm.templated) != 1 {
t.Fatalf("expected 1 release, got %d", len(helm.templated))
}
flags := helm.templated[0].flags
if !hasFlagWithValue(flags, "--post-renderer-args", "--release-arg") {
t.Errorf("expected --post-renderer-args=--release-arg or --post-renderer-args --release-arg in flags (release should override CLI), got %v", flags)
}
if hasFlagWithValue(flags, "--post-renderer-args", "--cli-arg") {
t.Errorf("unexpected --post-renderer-args --cli-arg in flags (release should override CLI), got %v", flags)
}
})
}
}
func TestApply(t *testing.T) {
type fields struct {
skipNeeds bool
@@ -4250,6 +4623,168 @@ releases:
"state should contain source helmfile name:\n%s\n", out)
}
type fetchConfigImpl struct {
configImpl
outputDir string
outputDirTemplate string
writeOutput bool
}
func (f fetchConfigImpl) OutputDir() string {
return f.outputDir
}
func (f fetchConfigImpl) OutputDirTemplate() string {
return f.outputDirTemplate
}
func (f fetchConfigImpl) WriteOutput() bool {
return f.writeOutput
}
func TestFetch_WriteOutputRequiresOutputDir(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: myrelease1
chart: mychart1
`,
}
var buffer bytes.Buffer
syncWriter := testhelper.NewSyncWriter(&buffer)
logger := helmexec.NewLogger(syncWriter, "debug")
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
Namespace: "testNamespace",
}, files)
expectNoCallsToHelm(app)
err := app.Fetch(fetchConfigImpl{
writeOutput: true,
outputDir: "",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "--output-dir is required")
}
func TestFetch_WriteOutput_ErrorsOnMultipleStateFiles(t *testing.T) {
// Two separate helmfile state files in a helmfile.d directory simulate the
// multi-file scenario that --write-output cannot safely handle: the resulting
// multi-document YAML stream would be merged by Helmfile in a way that can
// alter semantics (helmDefaults override, broken relative paths, etc.).
files := map[string]string{
"/path/to/helmfile.d/first.yaml": `
releases:
- name: release1
chart: chart1
`,
"/path/to/helmfile.d/second.yaml": `
releases:
- name: release2
chart: chart2
`,
}
var buffer bytes.Buffer
syncWriter := testhelper.NewSyncWriter(&buffer)
logger := helmexec.NewLogger(syncWriter, "debug")
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Fatalf("unexpected error creating vals runtime: %v", err)
}
helm := &mockHelmExec{}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey(DefaultHelmBinary, "default"): helm,
},
Namespace: "testNamespace",
valsRuntime: valsRuntime,
}, files)
outputDir := t.TempDir()
fetchErr := app.Fetch(fetchConfigImpl{
writeOutput: true,
outputDir: outputDir,
})
assert.Error(t, fetchErr, "expected error when --write-output is used with multiple state files")
assert.Contains(t, fetchErr.Error(), "--write-output requires a single helmfile state file")
}
func TestFetch_WriteOutputRestoresSequentialHelmfiles(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: myrelease1
chart: mychart1
`,
}
var buffer bytes.Buffer
syncWriter := testhelper.NewSyncWriter(&buffer)
logger := helmexec.NewLogger(syncWriter, "debug")
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Fatalf("unexpected error creating vals runtime: %v", err)
}
// Use a real mock helm exec (not noCallHelmExec) so that Fetch can proceed
// past the validation check and enter the SequentialHelmfiles mutation block.
// Start with enableLiveOutput = true so the restore path is actually exercised:
// Fetch will call SetEnableLiveOutput(false), then the deferred restore call
// SetEnableLiveOutput(true) (a.EnableLiveOutput). If the defer were missing,
// helm.enableLiveOutput would remain false and the assertion below would fail.
helm := &mockHelmExec{enableLiveOutput: true}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey(DefaultHelmBinary, "default"): helm,
},
Namespace: "testNamespace",
valsRuntime: valsRuntime,
// Start with SequentialHelmfiles = false; it must be restored after Fetch.
SequentialHelmfiles: false,
// Start with EnableLiveOutput = true; the deferred restore must bring it back.
EnableLiveOutput: true,
}, files)
outputDir := t.TempDir()
// Fetch with --write-output + --output-dir enters the mutation block,
// temporarily sets SequentialHelmfiles = true and helm.EnableLiveOutput = false,
// then restores both when it returns.
_ = app.Fetch(fetchConfigImpl{
writeOutput: true,
outputDir: outputDir,
})
assert.False(t, app.SequentialHelmfiles, "SequentialHelmfiles should be restored to false after Fetch returns")
assert.True(t, helm.enableLiveOutput, "helm.enableLiveOutput should be restored to true (a.EnableLiveOutput) after Fetch returns")
}
func TestList(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.d/first.yaml": `
+11
View File
@@ -93,6 +93,7 @@ type ApplyConfigProvider interface {
TrackMode() string
TrackTimeout() int
TrackLogs() bool
TrackFailOnError() bool
Description() string
@@ -132,6 +133,7 @@ type SyncConfigProvider interface {
TrackMode() string
TrackTimeout() int
TrackLogs() bool
TrackFailOnError() bool
Description() string
@@ -244,6 +246,7 @@ type FetchConfigProvider interface {
SkipRefresh() bool
OutputDir() string
OutputDirTemplate() string
WriteOutput() bool
concurrencyConfig
}
@@ -328,6 +331,14 @@ type InitConfigProvider interface {
Force() bool
}
type CreateConfigProvider interface {
Name() string
OutputDir() string
Force() bool
loggingConfig
}
type PrintEnvConfigProvider interface {
Output() string
}
+145
View File
@@ -0,0 +1,145 @@
package app
import (
"fmt"
"os"
"path/filepath"
"strings"
)
const (
helmfileYAMLTemplate = `# Helmfile configuration
# Documentation: https://helmfile.readthedocs.io/
# Common Helm defaults applied to all releases
helmDefaults:
createNamespace: true
wait: true
timeout: 300
# # Helm chart repositories
# repositories:
# - name: bitnami
# url: https://charts.bitnami.com/bitnami
# - name: ingress-nginx
# url: https://kubernetes.github.io/ingress-nginx
# - name: prometheus-community
# url: https://prometheus-community.github.io/helm-charts
# # Environment-specific values
# # Usage: helmfile -e <environment> apply
# environments:
# default:
# values:
# - environments/default.yaml
# staging:
# values:
# - environments/staging.yaml
# production:
# values:
# - environments/production.yaml
# # Helm releases
# releases:
# - name: my-app
# namespace: my-app
# chart: bitnami/nginx
# version: ~18.0.0
# values:
# - values/my-app.yaml
# # secrets:
# # - secrets/my-app.yaml
# # hooks:
# # - events: ["presync"]
# # command: kubectl
# # args: ["apply", "-f", "manifests/"]
`
envDefaultYAMLTemplate = `# Default environment values
# These values are available in helmfile.yaml as {{ .Values }}
# Example:
# replicaCount: 1
# image:
# repository: nginx
# tag: latest
`
)
func (a *App) Create(c CreateConfigProvider) error {
outputDir := c.OutputDir()
absDir, err := filepath.Abs(outputDir)
if err != nil {
return appError("", fmt.Errorf("failed to resolve output directory: %w", err))
}
// Scaffold file paths (intermediate directories may not exist yet).
helmfilePath := filepath.Join(absDir, "helmfile.yaml")
envFilePath := filepath.Join(absDir, "environments", "default.yaml")
gitkeepPath := filepath.Join(absDir, "values", ".gitkeep")
// Preflight: when --force is not set, check all scaffold paths before
// writing anything so the command fails atomically rather than leaving a
// partially-written project directory.
if !c.Force() {
var existing []string
for _, p := range []string{helmfilePath, envFilePath, gitkeepPath} {
_, statErr := os.Stat(p)
if statErr == nil {
existing = append(existing, p)
} else if !os.IsNotExist(statErr) {
return appError("", fmt.Errorf("failed to check %s: %w", p, statErr))
}
}
if len(existing) > 0 {
return appError("", fmt.Errorf("the following files already exist, use --force to overwrite: %s", strings.Join(existing, ", ")))
}
}
// Create directories.
for _, dir := range []string{absDir, filepath.Join(absDir, "environments"), filepath.Join(absDir, "values")} {
if err := os.MkdirAll(dir, 0o755); err != nil {
return appError("", fmt.Errorf("failed to create directory %s: %w", dir, err))
}
}
// Write scaffold files.
files := []struct {
path string
content []byte
}{
{helmfilePath, []byte(helmfileYAMLTemplate)},
{envFilePath, []byte(envDefaultYAMLTemplate)},
{gitkeepPath, []byte("")},
}
for _, f := range files {
if err := writeScaffoldFile(f.path, f.content, c.Force()); err != nil {
return appError("", fmt.Errorf("failed to write %s: %w", f.path, err))
}
c.Logger().Infof("created %s", f.path)
}
c.Logger().Infof("\nhelmfile project created in %s\n\nNext steps:\n cd %s\n # Edit helmfile.yaml to add your releases\n helmfile apply", absDir, absDir)
return nil
}
// writeScaffoldFile writes content to path. When force is false it uses
// O_EXCL so that a file appearing between the preflight check and the write
// is caught rather than silently overwritten (TOCTOU protection).
func writeScaffoldFile(path string, content []byte, force bool) error {
if force {
return os.WriteFile(path, content, 0o644)
}
f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644)
if err != nil {
if os.IsExist(err) {
return fmt.Errorf("file %s already exists, use --force to overwrite: %w", path, err)
}
return err
}
_, werr := f.Write(content)
cerr := f.Close()
if werr != nil {
return werr
}
return cerr
}
+208
View File
@@ -0,0 +1,208 @@
package app
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)
// mockCreateConfigProvider is a test double for CreateConfigProvider.
type mockCreateConfigProvider struct {
name string
outputDir string
force bool
logger *zap.SugaredLogger
}
func (m *mockCreateConfigProvider) Name() string { return m.name }
func (m *mockCreateConfigProvider) OutputDir() string { return m.outputDir }
func (m *mockCreateConfigProvider) Force() bool { return m.force }
func (m *mockCreateConfigProvider) Logger() *zap.SugaredLogger {
if m.logger != nil {
return m.logger
}
return newTestLogger()
}
func newMockCreateConfig(outputDir string, force bool) *mockCreateConfigProvider {
return &mockCreateConfigProvider{outputDir: outputDir, force: force}
}
func TestCreate_NewDirectory(t *testing.T) {
dir := t.TempDir()
outDir := filepath.Join(dir, "myproject")
a := &App{}
cfg := newMockCreateConfig(outDir, false)
require.NoError(t, a.Create(cfg))
// Verify all scaffold files were created.
assertFileContent(t, filepath.Join(outDir, "helmfile.yaml"), helmfileYAMLTemplate)
assertFileContent(t, filepath.Join(outDir, "environments", "default.yaml"), envDefaultYAMLTemplate)
assertFileExists(t, filepath.Join(outDir, "values", ".gitkeep"))
}
func TestCreate_CurrentDirectory(t *testing.T) {
dir := t.TempDir()
a := &App{}
cfg := newMockCreateConfig(dir, false)
require.NoError(t, a.Create(cfg))
assertFileContent(t, filepath.Join(dir, "helmfile.yaml"), helmfileYAMLTemplate)
assertFileContent(t, filepath.Join(dir, "environments", "default.yaml"), envDefaultYAMLTemplate)
assertFileExists(t, filepath.Join(dir, "values", ".gitkeep"))
}
func TestCreate_ExistingHelmfileYAMLNoForce(t *testing.T) {
dir := t.TempDir()
// Pre-create helmfile.yaml
require.NoError(t, os.WriteFile(filepath.Join(dir, "helmfile.yaml"), []byte("existing"), 0o644))
a := &App{}
cfg := newMockCreateConfig(dir, false)
err := a.Create(cfg)
require.Error(t, err)
assert.Contains(t, err.Error(), "already exist")
assert.Contains(t, err.Error(), "--force")
// Verify the existing file was not overwritten.
content, readErr := os.ReadFile(filepath.Join(dir, "helmfile.yaml"))
require.NoError(t, readErr)
assert.Equal(t, "existing", string(content))
}
func TestCreate_ExistingEnvDefaultYAMLNoForce(t *testing.T) {
dir := t.TempDir()
envDir := filepath.Join(dir, "environments")
require.NoError(t, os.MkdirAll(envDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(envDir, "default.yaml"), []byte("existing"), 0o644))
a := &App{}
cfg := newMockCreateConfig(dir, false)
err := a.Create(cfg)
require.Error(t, err)
assert.Contains(t, err.Error(), "already exist")
assert.Contains(t, err.Error(), "--force")
// Verify the existing file was not overwritten.
content, readErr := os.ReadFile(filepath.Join(envDir, "default.yaml"))
require.NoError(t, readErr)
assert.Equal(t, "existing", string(content))
}
func TestCreate_ExistingGitkeepNoForce(t *testing.T) {
dir := t.TempDir()
valuesDir := filepath.Join(dir, "values")
require.NoError(t, os.MkdirAll(valuesDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(valuesDir, ".gitkeep"), []byte("existing"), 0o644))
a := &App{}
cfg := newMockCreateConfig(dir, false)
err := a.Create(cfg)
require.Error(t, err)
assert.Contains(t, err.Error(), "already exist")
assert.Contains(t, err.Error(), "--force")
// Verify the existing file was not overwritten.
content, readErr := os.ReadFile(filepath.Join(valuesDir, ".gitkeep"))
require.NoError(t, readErr)
assert.Equal(t, "existing", string(content))
}
// TestCreate_PreflightAtomicOnLaterConflict verifies that when only a later
// scaffold file exists (e.g. environments/default.yaml but not helmfile.yaml),
// the preflight check catches it and no files are written at all.
func TestCreate_PreflightAtomicOnLaterConflict(t *testing.T) {
dir := t.TempDir()
envDir := filepath.Join(dir, "environments")
require.NoError(t, os.MkdirAll(envDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(envDir, "default.yaml"), []byte("existing"), 0o644))
a := &App{}
cfg := newMockCreateConfig(dir, false)
err := a.Create(cfg)
require.Error(t, err)
assert.Contains(t, err.Error(), "already exist")
// helmfile.yaml must NOT have been created (preflight aborted before any write).
_, statErr := os.Stat(filepath.Join(dir, "helmfile.yaml"))
assert.True(t, os.IsNotExist(statErr), "helmfile.yaml should not have been created")
}
func TestCreate_ExistingFilesWithForce(t *testing.T) {
dir := t.TempDir()
// Pre-create all scaffold files with different content.
require.NoError(t, os.WriteFile(filepath.Join(dir, "helmfile.yaml"), []byte("old"), 0o644))
envDir := filepath.Join(dir, "environments")
require.NoError(t, os.MkdirAll(envDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(envDir, "default.yaml"), []byte("old"), 0o644))
valuesDir := filepath.Join(dir, "values")
require.NoError(t, os.MkdirAll(valuesDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(valuesDir, ".gitkeep"), []byte("old"), 0o644))
a := &App{}
cfg := newMockCreateConfig(dir, true)
require.NoError(t, a.Create(cfg))
// Verify scaffold files were overwritten with the template content.
assertFileContent(t, filepath.Join(dir, "helmfile.yaml"), helmfileYAMLTemplate)
assertFileContent(t, filepath.Join(dir, "environments", "default.yaml"), envDefaultYAMLTemplate)
assertFileExists(t, filepath.Join(dir, "values", ".gitkeep"))
}
// assertFileContent asserts that the file at path exists and contains wantContent.
func assertFileContent(t *testing.T, path, wantContent string) {
t.Helper()
content, err := os.ReadFile(path)
require.NoError(t, err, "file %s should exist", path)
assert.Equal(t, wantContent, string(content))
}
// assertFileExists asserts that the file at path exists.
func assertFileExists(t *testing.T, path string) {
t.Helper()
_, err := os.Stat(path)
assert.NoError(t, err, "file %s should exist", path)
}
func TestWriteScaffoldFile_CreatesNewFile(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "new.yaml")
require.NoError(t, writeScaffoldFile(path, []byte("hello"), false))
assertFileContent(t, path, "hello")
}
func TestWriteScaffoldFile_ExistingFileNoForce(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "existing.yaml")
require.NoError(t, os.WriteFile(path, []byte("original"), 0o644))
err := writeScaffoldFile(path, []byte("new"), false)
require.Error(t, err)
assert.Contains(t, err.Error(), "--force")
// Original content must be unchanged.
assertFileContent(t, path, "original")
}
func TestWriteScaffoldFile_ExistingFileWithForce(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "existing.yaml")
require.NoError(t, os.WriteFile(path, []byte("original"), 0o644))
require.NoError(t, writeScaffoldFile(path, []byte("new"), true))
assertFileContent(t, path, "new")
}
+23 -22
View File
@@ -50,30 +50,39 @@ type desiredStateLoader struct {
func (ld *desiredStateLoader) Load(f string, opts LoadOpts) (*state.HelmState, error) {
var overrodeEnv *environment.Environment
args := opts.Environment.OverrideValues
fileArgs := opts.Environment.OverrideValues
setArgs := opts.Environment.OverrideCLISetValues
if len(args) > 0 {
if len(fileArgs) > 0 || len(setArgs) > 0 {
if opts.CalleePath == "" {
return nil, fmt.Errorf("bug: opts.CalleePath was nil: f=%s, opts=%v", f, opts)
return nil, fmt.Errorf("bug: opts.CalleePath was empty: f=%s, opts=%v", f, opts)
}
storage := state.NewStorage(opts.CalleePath, ld.logger, ld.fs)
envld := state.NewEnvironmentValuesLoader(storage, ld.fs, ld.logger, ld.remote)
handler := state.MissingFileHandlerError
vals, err := envld.LoadEnvironmentValues(&handler, args, environment.New(ld.env), ld.env)
if err != nil {
return nil, err
overrodeEnv = &environment.Environment{
Name: ld.env,
Values: map[string]any{},
CLIOverrides: map[string]any{},
}
if opts.Environment.OverrideValuesAreCLI {
overrodeEnv = &environment.Environment{
Name: ld.env,
CLIOverrides: vals,
// --state-values-file: loaded into Values so arrays replace (not merge)
if len(fileArgs) > 0 {
fileVals, err := envld.LoadEnvironmentValues(&handler, fileArgs, environment.New(ld.env), ld.env, "")
if err != nil {
return nil, err
}
} else {
overrodeEnv = &environment.Environment{
Name: ld.env,
Values: vals,
overrodeEnv.Values = fileVals
}
// --state-values-set: loaded into CLIOverrides so arrays merge element-by-element
if len(setArgs) > 0 {
setVals, err := envld.LoadEnvironmentValues(&handler, setArgs, environment.New(ld.env), ld.env, "")
if err != nil {
return nil, err
}
overrodeEnv.CLIOverrides = setVals
}
}
@@ -270,14 +279,6 @@ func (ld *desiredStateLoader) load(env, overrodeEnv *environment.Environment, ba
finalState.RenderedValues = currentState.RenderedValues
}
if len(finalState.HelmDefaults.PostRendererArgs) > 0 {
for i := range finalState.Releases {
if len(finalState.Releases[i].PostRendererArgs) == 0 {
finalState.Releases[i].PostRendererArgs = finalState.HelmDefaults.PostRendererArgs
}
}
finalState.HelmDefaults.PostRendererArgs = nil
}
env = &finalState.Env
ld.logger.Debugf("merged environment: %v", env)
+2 -2
View File
@@ -20,7 +20,7 @@ import (
const (
HelmRequiredVersion = "v3.18.6" // Minimum required version (supports Helm 3.x and 4.x)
HelmDiffRecommendedVersion = "v3.15.3"
HelmRecommendedVersion = "v4.1.0" // Recommended to use latest Helm 4
HelmRecommendedVersion = "v4.2.0" // Recommended Helm 4 version
HelmSecretsRecommendedVersion = "v4.7.4" // v4.7.0+ works with both Helm 3 (single plugin) and Helm 4 (split plugin architecture)
HelmGitRecommendedVersion = "v1.3.0"
HelmS3RecommendedVersion = "v0.16.3"
@@ -217,7 +217,7 @@ func (h *HelmfileInit) CheckHelmPlugins() error {
if err != nil {
return err
}
err = helm.UpdatePlugin(p.name)
err = helm.UpdatePlugin(p.name, p.repo, p.version)
if err != nil {
// Check if plugin was updated despite the error
updatedVersion, verifyErr := helmexec.GetPluginVersion(p.name, pluginsDir)
+135
View File
@@ -11,6 +11,7 @@ import (
"strings"
"testing"
"github.com/Masterminds/semver/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
@@ -207,3 +208,137 @@ func TestCheckHelmPlugins_InstallErrorPluginTrulyMissing(t *testing.T) {
assert.Error(t, err)
assert.Contains(t, err.Error(), "sh: not found")
}
func TestCheckHelmPlugins_UpdateFailsFallbackToReinstall(t *testing.T) {
pluginsDir := t.TempDir()
t.Setenv("HELM_PLUGINS", pluginsDir)
// Pre-populate plugins with outdated versions so the update path is triggered.
for _, p := range helmPlugins {
createPluginYAML(t, pluginsDir, p.name, p.name, "0.0.1")
}
// Track which plugin sub-commands were executed.
var calledOps []string
// The mock runner simulates "helm plugin update" failing and falling back to
// "helm plugin uninstall" + "helm plugin install" which succeeds and writes the
// required version to disk.
runner := &initMockRunner{
executeFunc: func(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
for _, a := range args {
if a == "--short" {
return []byte("v3.18.6"), nil
}
}
if len(args) >= 2 && args[0] == "plugin" {
switch args[1] {
case "update":
if len(args) >= 3 {
calledOps = append(calledOps, "update:"+args[2])
}
// Simulate helm plugin update failing (as can happen with Helm 4)
return nil, helmexec.ExitError{Message: "plugin update failed", Code: 1}
case "uninstall":
if len(args) >= 3 {
calledOps = append(calledOps, "uninstall:"+args[2])
}
// Simulate successful uninstall
return []byte{}, nil
case "install":
// Find which plugin is being installed by matching the repo URL.
if len(args) >= 3 {
repo := args[2]
for _, p := range helmPlugins {
if p.repo == repo {
calledOps = append(calledOps, "install:"+p.name)
createPluginYAML(t, pluginsDir, p.name, p.name, strings.TrimPrefix(p.version, "v"))
break
}
}
}
return []byte{}, nil
}
}
return []byte{}, nil
},
}
h := NewHelmfileInit("helm", &mockInitConfigProvider{force: true}, newTestLogger(), runner)
err := h.CheckHelmPlugins()
// Should succeed: update failed but fallback reinstall updated the plugin
assert.NoError(t, err)
// Verify that for each plugin the fallback path was taken:
// update was attempted, then uninstall + install were called.
for _, p := range helmPlugins {
assert.Contains(t, calledOps, "update:"+p.name, "expected update to be attempted for plugin %s", p.name)
assert.Contains(t, calledOps, "uninstall:"+p.name, "expected uninstall to be called for plugin %s", p.name)
assert.Contains(t, calledOps, "install:"+p.name, "expected install to be called for plugin %s", p.name)
}
// Verify that all plugins are now at (or above) the required version on disk.
for _, p := range helmPlugins {
requiredVersion, err := semver.NewVersion(p.version)
require.NoError(t, err)
installedVersion, err := helmexec.GetPluginVersion(p.name, pluginsDir)
require.NoError(t, err, "plugin %s should be present after reinstall", p.name)
assert.False(t, installedVersion.LessThan(requiredVersion),
"plugin %s: installed version %s should be >= required version %s", p.name, installedVersion, requiredVersion)
}
}
func TestCheckHelmPlugins_UpdateErrorButPluginAtRequiredVersion(t *testing.T) {
pluginsDir := t.TempDir()
t.Setenv("HELM_PLUGINS", pluginsDir)
// Pre-populate plugins with outdated versions so the update path is triggered.
for _, p := range helmPlugins {
createPluginYAML(t, pluginsDir, p.name, p.name, "0.0.1")
}
// The mock runner simulates:
// 1. "helm plugin update" failing
// 2. "helm plugin uninstall" succeeding
// 3. "helm plugin install" writing the correct version but returning an error
// (e.g., post-install script error on Windows)
// In this case, UpdatePlugin returns the install error, but CheckHelmPlugins
// verifies the version and warns instead of returning an error.
runner := &initMockRunner{
executeFunc: func(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
for _, a := range args {
if a == "--short" {
return []byte("v3.18.6"), nil
}
}
if len(args) >= 2 && args[0] == "plugin" {
switch args[1] {
case "update":
return nil, helmexec.ExitError{Message: "plugin update failed", Code: 1}
case "uninstall":
return []byte{}, nil
case "install":
// Write the correct version to disk, then return an error
// (simulates post-install script failure on Windows)
if len(args) >= 3 {
repo := args[2]
for _, p := range helmPlugins {
if p.repo == repo {
createPluginYAML(t, pluginsDir, p.name, p.name, strings.TrimPrefix(p.version, "v"))
break
}
}
}
return nil, helmexec.ExitError{Message: "post-install script failed", Code: 1}
}
}
return []byte{}, nil
},
}
h := NewHelmfileInit("helm", &mockInitConfigProvider{force: true}, newTestLogger(), runner)
err := h.CheckHelmPlugins()
// Should succeed: UpdatePlugin returned an error (from the fallback install step),
// but the plugin is present at the required version, so CheckHelmPlugins warns and continues.
assert.NoError(t, err)
}
+11 -1
View File
@@ -30,7 +30,17 @@ func (o LoadOpts) DeepCopy() LoadOpts {
panic(err)
}
new.Environment.OverrideValuesAreCLI = o.Environment.OverrideValuesAreCLI
if src := o.Environment.OverrideCLISetValues; src != nil {
b, err := yaml.Marshal(src)
if err != nil {
panic(err)
}
var dst []any
if err := yaml.Unmarshal(b, &dst); err != nil {
panic(err)
}
new.Environment.OverrideCLISetValues = dst
}
return new
}
+22 -5
View File
@@ -21,16 +21,33 @@ func TestLoadOptsDeepCopy(t *testing.T) {
require.Equal(t, lOld, lNew, "DeepCopy should return a copy of the LoadOpts struct")
}
// TestLoadOptsDeepCopyPreservesOverrideValuesAreCLI verifies that DeepCopy
// preserves the OverrideValuesAreCLI flag which is tagged yaml:"-".
func TestLoadOptsDeepCopyPreservesOverrideValuesAreCLI(t *testing.T) {
// TestLoadOptsDeepCopyPreservesOverrideCLISetValues verifies that DeepCopy
// preserves the OverrideCLISetValues field which is tagged yaml:"-".
func TestLoadOptsDeepCopyPreservesOverrideCLISetValues(t *testing.T) {
lOld := LoadOpts{
Selectors: []string{"test"},
CalleePath: "test",
}
lOld.Environment.OverrideValuesAreCLI = true
lOld.Environment.OverrideCLISetValues = []any{map[string]any{"key": "value"}}
lNew := lOld.DeepCopy()
require.True(t, lNew.Environment.OverrideValuesAreCLI, "DeepCopy should preserve OverrideValuesAreCLI flag")
require.Equal(t, lOld.Environment.OverrideCLISetValues, lNew.Environment.OverrideCLISetValues, "DeepCopy should preserve OverrideCLISetValues field")
}
// TestLoadOptsDeepCopyOverrideCLISetValuesIsNotShallow verifies that mutating a
// map nested inside OverrideCLISetValues on the copy does not affect the
// original.
func TestLoadOptsDeepCopyOverrideCLISetValuesIsNotShallow(t *testing.T) {
lOld := LoadOpts{}
lOld.Environment.OverrideCLISetValues = []any{map[string]any{"key": "original"}}
lNew := lOld.DeepCopy()
// Mutate the map inside the copy.
lNew.Environment.OverrideCLISetValues[0].(map[string]any)["key"] = "mutated"
// The original must be unaffected; this fails with a shallow copy.
require.Equal(t, "original", lOld.Environment.OverrideCLISetValues[0].(map[string]any)["key"],
"mutating the copy's OverrideCLISetValues map must not affect the original (aliasing bug)")
}
+11 -3
View File
@@ -57,9 +57,9 @@ func (r *Run) prepareChartsIfNeeded(helmfileCommand string, dir string, concurre
return releaseToChart, nil
}
func (r *Run) withPreparedCharts(helmfileCommand string, opts state.ChartPrepareOptions, f func() []error) error {
func (r *Run) WithPreparedCharts(helmfileCommand string, opts state.ChartPrepareOptions, f func() []error) error {
if r.ReleaseToChart != nil {
panic("Run.PrepareCharts can be called only once")
return fmt.Errorf("Run.WithPreparedCharts can be called only once")
}
// Check both CLI options and helmDefaults for skipping repos (issue #2296)
@@ -88,7 +88,7 @@ func (r *Run) withPreparedCharts(helmfileCommand string, opts state.ChartPrepare
dir = tempDir
} else {
dir = opts.OutputDir
fmt.Printf("Charts will be downloaded to: %s\n", dir)
fmt.Fprintf(os.Stderr, "Charts will be downloaded to: %s\n", dir)
}
if _, err := r.state.TriggerGlobalPrepareEvent(helmfileCommand); err != nil {
@@ -238,3 +238,11 @@ func (r *Run) diff(triggerCleanupEvent bool, detailedExitCode bool, c DiffConfig
return &infoMsg, releasesToBeUpdated, releasesToBeDeleted, nil
}
func (r *Run) State() *state.HelmState {
return r.state
}
func (r *Run) Helm() helmexec.Interface {
return r.helm
}
+7
View File
@@ -88,6 +88,8 @@ type ApplyOptions struct {
TrackTimeout int
// TrackLogs enables log streaming with kubedog
TrackLogs bool
// TrackFailOnError controls whether kubedog tracking failures cause a non-zero exit code
TrackFailOnError bool
// Description is the description that will be passed to helm upgrade --description
Description string
}
@@ -316,6 +318,11 @@ func (a *ApplyImpl) TrackLogs() bool {
return a.ApplyOptions.TrackLogs
}
// TrackFailOnError returns whether kubedog tracking failures should cause a non-zero exit code.
func (a *ApplyImpl) TrackFailOnError() bool {
return a.ApplyOptions.TrackFailOnError
}
// Description returns the description.
func (a *ApplyImpl) Description() string {
return a.ApplyOptions.Description
+62
View File
@@ -0,0 +1,62 @@
package config
import (
"fmt"
"strings"
)
type CreateOptions struct {
Name string
OutputDir string
Force bool
}
func NewCreateOptions() *CreateOptions {
return &CreateOptions{}
}
type CreateImpl struct {
*GlobalImpl
*CreateOptions
}
func NewCreateImpl(g *GlobalImpl, o *CreateOptions) *CreateImpl {
return &CreateImpl{
GlobalImpl: g,
CreateOptions: o,
}
}
func (c *CreateImpl) Name() string {
return c.CreateOptions.Name
}
func (c *CreateImpl) OutputDir() string {
if c.CreateOptions.OutputDir != "" {
return c.CreateOptions.OutputDir
}
if c.CreateOptions.Name != "" {
return c.CreateOptions.Name
}
return "."
}
func (c *CreateImpl) Force() bool {
return c.CreateOptions.Force
}
func (c *CreateImpl) ValidateConfig() error {
name := c.CreateOptions.Name
if name != "" {
if strings.ContainsAny(name, "/\\") {
return fmt.Errorf("invalid project name %q: must not contain path separators", name)
}
if name == ".." || name == "." {
return fmt.Errorf("invalid project name %q", name)
}
if strings.TrimSpace(name) == "" {
return fmt.Errorf("project name must not be empty or whitespace only")
}
}
return c.GlobalImpl.ValidateConfig()
}
+66
View File
@@ -0,0 +1,66 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newTestCreateImplWithDefaults(name string) *CreateImpl {
return NewCreateImpl(NewGlobalImpl(&GlobalOptions{}), &CreateOptions{
Name: name,
})
}
func TestCreateImpl_ValidateConfig_NameWithForwardSlash(t *testing.T) {
c := newTestCreateImplWithDefaults("foo/bar")
err := c.ValidateConfig()
require.Error(t, err)
assert.Contains(t, err.Error(), "must not contain path separators")
}
func TestCreateImpl_ValidateConfig_NameWithBackslash(t *testing.T) {
c := newTestCreateImplWithDefaults(`foo\bar`)
err := c.ValidateConfig()
require.Error(t, err)
assert.Contains(t, err.Error(), "must not contain path separators")
}
func TestCreateImpl_ValidateConfig_NameDotDot(t *testing.T) {
c := newTestCreateImplWithDefaults("..")
err := c.ValidateConfig()
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid project name")
}
func TestCreateImpl_ValidateConfig_NameDot(t *testing.T) {
c := newTestCreateImplWithDefaults(".")
err := c.ValidateConfig()
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid project name")
}
func TestCreateImpl_ValidateConfig_WhitespaceOnlyName(t *testing.T) {
c := newTestCreateImplWithDefaults(" ")
err := c.ValidateConfig()
require.Error(t, err)
assert.Contains(t, err.Error(), "must not be empty or whitespace only")
}
func TestCreateImpl_ValidateConfig_ValidName(t *testing.T) {
c := newTestCreateImplWithDefaults("myproject")
require.NoError(t, c.ValidateConfig())
}
func TestCreateImpl_ValidateConfig_GlobalColorConflict(t *testing.T) {
// Delegates to GlobalImpl.ValidateConfig which rejects --color + --no-color.
c := NewCreateImpl(
NewGlobalImpl(&GlobalOptions{Color: true, NoColor: true}),
&CreateOptions{},
)
err := c.ValidateConfig()
require.Error(t, err)
assert.Contains(t, err.Error(), "--color")
assert.Contains(t, err.Error(), "--no-color")
}
+10 -3
View File
@@ -1,6 +1,6 @@
package config
// FetchOptions is the options for the build command
// FetchOptions is the options for the fetch command
type FetchOptions struct {
// Concurrency is the maximum number of concurrent helm processes to run, 0 is unlimited
Concurrency int
@@ -8,14 +8,16 @@ type FetchOptions struct {
OutputDir string
// OutputDirTemplate is the go template to generate the path of output directory
OutputDirTemplate string
// WriteOutput writes a helmfile.yaml with chart references updated to point to downloaded local chart paths
WriteOutput bool
}
// NewFetchOptions creates a new Apply
// NewFetchOptions creates a new FetchOptions
func NewFetchOptions() *FetchOptions {
return &FetchOptions{}
}
// FetchImpl is impl for applyOptions
// FetchImpl is impl for fetchOptions
type FetchImpl struct {
*GlobalImpl
*FetchOptions
@@ -43,3 +45,8 @@ func (c *FetchImpl) OutputDir() string {
func (c *FetchImpl) OutputDirTemplate() string {
return c.FetchOptions.OutputDirTemplate
}
// WriteOutput returns whether to write a modified helmfile.yaml with local chart paths
func (c *FetchImpl) WriteOutput() bool {
return c.FetchOptions.WriteOutput
}
+89 -7
View File
@@ -109,12 +109,63 @@ func (g *GlobalImpl) SetSet(set map[string]any) {
// HelmBinary returns the path to the Helm binary.
func (g *GlobalImpl) HelmBinary() string {
return g.GlobalOptions.HelmBinary
var helmBinary string
switch {
case g.GlobalOptions.HelmBinary != "":
helmBinary = g.GlobalOptions.HelmBinary
case os.Getenv("HELMFILE_HELM_BINARY") != "":
helmBinary = os.Getenv("HELMFILE_HELM_BINARY")
default:
helmBinary = state.DefaultHelmBinary
}
return helmBinary
}
// KustomizeBinary returns the path to the Kustomize binary.
func (g *GlobalImpl) KustomizeBinary() string {
return g.GlobalOptions.KustomizeBinary
var kustomizeBinary string
switch {
case g.GlobalOptions.KustomizeBinary != "":
kustomizeBinary = g.GlobalOptions.KustomizeBinary
case os.Getenv("HELMFILE_KUSTOMIZE_BINARY") != "":
kustomizeBinary = os.Getenv("HELMFILE_KUSTOMIZE_BINARY")
default:
kustomizeBinary = state.DefaultKustomizeBinary
}
return kustomizeBinary
}
// LogLevel returns the log level to use.
func (g *GlobalImpl) LogLevel() string {
var logLevel string
switch {
case g.GlobalOptions.LogLevel != "":
logLevel = g.GlobalOptions.LogLevel
case os.Getenv("HELMFILE_LOG_LEVEL") != "":
logLevel = os.Getenv("HELMFILE_LOG_LEVEL")
default:
logLevel = "info"
}
return logLevel
}
// Debug returns whether debug output is enabled.
func (g *GlobalImpl) Debug() bool {
if g.GlobalOptions.Debug {
return true
}
return os.Getenv(envvar.Debug) == "true"
}
// Quiet returns whether quiet output is enabled.
func (g *GlobalImpl) Quiet() bool {
if g.GlobalOptions.Quiet {
return true
}
return os.Getenv(envvar.Quiet) == "true"
}
// Kubeconfig returns the path to the kubeconfig file to use.
@@ -124,12 +175,32 @@ func (g *GlobalImpl) Kubeconfig() string {
// KubeContext returns the name of the kubectl context to use.
func (g *GlobalImpl) KubeContext() string {
return g.GlobalOptions.KubeContext
var kubeContext string
switch {
case g.GlobalOptions.KubeContext != "":
kubeContext = g.GlobalOptions.KubeContext
case os.Getenv("HELMFILE_KUBE_CONTEXT") != "":
kubeContext = os.Getenv("HELMFILE_KUBE_CONTEXT")
default:
kubeContext = ""
}
return kubeContext
}
// Namespace returns the namespace to use.
func (g *GlobalImpl) Namespace() string {
return g.GlobalOptions.Namespace
var namespace string
switch {
case g.GlobalOptions.Namespace != "":
namespace = g.GlobalOptions.Namespace
case os.Getenv("HELMFILE_NAMESPACE") != "":
namespace = os.Getenv("HELMFILE_NAMESPACE")
default:
namespace = ""
}
return namespace
}
// Chart returns the chart to use.
@@ -222,7 +293,7 @@ func (g *GlobalImpl) Color() bool {
return c
}
if g.GlobalOptions.NoColor {
if g.NoColor() {
return false
}
@@ -239,7 +310,18 @@ func (g *GlobalImpl) Color() bool {
// NoColor returns the no color flag
func (g *GlobalImpl) NoColor() bool {
return g.GlobalOptions.NoColor
if g.GlobalOptions.NoColor {
return true
}
// Explicit --color short-circuits env-derived no-color: a flag must win over an env var.
if g.GlobalOptions.Color {
return false
}
if os.Getenv(envvar.NoColor) == "true" {
return true
}
// Honor the de-facto https://no-color.org/ standard: any non-empty value disables color.
return os.Getenv("NO_COLOR") != ""
}
// Env returns the environment to use.
@@ -276,7 +358,7 @@ func (g *GlobalImpl) Interactive() bool {
// Args returns the args to use for helm
func (g *GlobalImpl) Args() string {
args := g.GlobalOptions.Args
enableHelmDebug := g.Debug
enableHelmDebug := g.Debug()
if enableHelmDebug {
args = fmt.Sprintf("%s %s", args, "--debug")
+375
View File
@@ -45,3 +45,378 @@ func TestFileOrDir(t *testing.T) {
}
os.Unsetenv(envvar.FilePath)
}
// TestKubeContext tests the kube-context flag and HELMFILE_KUBE_CONTEXT env var fallback
func TestKubeContext(t *testing.T) {
tests := []struct {
opts GlobalOptions
env string
expected string
}{
{
opts: GlobalOptions{},
env: "",
expected: "",
},
{
opts: GlobalOptions{},
env: "envset",
expected: "envset",
},
{
opts: GlobalOptions{KubeContext: "flagset"},
env: "",
expected: "flagset",
},
{
opts: GlobalOptions{KubeContext: "flagset"},
env: "envset",
expected: "flagset",
},
}
for _, test := range tests {
os.Setenv(envvar.KubeContext, test.env)
received := NewGlobalImpl(&test.opts).KubeContext()
require.Equalf(t, test.expected, received, "KubeContext expected %s, received %s", test.expected, received)
}
os.Unsetenv(envvar.KubeContext)
}
// TestNamespace tests the namespace flag and HELMFILE_NAMESPACE env var fallback
func TestNamespace(t *testing.T) {
tests := []struct {
opts GlobalOptions
env string
expected string
}{
{
opts: GlobalOptions{},
env: "",
expected: "",
},
{
opts: GlobalOptions{},
env: "envset",
expected: "envset",
},
{
opts: GlobalOptions{Namespace: "flagset"},
env: "",
expected: "flagset",
},
{
opts: GlobalOptions{Namespace: "flagset"},
env: "envset",
expected: "flagset",
},
}
for _, test := range tests {
os.Setenv(envvar.Namespace, test.env)
received := NewGlobalImpl(&test.opts).Namespace()
require.Equalf(t, test.expected, received, "Namespace expected %s, received %s", test.expected, received)
}
os.Unsetenv(envvar.Namespace)
}
// TestHelmBinary tests the helm-binary flag and HELMFILE_HELM_BINARY env var fallback
func TestHelmBinary(t *testing.T) {
tests := []struct {
opts GlobalOptions
env string
expected string
}{
{
opts: GlobalOptions{},
env: "",
expected: "helm",
},
{
opts: GlobalOptions{},
env: "envset",
expected: "envset",
},
{
opts: GlobalOptions{HelmBinary: "flagset"},
env: "",
expected: "flagset",
},
{
opts: GlobalOptions{HelmBinary: "flagset"},
env: "envset",
expected: "flagset",
},
}
for _, test := range tests {
os.Setenv(envvar.HelmBinary, test.env)
received := NewGlobalImpl(&test.opts).HelmBinary()
require.Equalf(t, test.expected, received, "HelmBinary expected %s, received %s", test.expected, received)
}
os.Unsetenv(envvar.HelmBinary)
}
// TestKustomizeBinary tests the kustomize-binary flag and HELMFILE_KUSTOMIZE_BINARY env var fallback
func TestKustomizeBinary(t *testing.T) {
tests := []struct {
opts GlobalOptions
env string
expected string
}{
{
opts: GlobalOptions{},
env: "",
expected: "kustomize",
},
{
opts: GlobalOptions{},
env: "envset",
expected: "envset",
},
{
opts: GlobalOptions{KustomizeBinary: "flagset"},
env: "",
expected: "flagset",
},
{
opts: GlobalOptions{KustomizeBinary: "flagset"},
env: "envset",
expected: "flagset",
},
}
for _, test := range tests {
os.Setenv(envvar.KustomizeBinary, test.env)
received := NewGlobalImpl(&test.opts).KustomizeBinary()
require.Equalf(t, test.expected, received, "KustomizeBinary expected %s, received %s", test.expected, received)
}
os.Unsetenv(envvar.KustomizeBinary)
}
// TestLogLevel tests the log-level flag and HELMFILE_LOG_LEVEL env var fallback
func TestLogLevel(t *testing.T) {
tests := []struct {
opts GlobalOptions
env string
expected string
}{
{
opts: GlobalOptions{},
env: "",
expected: "info",
},
{
opts: GlobalOptions{},
env: "envset",
expected: "envset",
},
{
opts: GlobalOptions{LogLevel: "flagset"},
env: "",
expected: "flagset",
},
{
opts: GlobalOptions{LogLevel: "flagset"},
env: "envset",
expected: "flagset",
},
}
for _, test := range tests {
os.Setenv(envvar.LogLevel, test.env)
received := NewGlobalImpl(&test.opts).LogLevel()
require.Equalf(t, test.expected, received, "LogLevel expected %s, received %s", test.expected, received)
}
os.Unsetenv(envvar.LogLevel)
}
// TestDebug tests the debug flag and HELMFILE_DEBUG env var fallback
func TestDebug(t *testing.T) {
tests := []struct {
opts GlobalOptions
env string
expected bool
}{
{
opts: GlobalOptions{},
env: "",
expected: false,
},
{
opts: GlobalOptions{},
env: "true",
expected: true,
},
{
opts: GlobalOptions{},
env: "anything",
expected: false,
},
{
opts: GlobalOptions{Debug: true},
env: "",
expected: true,
},
{
opts: GlobalOptions{Debug: true},
env: "true",
expected: true,
},
}
for _, test := range tests {
os.Setenv(envvar.Debug, test.env)
received := NewGlobalImpl(&test.opts).Debug()
require.Equalf(t, test.expected, received, "Debug expected %t, received %t", test.expected, received)
}
os.Unsetenv(envvar.Debug)
}
// TestQuiet tests the quiet flag and HELMFILE_QUIET env var fallback
func TestQuiet(t *testing.T) {
tests := []struct {
opts GlobalOptions
env string
expected bool
}{
{
opts: GlobalOptions{},
env: "",
expected: false,
},
{
opts: GlobalOptions{},
env: "true",
expected: true,
},
{
opts: GlobalOptions{},
env: "anything",
expected: false,
},
{
opts: GlobalOptions{Quiet: true},
env: "",
expected: true,
},
{
opts: GlobalOptions{Quiet: true},
env: "true",
expected: true,
},
}
for _, test := range tests {
os.Setenv(envvar.Quiet, test.env)
received := NewGlobalImpl(&test.opts).Quiet()
require.Equalf(t, test.expected, received, "Quiet expected %t, received %t", test.expected, received)
}
os.Unsetenv(envvar.Quiet)
}
// TestNoColor tests the no-color flag, HELMFILE_NO_COLOR and NO_COLOR env var fallbacks
func TestNoColor(t *testing.T) {
tests := []struct {
opts GlobalOptions
helmfileEnv string
standardEnv string
expected bool
}{
{
opts: GlobalOptions{},
helmfileEnv: "",
standardEnv: "",
expected: false,
},
{
opts: GlobalOptions{},
helmfileEnv: "true",
standardEnv: "",
expected: true,
},
{
opts: GlobalOptions{},
helmfileEnv: "anything",
standardEnv: "",
expected: false,
},
{
opts: GlobalOptions{},
helmfileEnv: "",
standardEnv: "1",
expected: true,
},
{
opts: GlobalOptions{},
helmfileEnv: "",
standardEnv: "anything",
expected: true,
},
{
opts: GlobalOptions{NoColor: true},
helmfileEnv: "",
standardEnv: "",
expected: true,
},
}
for _, test := range tests {
os.Setenv(envvar.NoColor, test.helmfileEnv)
os.Setenv("NO_COLOR", test.standardEnv)
received := NewGlobalImpl(&test.opts).NoColor()
require.Equalf(t, test.expected, received, "NoColor expected %t, received %t", test.expected, received)
}
os.Unsetenv(envvar.NoColor)
os.Unsetenv("NO_COLOR")
}
// TestColorRespectsNoColorEnv guards against ValidateConfig() firing when
// HELMFILE_NO_COLOR / NO_COLOR is set without an explicit --color/--no-color flag.
// Color() must consult NoColor() (which is env-aware) before falling back to TTY autodetect.
func TestColorRespectsNoColorEnv(t *testing.T) {
tests := []struct {
name string
helmfileEnv string
standardEnv string
}{
{name: "HELMFILE_NO_COLOR=true", helmfileEnv: "true"},
{name: "NO_COLOR set", standardEnv: "1"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Setenv(envvar.NoColor, test.helmfileEnv)
t.Setenv("NO_COLOR", test.standardEnv)
g := NewGlobalImpl(&GlobalOptions{})
require.True(t, g.NoColor(), "NoColor() should be true when env is set")
require.False(t, g.Color(), "Color() should be false when NoColor() is true via env")
require.NoError(t, g.ValidateConfig(), "ValidateConfig() should not error from env-only no-color")
})
}
}
// TestColorFlagOverridesNoColorEnv guards against ValidateConfig() firing when
// --color is explicitly passed but HELMFILE_NO_COLOR / NO_COLOR is set in the
// environment. The flag must win over the env var.
func TestColorFlagOverridesNoColorEnv(t *testing.T) {
tests := []struct {
name string
helmfileEnv string
standardEnv string
}{
{name: "HELMFILE_NO_COLOR=true", helmfileEnv: "true"},
{name: "NO_COLOR set", standardEnv: "1"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Setenv(envvar.NoColor, test.helmfileEnv)
t.Setenv("NO_COLOR", test.standardEnv)
g := NewGlobalImpl(&GlobalOptions{Color: true})
require.True(t, g.Color(), "Color() should be true when --color is set")
require.False(t, g.NoColor(), "NoColor() should be false when --color is set, even if env says otherwise")
require.NoError(t, g.ValidateConfig(), "ValidateConfig() should not error when --color overrides env no-color")
})
}
}
+87
View File
@@ -59,8 +59,25 @@ type SyncOptions struct {
TrackTimeout int
// TrackLogs enables log streaming with kubedog
TrackLogs bool
// TrackFailOnError controls whether kubedog tracking failures cause a non-zero exit code
TrackFailOnError bool
// Description is the description that will be passed to helm upgrade --description
Description string
// Diff-related options for --interactive mode
SuppressOutputLineRegex []string
IncludeTests bool
Suppress []string
SuppressSecrets bool
ShowSecrets bool
NoHooks bool
SuppressDiff bool
SkipDiffOnInstall bool
DiffArgs string
DetailedExitcode bool
StripTrailingCR bool
Context int
DiffOutput string
}
// NewSyncOptions creates a new Apply
@@ -216,11 +233,81 @@ func (t *SyncImpl) TrackLogs() bool {
return t.SyncOptions.TrackLogs
}
// TrackFailOnError returns whether kubedog tracking failures should cause a non-zero exit code.
func (t *SyncImpl) TrackFailOnError() bool {
return t.SyncOptions.TrackFailOnError
}
// Description returns the description.
func (t *SyncImpl) Description() string {
return t.SyncOptions.Description
}
// SuppressOutputLineRegex returns the SuppressOutputLineRegex.
func (t *SyncImpl) SuppressOutputLineRegex() []string {
return t.SyncOptions.SuppressOutputLineRegex
}
// IncludeTests returns the IncludeTests.
func (t *SyncImpl) IncludeTests() bool {
return t.SyncOptions.IncludeTests
}
// Suppress returns the Suppress.
func (t *SyncImpl) Suppress() []string {
return t.SyncOptions.Suppress
}
// SuppressSecrets returns the SuppressSecrets.
func (t *SyncImpl) SuppressSecrets() bool {
return t.SyncOptions.SuppressSecrets
}
// ShowSecrets returns the ShowSecrets.
func (t *SyncImpl) ShowSecrets() bool {
return t.SyncOptions.ShowSecrets
}
// NoHooks returns the NoHooks.
func (t *SyncImpl) NoHooks() bool {
return t.SyncOptions.NoHooks
}
// SuppressDiff returns the SuppressDiff.
func (t *SyncImpl) SuppressDiff() bool {
return t.SyncOptions.SuppressDiff
}
// SkipDiffOnInstall returns the SkipDiffOnInstall.
func (t *SyncImpl) SkipDiffOnInstall() bool {
return t.SyncOptions.SkipDiffOnInstall
}
// DiffArgs returns the DiffArgs.
func (t *SyncImpl) DiffArgs() string {
return t.SyncOptions.DiffArgs
}
// DetailedExitcode returns the DetailedExitcode.
func (t *SyncImpl) DetailedExitcode() bool {
return t.SyncOptions.DetailedExitcode
}
// StripTrailingCR returns the StripTrailingCR.
func (t *SyncImpl) StripTrailingCR() bool {
return t.SyncOptions.StripTrailingCR
}
// Context returns the Context.
func (t *SyncImpl) Context() int {
return t.SyncOptions.Context
}
// DiffOutput returns the DiffOutput.
func (t *SyncImpl) DiffOutput() string {
return t.SyncOptions.DiffOutput
}
func (t *SyncImpl) ValidateConfig() error {
validTrackModes := []string{"helm", "helm-legacy", "kubedog"}
if t.SyncOptions.TrackMode != "" && !slices.Contains(validTrackModes, t.SyncOptions.TrackMode) {
+32
View File
@@ -205,3 +205,35 @@ func TestEnvironment_GetMergedValues_Issue2281_SparseArrayMerge(t *testing.T) {
assert.Equal(t, "second thing", elem1["thing"])
assert.Equal(t, "cmdline", elem1["anotherThing"])
}
func TestEnvironment_GetMergedValues_Issue2527_ValuesOverrideDefaults(t *testing.T) {
// Regression test for https://github.com/helmfile/helmfile/issues/2527:
// A boolean false in Values must not be overridden by a true in Defaults.
env := &Environment{
Name: "test",
Defaults: map[string]any{
"helmDefaults": map[string]any{
"atomic": true,
"wait": true,
"timeout": 300,
},
},
Values: map[string]any{
"appName": "my-app",
"helmDefaults": map[string]any{
"atomic": false, // explicit false override must survive
"wait": true,
"timeout": 300,
},
},
CLIOverrides: map[string]any{},
}
mergedValues, err := env.GetMergedValues()
require.NoError(t, err)
hd := mergedValues["helmDefaults"].(map[string]any)
assert.Equal(t, false, hd["atomic"], "Values false should override Defaults true for atomic")
assert.Equal(t, true, hd["wait"])
assert.Equal(t, 300, hd["timeout"])
}
+8
View File
@@ -9,6 +9,14 @@ const (
DisableRunnerUniqueID = "HELMFILE_DISABLE_RUNNER_UNIQUE_ID"
Experimental = "HELMFILE_EXPERIMENTAL" // environment variable for experimental features, expecting "true" lower case
Environment = "HELMFILE_ENVIRONMENT"
KubeContext = "HELMFILE_KUBE_CONTEXT"
Namespace = "HELMFILE_NAMESPACE"
HelmBinary = "HELMFILE_HELM_BINARY"
KustomizeBinary = "HELMFILE_KUSTOMIZE_BINARY"
LogLevel = "HELMFILE_LOG_LEVEL"
Debug = "HELMFILE_DEBUG"
Quiet = "HELMFILE_QUIET"
NoColor = "HELMFILE_NO_COLOR"
FilePath = "HELMFILE_FILE_PATH"
TempDir = "HELMFILE_TEMPDIR"
UpgradeNoticeDisabled = "HELMFILE_UPGRADE_NOTICE_DISABLED"
+15
View File
@@ -27,7 +27,10 @@ func (fs fileStat) Sys() any { return nil }
type FileSystem struct {
ReadFile func(string) ([]byte, error)
ReadDir func(string) ([]fs.DirEntry, error)
WriteFile func(string, []byte, fs.FileMode) error
DeleteFile func(string) error
MkdirTemp func(string, string) (string, error)
RemoveAll func(string) error
FileExists func(string) (bool, error)
Glob func(string) ([]string, error)
FileExistsAt func(string) bool
@@ -44,7 +47,10 @@ type FileSystem struct {
func DefaultFileSystem() *FileSystem {
dfs := FileSystem{
ReadDir: os.ReadDir,
WriteFile: os.WriteFile,
DeleteFile: os.Remove,
MkdirTemp: os.MkdirTemp,
RemoveAll: os.RemoveAll,
Glob: filepath.Glob,
Getwd: os.Getwd,
Chdir: os.Chdir,
@@ -107,6 +113,15 @@ func FromFileSystem(params FileSystem) *FileSystem {
if params.CopyDir != nil {
dfs.CopyDir = params.CopyDir
}
if params.WriteFile != nil {
dfs.WriteFile = params.WriteFile
}
if params.MkdirTemp != nil {
dfs.MkdirTemp = params.MkdirTemp
}
if params.RemoveAll != nil {
dfs.RemoveAll = params.RemoveAll
}
return dfs
}
+4
View File
@@ -252,4 +252,8 @@ func TestFs_DefaultBuilder(t *testing.T) {
assert.NotNil(t, ffs.Chdir)
assert.NotNil(t, ffs.Abs)
assert.NotNil(t, ffs.EvalSymlinks)
assert.NotNil(t, ffs.WriteFile)
assert.NotNil(t, ffs.MkdirTemp)
assert.NotNil(t, ffs.RemoveAll)
assert.NotNil(t, ffs.CopyDir)
}
+118 -10
View File
@@ -256,7 +256,7 @@ func (helm *execer) AddRepo(name, repository, cafile, certfile, keyfile, usernam
if username != "" && password != "" {
args = append(args, "--username", username, "--password-stdin")
buffer := bytes.Buffer{}
buffer.Write([]byte(fmt.Sprintf("%s\n", password)))
fmt.Fprintf(&buffer, "%s\n", password)
out, err = helm.execStdIn(args, map[string]string{}, &buffer)
} else {
out, err = helm.exec(args, map[string]string{}, nil)
@@ -311,7 +311,7 @@ func (helm *execer) RegistryLogin(repository, username, password, caFile, certFi
args = append(args, "--username", username, "--password-stdin")
buffer := bytes.Buffer{}
buffer.Write([]byte(fmt.Sprintf("%s\n", password)))
fmt.Fprintf(&buffer, "%s\n", password)
helm.logger.Info("Logging in to registry")
out, err := helm.execStdIn(args, map[string]string{"HELM_EXPERIMENTAL_OCI": "1"}, &buffer)
@@ -651,17 +651,77 @@ func (helm *execer) TemplateRelease(name string, chart string, flags ...string)
helm.logger.Infof("Templating release=%v, chart=%v", name, redactedURL(chart))
args := []string{"template", name, chart}
out, err := helm.exec(append(args, flags...), map[string]string{}, nil)
var outputToFile bool
var hasPostRenderer bool
for _, f := range flags {
if strings.HasPrefix("--output-dir", f) {
if f == "--output-dir" || strings.HasPrefix(f, "--output-dir=") {
outputToFile = true
break
}
if f == "--post-renderer" || strings.HasPrefix(f, "--post-renderer=") {
hasPostRenderer = true
}
}
if outputToFile && hasPostRenderer && helm.IsHelm3() {
// Helm 3 does not apply --post-renderer to files written by --output-dir.
// It writes pre-post-renderer content to files and sends post-renderer output to stdout.
// Helm 4 handles this correctly, so the workaround is only needed for Helm 3.
// Workaround: run without --output-dir, capture stdout (with post-renderer applied),
// and write the output to the output directory ourselves.
var outputDir string
filteredFlags := make([]string, 0, len(flags))
for i := 0; i < len(flags); i++ {
if flags[i] == "--output-dir" && i+1 < len(flags) {
outputDir = flags[i+1]
i++
continue
}
if strings.HasPrefix(flags[i], "--output-dir=") {
outputDir = strings.TrimPrefix(flags[i], "--output-dir=")
continue
}
filteredFlags = append(filteredFlags, flags[i])
}
if outputDir == "" {
return fmt.Errorf("output dir not found for template command")
}
out, err := helm.exec(append(args, filteredFlags...), map[string]string{}, nil)
if err != nil {
return err
}
templatesDir := filepath.Join(outputDir, "templates")
legacyOutputPath := filepath.Join(outputDir, name+".yaml")
outputPath := filepath.Join(templatesDir, name+".yaml")
if removeErr := os.Remove(legacyOutputPath); removeErr != nil && !os.IsNotExist(removeErr) {
return fmt.Errorf("failed to remove legacy output file %s: %w", legacyOutputPath, removeErr)
}
// Remove only the specific file written by the previous run to avoid clobbering
// unrelated files in a shared output directory.
if removeErr := os.Remove(outputPath); removeErr != nil && !os.IsNotExist(removeErr) {
return fmt.Errorf("failed to remove stale output file %s: %w", outputPath, removeErr)
}
if len(out) > 0 {
if mkdirErr := os.MkdirAll(templatesDir, 0755); mkdirErr != nil {
return fmt.Errorf("failed to create templates directory %s: %w", templatesDir, mkdirErr)
}
if writeErr := os.WriteFile(outputPath, append(out, '\n'), 0644); writeErr != nil {
return fmt.Errorf("failed to write output file %s: %w", outputPath, writeErr)
}
helm.logger.Debugf("Wrote post-renderer output to %s", outputPath)
}
return nil
}
out, err := helm.exec(append(args, flags...), map[string]string{}, nil)
if outputToFile {
// With --output-dir is passed to helm-template,
// we can safely direct all the logs from it to our logger.
@@ -856,7 +916,7 @@ func (helm *execer) AddPlugin(name, path, version string) error {
helm.logger.Infof("Install helm plugin %v", name)
// Special handling for helm-secrets 4.7.0+ with Helm 4 which uses split plugin architecture
if name == "secrets" && version >= "v4.7.0" && helm.IsHelm4() {
if name == "secrets" && helmSecretsRequiresSplitInstall(version) && helm.IsHelm4() {
return helm.installHelmSecretsV4(version)
}
@@ -911,11 +971,59 @@ func (helm *execer) installHelmSecretsV4(version string) error {
return nil
}
func (helm *execer) UpdatePlugin(name string) error {
helm.logger.Infof("Update helm plugin %v", name)
// helmSecretsV4SplitMinVersion is the minimum helm-secrets version that uses the
// split plugin architecture (secrets, secrets-getter, secrets-post-renderer) with Helm 4.
var helmSecretsV4SplitMinVersion = semver.MustParse("4.7.0")
// helmSecretsRequiresSplitInstall returns true when the given helm-secrets version
// requires the split plugin architecture introduced in v4.7.0 for Helm 4.
func helmSecretsRequiresSplitInstall(version string) bool {
v, err := semver.NewVersion(version)
if err != nil {
return false
}
return !v.LessThan(helmSecretsV4SplitMinVersion)
}
func (helm *execer) uninstallPlugin(name string) error {
helm.logger.Infof("Uninstalling helm plugin %v", name)
out, err := helm.exec([]string{"plugin", "uninstall", name}, map[string]string{}, nil)
if err == nil {
helm.info(out)
}
return err
}
func (helm *execer) UpdatePlugin(name, repo, version string) error {
helm.logger.Infof("Updating helm plugin %v", name)
// Special handling for helm-secrets 4.7.0+ with Helm 4 which uses split plugin architecture
if name == "secrets" && helmSecretsRequiresSplitInstall(version) && helm.IsHelm4() {
// Uninstall existing secrets plugins; ignore errors as some may not exist
for _, secretsPlugin := range []string{"secrets", "secrets-getter", "secrets-post-renderer"} {
if err := helm.uninstallPlugin(secretsPlugin); err != nil {
helm.logger.Debugf("Failed to uninstall helm plugin %v (may not exist): %v", secretsPlugin, err)
}
}
return helm.installHelmSecretsV4(version)
}
// Try standard helm plugin update
out, err := helm.exec([]string{"plugin", "update", name}, map[string]string{}, nil)
helm.info(out)
return err
if err != nil {
// If standard update failed, fall back to uninstall + reinstall with specific version
updateErr := err
helm.logger.Infof("helm plugin update %v failed (%v), falling back to reinstall with version %v", name, updateErr, version)
if uninstallErr := helm.uninstallPlugin(name); uninstallErr != nil {
return fmt.Errorf("helm plugin update failed (%w) and uninstall for reinstall also failed: %w", updateErr, uninstallErr)
}
if reinstallErr := helm.AddPlugin(name, repo, version); reinstallErr != nil {
return fmt.Errorf("helm plugin update failed (%w) and reinstall also failed: %w", updateErr, reinstallErr)
}
return nil
}
return nil
}
func (helm *execer) exec(args []string, env map[string]string, overrideEnableLiveOutput *bool) ([]byte, error) {
+165 -4
View File
@@ -21,8 +21,9 @@ import (
// Mocking the command-line runner
type mockRunner struct {
output []byte
err error
output []byte
versionOutput []byte // if set, returned for "helm version --short" probe; overrides default Helm 4 fallback
err error
}
func (mock *mockRunner) ExecuteStdIn(cmd string, args []string, env map[string]string, stdin io.Reader) ([]byte, error) {
@@ -30,8 +31,13 @@ func (mock *mockRunner) ExecuteStdIn(cmd string, args []string, env map[string]s
}
func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
if len(mock.output) == 0 && strings.Join(args, " ") == "version --short" {
return []byte("v4.0.1+g12500dd"), nil
if strings.Join(args, " ") == "version --short" {
if mock.versionOutput != nil {
return mock.versionOutput, nil
}
if len(mock.output) == 0 {
return []byte("v4.0.1+g12500dd"), nil
}
}
return mock.output, mock.err
}
@@ -1255,6 +1261,64 @@ exec: helm --kubeconfig config --kube-context dev template release https://examp
}
}
func Test_Template_PostRendererWithOutputDir(t *testing.T) {
tests := []struct {
name string
postRendererFlag string
}{
{"separate flags", "--post-renderer"},
{"combined flag", "--post-renderer=/bin/echo"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir := t.TempDir()
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
// Use Helm 3 version for the version probe so the Helm 3 workaround is applied.
// The workaround is not needed for Helm 4, which natively applies --post-renderer to --output-dir output.
runner := &mockRunner{versionOutput: []byte("v3.20.0")}
helm, err := New("helm", HelmExecOptions{}, logger, "config", "dev", runner)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
runner.output = []byte("apiVersion: v1\nkind: Namespace\n")
var flags []string
if tt.postRendererFlag == "--post-renderer" {
flags = []string{"--post-renderer", "/bin/echo", "--output-dir", tmpDir, "--values", "file.yml"}
} else {
flags = []string{tt.postRendererFlag, "--output-dir", tmpDir, "--values", "file.yml"}
}
err = helm.TemplateRelease("myrelease", "path/to/chart", flags...)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
outputPath := filepath.Join(tmpDir, "templates", "myrelease.yaml")
data, err := os.ReadFile(outputPath)
if err != nil {
t.Fatalf("expected output file %s to exist: %v", outputPath, err)
}
expected := "apiVersion: v1\nkind: Namespace\n\n"
if string(data) != expected {
t.Errorf("output file content:\nactual=%q\nexpect=%q", string(data), expected)
}
outputLog := buffer.String()
if strings.Contains(outputLog, "--output-dir") {
t.Errorf("helm should NOT have been called with --output-dir, got: %s", outputLog)
}
if !strings.Contains(outputLog, "--post-renderer") {
t.Errorf("helm should have been called with --post-renderer, got: %s", outputLog)
}
})
}
}
func Test_IsHelm3(t *testing.T) {
helm3Runner := mockRunner{output: []byte("v3.0.0+ge29ce2a\n")}
helm, err := New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm3Runner)
@@ -1547,3 +1611,100 @@ func TestParseHelmVersion(t *testing.T) {
})
}
}
func Test_helmSecretsRequiresSplitInstall(t *testing.T) {
tests := []struct {
name string
version string
want bool
}{
{name: "below threshold", version: "v4.6.9", want: false},
{name: "exactly at threshold", version: "v4.7.0", want: true},
{name: "above threshold single digit minor", version: "v4.8.0", want: true},
{name: "above threshold double digit minor (v4.10.0+)", version: "v4.10.0", want: true},
{name: "pre-release below threshold", version: "v4.7.0-beta.1", want: false},
{name: "invalid version string", version: "not-a-version", want: false},
{name: "empty string", version: "", want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := helmSecretsRequiresSplitInstall(tt.version)
assert.Equal(t, tt.want, got)
})
}
}
type funcRunner struct {
execute func(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error)
}
func (r *funcRunner) ExecuteStdIn(cmd string, args []string, env map[string]string, stdin io.Reader) ([]byte, error) {
return r.execute(cmd, args, env, false)
}
func (r *funcRunner) Execute(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
return r.execute(cmd, args, env, enableLiveOutput)
}
func Test_UpdatePlugin_Helm4SecretsUsesUninstallReinstall(t *testing.T) {
var calledArgs [][]string
runner := &funcRunner{
execute: func(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
calledArgs = append(calledArgs, append([]string(nil), args...))
return []byte{}, nil
},
}
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := &execer{
helmBinary: "helm",
version: semver.MustParse("4.0.0"),
logger: logger,
kubeconfig: "config",
kubeContext: "dev",
runner: runner,
}
err := helm.UpdatePlugin("secrets", "https://github.com/jkroepke/helm-secrets", "v4.7.0")
require.NoError(t, err)
// Verify that "plugin update" was NOT called (the Helm 4 secrets path should skip it).
for _, args := range calledArgs {
for i, a := range args {
if a == "plugin" && i+1 < len(args) && args[i+1] == "update" {
t.Errorf("expected 'plugin update' to not be called for Helm 4 secrets, but it was: %v", args)
}
}
}
// Verify that uninstall was called for all three split plugins.
checkUninstall := func(name string) {
for _, args := range calledArgs {
for i, a := range args {
if a == "plugin" && i+2 < len(args) && args[i+1] == "uninstall" && args[i+2] == name {
return
}
}
}
t.Errorf("expected 'plugin uninstall %s' to be called", name)
}
checkUninstall("secrets")
checkUninstall("secrets-getter")
checkUninstall("secrets-post-renderer")
// Verify that install was called for all three split plugin tarballs.
checkInstall := func(urlSubstring string) {
for _, args := range calledArgs {
for i, a := range args {
if a == "plugin" && i+2 < len(args) && args[i+1] == "install" && strings.Contains(args[i+2], urlSubstring) {
return
}
}
}
t.Errorf("expected 'plugin install' for %q to be called", urlSubstring)
}
checkInstall("secrets-4.7.0.tgz")
checkInstall("secrets-getter-4.7.0.tgz")
checkInstall("secrets-post-renderer-4.7.0.tgz")
}
+312
View File
@@ -0,0 +1,312 @@
package kubedog
import (
"fmt"
"io"
"os"
"sort"
"strings"
"github.com/werf/kubedog/pkg/tracker/daemonset"
"github.com/werf/kubedog/pkg/tracker/deployment"
"github.com/werf/kubedog/pkg/tracker/indicators"
"github.com/werf/kubedog/pkg/tracker/job"
"github.com/werf/kubedog/pkg/tracker/pod"
"github.com/werf/kubedog/pkg/tracker/statefulset"
"github.com/werf/kubedog/pkg/utils"
"golang.org/x/term"
)
var statusProgressTableRatio = []float64{.58, .11, .12, .19}
var statusProgressSubTableRatio = []float64{.40, .15, .20, .25}
func writeOut(out io.Writer, s string) {
_, _ = fmt.Fprint(out, s)
}
func displayDeploymentStatusProgress(out io.Writer, resourceCaption string, status deployment.DeploymentStatus, prevStatus *deployment.DeploymentStatus) {
t := utils.NewTable(statusProgressTableRatio...)
t.SetWidth(termWidth())
showProgress := status.StatusGeneration > prevStatus.StatusGeneration
replicas := "-"
if status.ReplicasIndicator != nil {
replicas = status.ReplicasIndicator.FormatTableElem(prevStatus.ReplicasIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
WithTargetValue: true,
})
}
available := "-"
if status.AvailableIndicator != nil {
available = status.AvailableIndicator.FormatTableElem(prevStatus.AvailableIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
})
}
uptodate := "-"
if status.UpToDateIndicator != nil {
uptodate = status.UpToDateIndicator.FormatTableElem(prevStatus.UpToDateIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
})
}
t.Header("DEPLOYMENT", "REPLICAS", "AVAILABLE", "UP-TO-DATE")
args := []interface{}{resourceCaption, replicas, available, uptodate}
if status.IsFailed {
args = append(args, formatResourceError(status.FailedReason))
}
t.Row(args...)
displayChildPodsAndWaiting(&t, prevStatus.Pods, status.Pods, status.NewPodsNames, status.WaitingForMessages)
writeOut(out, t.Render())
}
func displayStatefulSetStatusProgress(out io.Writer, resourceCaption string, status statefulset.StatefulSetStatus, prevStatus *statefulset.StatefulSetStatus) {
t := utils.NewTable(statusProgressTableRatio...)
t.SetWidth(termWidth())
showProgress := status.StatusGeneration > prevStatus.StatusGeneration
replicas := "-"
if status.ReplicasIndicator != nil {
replicas = status.ReplicasIndicator.FormatTableElem(prevStatus.ReplicasIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
WithTargetValue: true,
})
}
ready := "-"
if status.ReadyIndicator != nil {
ready = status.ReadyIndicator.FormatTableElem(prevStatus.ReadyIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
})
}
uptodate := "-"
if status.UpToDateIndicator != nil {
uptodate = status.UpToDateIndicator.FormatTableElem(prevStatus.UpToDateIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
})
}
t.Header("STATEFULSET", "REPLICAS", "READY", "UP-TO-DATE")
args := []interface{}{resourceCaption, replicas, ready, uptodate}
if status.IsFailed {
args = append(args, formatResourceError(status.FailedReason))
} else {
for _, w := range status.WarningMessages {
args = append(args, formatResourceWarning(w))
}
}
t.Row(args...)
displayChildPodsAndWaiting(&t, prevStatus.Pods, status.Pods, status.NewPodsNames, status.WaitingForMessages)
writeOut(out, t.Render())
}
func displayDaemonSetStatusProgress(out io.Writer, resourceCaption string, status daemonset.DaemonSetStatus, prevStatus *daemonset.DaemonSetStatus) {
t := utils.NewTable(statusProgressTableRatio...)
t.SetWidth(termWidth())
showProgress := status.StatusGeneration > prevStatus.StatusGeneration
replicas := "-"
if status.ReplicasIndicator != nil {
replicas = status.ReplicasIndicator.FormatTableElem(prevStatus.ReplicasIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
WithTargetValue: true,
})
}
available := "-"
if status.AvailableIndicator != nil {
available = status.AvailableIndicator.FormatTableElem(prevStatus.AvailableIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
})
}
uptodate := "-"
if status.UpToDateIndicator != nil {
uptodate = status.UpToDateIndicator.FormatTableElem(prevStatus.UpToDateIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
})
}
t.Header("DAEMONSET", "REPLICAS", "AVAILABLE", "UP-TO-DATE")
args := []interface{}{resourceCaption, replicas, available, uptodate}
if status.IsFailed {
args = append(args, formatResourceError(status.FailedReason))
}
t.Row(args...)
displayChildPodsAndWaiting(&t, prevStatus.Pods, status.Pods, status.NewPodsNames, status.WaitingForMessages)
writeOut(out, t.Render())
}
func displayJobStatusProgress(out io.Writer, resourceCaption string, status job.JobStatus, prevStatus *job.JobStatus) {
t := utils.NewTable(statusProgressTableRatio...)
t.SetWidth(termWidth())
showProgress := status.StatusGeneration > prevStatus.StatusGeneration
succeeded := "-"
if status.SucceededIndicator != nil {
succeeded = status.SucceededIndicator.FormatTableElem(prevStatus.SucceededIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
})
}
t.Header("JOB", "ACTIVE", "DURATION", "SUCCEEDED/FAILED")
var active interface{} = "-"
if status.Active != 0 {
active = status.Active
}
failed := fmt.Sprintf("%d", status.Failed)
args := []interface{}{resourceCaption, active, status.Age, strings.Join([]string{succeeded, failed}, "/")}
if status.IsFailed {
args = append(args, formatResourceError(status.FailedReason))
}
t.Row(args...)
if len(status.Pods) > 0 {
st := displayChildPodsStatusProgress(&t, prevStatus.Pods, status.Pods, nil, showProgress)
extraMsg := ""
if len(status.WaitingForMessages) > 0 {
extraMsg += "---\n"
extraMsg += utils.BlueF("Waiting for: %s", strings.Join(status.WaitingForMessages, ", "))
}
st.Commit(extraMsg)
}
writeOut(out, t.Render())
}
func displayChildPodsAndWaiting(t *utils.Table, prevPods, pods map[string]pod.PodStatus, newPodsNames []string, waitingForMessages []string) {
if len(pods) > 0 {
st := displayChildPodsStatusProgress(t, prevPods, pods, newPodsNames, true)
extraMsg := ""
if len(waitingForMessages) > 0 {
extraMsg += "---\n"
extraMsg += utils.BlueF("Waiting for: %s", strings.Join(waitingForMessages, ", "))
}
st.Commit(extraMsg)
}
}
func displayChildPodsStatusProgress(t *utils.Table, prevPods, pods map[string]pod.PodStatus, newPodsNames []string, showProgress bool) *utils.Table {
subT := t.SubTable(statusProgressSubTableRatio...)
st := &subT
st.Header("POD", "READY", "RESTARTS", "STATUS")
podsNames := make([]string, 0, len(pods))
for podName := range pods {
podsNames = append(podsNames, podName)
}
sort.Strings(podsNames)
var podRows [][]interface{}
newPodSet := make(map[string]struct{}, len(newPodsNames))
for _, name := range newPodsNames {
newPodSet[name] = struct{}{}
}
for _, podName := range podsNames {
var podRow []interface{}
_, isPodNew := newPodSet[podName]
prevPodStatus := prevPods[podName]
podStatus := pods[podName]
isReady := false
if podStatus.StatusIndicator != nil {
isReady = podStatus.StatusIndicator.IsReady()
}
resource := formatPodResourceCaption(podName, isReady, podStatus.IsFailed, isPodNew)
ready := fmt.Sprintf("%d/%d", podStatus.ReadyContainers, podStatus.TotalContainers)
status := "-"
if podStatus.StatusIndicator != nil {
status = podStatus.StatusIndicator.FormatTableElem(prevPodStatus.StatusIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
IsResourceNew: isPodNew,
})
}
podRow = append(podRow, resource, ready, podStatus.Restarts, status)
if podStatus.IsFailed {
podRow = append(podRow, formatResourceError(podStatus.FailedReason))
}
podRows = append(podRows, podRow)
}
st.Rows(podRows...)
return st
}
func formatResourceCaption(caption string, isReady, isFailed bool) string {
switch {
case isReady:
return utils.GreenF("%s", caption)
case isFailed:
return utils.RedF("%s", caption)
default:
return utils.YellowF("%s", caption)
}
}
func formatPodResourceCaption(podName string, isReady, isFailed, isNew bool) string {
if !isNew {
return podName
}
return formatResourceCaption(podName, isReady, isFailed)
}
func formatResourceError(reason string) string {
return utils.RedF("error: %s", reason)
}
func formatResourceWarning(reason string) string {
return utils.YellowF("warning: %s", reason)
}
func termWidth() int {
if w, _, err := term.GetSize(int(os.Stderr.Fd())); err == nil && w > 0 {
return w
}
return 140
}
func displayCanaryStatus(out io.Writer, resourceCaption string, status CanaryStatusView) {
var parts []string
if status.Phase != "" {
parts = append(parts, fmt.Sprintf("phase %s", status.Phase))
}
if status.Age != "" {
parts = append(parts, fmt.Sprintf("age %s", status.Age))
}
msg := fmt.Sprintf("%s: %s", resourceCaption, strings.Join(parts, ", "))
if status.IsFailed {
msg = utils.RedF("%s", msg)
}
_, _ = fmt.Fprintln(out, msg)
}
type CanaryStatusView struct {
Phase string
Age string
IsFailed bool
}
func statusOutput() io.Writer {
return os.Stderr
}
+453
View File
@@ -0,0 +1,453 @@
package kubedog
import (
"bytes"
"os"
"strings"
"testing"
"github.com/gookit/color"
"github.com/stretchr/testify/assert"
"github.com/werf/kubedog/pkg/tracker/daemonset"
"github.com/werf/kubedog/pkg/tracker/deployment"
"github.com/werf/kubedog/pkg/tracker/job"
"github.com/werf/kubedog/pkg/tracker/pod"
"github.com/werf/kubedog/pkg/tracker/statefulset"
)
// TestMain forces ANSI color output so that tests asserting on escape codes
// pass in non-TTY environments such as CI runners.
func TestMain(m *testing.M) {
color.ForceColor()
os.Exit(m.Run())
}
// --- formatResourceCaption ---
func TestFormatResourceCaption_Ready(t *testing.T) {
result := formatResourceCaption("deploy/myapp", true, false)
assert.Contains(t, result, "deploy/myapp")
// Green ANSI escape should be present
assert.Contains(t, result, "\033[")
}
func TestFormatResourceCaption_Failed(t *testing.T) {
result := formatResourceCaption("deploy/myapp", false, true)
assert.Contains(t, result, "deploy/myapp")
assert.Contains(t, result, "\033[")
}
func TestFormatResourceCaption_InProgress(t *testing.T) {
result := formatResourceCaption("deploy/myapp", false, false)
assert.Contains(t, result, "deploy/myapp")
// Yellow for in-progress
assert.Contains(t, result, "\033[")
}
func TestFormatResourceCaption_ReadyTakesPrecedence(t *testing.T) {
// isReady=true should win over isFailed=true
resultReady := formatResourceCaption("x", true, false)
resultFailed := formatResourceCaption("x", false, true)
// Colors should differ
assert.NotEqual(t, resultReady, resultFailed)
}
// --- formatPodResourceCaption ---
func TestFormatPodResourceCaption_NotNew(t *testing.T) {
result := formatPodResourceCaption("my-pod-abc", true, false, false)
// Not a new pod: no coloring applied, just the plain name
assert.Equal(t, "my-pod-abc", result)
}
func TestFormatPodResourceCaption_NewAndReady(t *testing.T) {
result := formatPodResourceCaption("my-pod-abc", true, false, true)
assert.Contains(t, result, "my-pod-abc")
assert.Contains(t, result, "\033[")
}
func TestFormatPodResourceCaption_NewAndFailed(t *testing.T) {
result := formatPodResourceCaption("my-pod-abc", false, true, true)
assert.Contains(t, result, "my-pod-abc")
assert.Contains(t, result, "\033[")
}
func TestFormatPodResourceCaption_NewInProgress(t *testing.T) {
result := formatPodResourceCaption("my-pod-abc", false, false, true)
assert.Contains(t, result, "my-pod-abc")
assert.Contains(t, result, "\033[")
}
// --- formatResourceError / formatResourceWarning ---
func TestFormatResourceError(t *testing.T) {
result := formatResourceError("CrashLoopBackOff")
assert.Contains(t, result, "error:")
assert.Contains(t, result, "CrashLoopBackOff")
}
func TestFormatResourceWarning(t *testing.T) {
result := formatResourceWarning("PodNotScheduled")
assert.Contains(t, result, "warning:")
assert.Contains(t, result, "PodNotScheduled")
}
// --- termWidth ---
func TestTermWidth_ReturnsPositive(t *testing.T) {
w := termWidth()
assert.Greater(t, w, 0)
}
// --- displayDeploymentStatusProgress ---
func TestDisplayDeploymentStatusProgress_ZeroStatus(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("deploy/myapp", false, false)
var prev deployment.DeploymentStatus
status := deployment.DeploymentStatus{}
// Must not panic and must produce some output
assert.NotPanics(t, func() {
displayDeploymentStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.NotEmpty(t, out)
assert.Contains(t, out, "DEPLOYMENT")
}
func TestDisplayDeploymentStatusProgress_Failed(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("deploy/myapp", false, true)
var prev deployment.DeploymentStatus
status := deployment.DeploymentStatus{
IsFailed: true,
FailedReason: "ImagePullBackOff",
}
assert.NotPanics(t, func() {
displayDeploymentStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "error:")
assert.Contains(t, out, "ImagePullBackOff")
}
func TestDisplayDeploymentStatusProgress_WithWaitingMessage(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("deploy/myapp", false, false)
var prev deployment.DeploymentStatus
// WaitingForMessages is only rendered when there are pods
status := deployment.DeploymentStatus{
StatusGeneration: 1,
WaitingForMessages: []string{"up-to-date 1->3"},
Pods: map[string]pod.PodStatus{
"myapp-pod-abc": {ReadyContainers: 1, TotalContainers: 1},
},
}
assert.NotPanics(t, func() {
displayDeploymentStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "Waiting for:")
assert.Contains(t, out, "up-to-date 1->3")
}
func TestDisplayDeploymentStatusProgress_WithPods(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("deploy/myapp", false, false)
prev := deployment.DeploymentStatus{}
status := deployment.DeploymentStatus{
StatusGeneration: 1,
Pods: map[string]pod.PodStatus{
"myapp-abc-123": {ReadyContainers: 1, TotalContainers: 1},
},
NewPodsNames: []string{"myapp-abc-123"},
}
assert.NotPanics(t, func() {
displayDeploymentStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "POD")
assert.Contains(t, out, "myapp-abc-123")
}
// --- displayStatefulSetStatusProgress ---
func TestDisplayStatefulSetStatusProgress_ZeroStatus(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("sts/myapp", false, false)
var prev statefulset.StatefulSetStatus
status := statefulset.StatefulSetStatus{}
assert.NotPanics(t, func() {
displayStatefulSetStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "STATEFULSET")
}
func TestDisplayStatefulSetStatusProgress_WithWarnings(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("sts/myapp", false, false)
var prev statefulset.StatefulSetStatus
status := statefulset.StatefulSetStatus{
WarningMessages: []string{"PodNotScheduled: insufficient resources"},
}
assert.NotPanics(t, func() {
displayStatefulSetStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "warning:")
assert.Contains(t, out, "PodNotScheduled")
}
func TestDisplayStatefulSetStatusProgress_Failed(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("sts/myapp", false, true)
var prev statefulset.StatefulSetStatus
status := statefulset.StatefulSetStatus{
IsFailed: true,
FailedReason: "timeout waiting for ready",
}
assert.NotPanics(t, func() {
displayStatefulSetStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "error:")
assert.Contains(t, out, "timeout waiting for ready")
}
// --- displayDaemonSetStatusProgress ---
func TestDisplayDaemonSetStatusProgress_ZeroStatus(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("ds/myapp", false, false)
var prev daemonset.DaemonSetStatus
status := daemonset.DaemonSetStatus{}
assert.NotPanics(t, func() {
displayDaemonSetStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "DAEMONSET")
}
func TestDisplayDaemonSetStatusProgress_Failed(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("ds/myapp", false, true)
var prev daemonset.DaemonSetStatus
status := daemonset.DaemonSetStatus{
IsFailed: true,
FailedReason: "node not ready",
}
assert.NotPanics(t, func() {
displayDaemonSetStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "error:")
assert.Contains(t, out, "node not ready")
}
// --- displayJobStatusProgress ---
func TestDisplayJobStatusProgress_ZeroStatus(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("job/myjob", false, false)
var prev job.JobStatus
status := job.JobStatus{}
assert.NotPanics(t, func() {
displayJobStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "JOB")
}
func TestDisplayJobStatusProgress_Active(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("job/myjob", false, false)
var prev job.JobStatus
status := job.JobStatus{
StatusGeneration: 1,
}
assert.NotPanics(t, func() {
displayJobStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "ACTIVE")
}
func TestDisplayJobStatusProgress_Failed(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("job/myjob", false, true)
var prev job.JobStatus
status := job.JobStatus{
IsFailed: true,
FailedReason: "BackoffLimitExceeded",
}
assert.NotPanics(t, func() {
displayJobStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "error:")
assert.Contains(t, out, "BackoffLimitExceeded")
}
func TestDisplayJobStatusProgress_WithWaitingMessage(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("job/myjob", false, false)
var prev job.JobStatus
status := job.JobStatus{
WaitingForMessages: []string{"succeeded 0->1"},
Pods: map[string]pod.PodStatus{
"myjob-abc": {ReadyContainers: 0, TotalContainers: 1},
},
}
assert.NotPanics(t, func() {
displayJobStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "Waiting for:")
assert.Contains(t, out, "succeeded 0->1")
}
// --- displayChildPodsStatusProgress ---
func TestDisplayChildPodsStatusProgress_Empty(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("deploy/myapp", false, false)
// With no pods, only the header should be rendered
prev := deployment.DeploymentStatus{}
status := deployment.DeploymentStatus{
Pods: map[string]pod.PodStatus{},
}
assert.NotPanics(t, func() {
displayDeploymentStatusProgress(&buf, caption, status, &prev)
})
// No POD sub-table header when pods is empty
out := buf.String()
assert.NotContains(t, out, "POD")
}
func TestDisplayChildPodsStatusProgress_NewPodSet(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("deploy/myapp", false, false)
prev := deployment.DeploymentStatus{}
// Two pods: one new, one old
status := deployment.DeploymentStatus{
StatusGeneration: 1,
Pods: map[string]pod.PodStatus{
"pod-new-abc": {ReadyContainers: 0, TotalContainers: 1},
"pod-old-xyz": {ReadyContainers: 1, TotalContainers: 1},
},
NewPodsNames: []string{"pod-new-abc"},
}
assert.NotPanics(t, func() {
displayDeploymentStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "pod-new-abc")
assert.Contains(t, out, "pod-old-xyz")
}
func TestDisplayChildPodsStatusProgress_ManyPodsO1Check(t *testing.T) {
// Verifies O(1) new-pod detection works correctly for many pods
var buf bytes.Buffer
caption := formatResourceCaption("deploy/myapp", false, false)
prev := deployment.DeploymentStatus{}
pods := make(map[string]pod.PodStatus)
newNames := make([]string, 0, 10)
for i := 0; i < 20; i++ {
name := strings.Repeat("a", i+1)
pods[name] = pod.PodStatus{ReadyContainers: 1, TotalContainers: 1}
if i%2 == 0 {
newNames = append(newNames, name)
}
}
status := deployment.DeploymentStatus{
StatusGeneration: 1,
Pods: pods,
NewPodsNames: newNames,
}
assert.NotPanics(t, func() {
displayDeploymentStatusProgress(&buf, caption, status, &prev)
})
assert.NotEmpty(t, buf.String())
}
// --- displayCanaryStatus ---
func TestDisplayCanaryStatus_Normal(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("canary/myapp", false, false)
view := CanaryStatusView{Phase: "Progressing", Age: "1m"}
assert.NotPanics(t, func() {
displayCanaryStatus(&buf, caption, view)
})
out := buf.String()
assert.Contains(t, out, "Progressing")
assert.Contains(t, out, "1m")
}
func TestDisplayCanaryStatus_Failed(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("canary/myapp", false, true)
view := CanaryStatusView{Phase: "Failed", IsFailed: true}
assert.NotPanics(t, func() {
displayCanaryStatus(&buf, caption, view)
})
out := buf.String()
assert.Contains(t, out, "Failed")
}
func TestDisplayCanaryStatus_Succeeded(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("canary/myapp", true, false)
view := CanaryStatusView{Phase: "Succeeded"}
assert.NotPanics(t, func() {
displayCanaryStatus(&buf, caption, view)
})
out := buf.String()
assert.Contains(t, out, "Succeeded")
}
func TestDisplayCanaryStatus_EmptyPhaseAndAge(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("canary/myapp", false, false)
view := CanaryStatusView{}
assert.NotPanics(t, func() {
displayCanaryStatus(&buf, caption, view)
})
// Should still produce output (at least the caption + newline)
assert.NotEmpty(t, buf.String())
}
// --- writeOut ---
func TestWriteOut(t *testing.T) {
var buf bytes.Buffer
writeOut(&buf, "hello world")
assert.Equal(t, "hello world", buf.String())
}
func TestWriteOut_Empty(t *testing.T) {
var buf bytes.Buffer
writeOut(&buf, "")
assert.Equal(t, "", buf.String())
}
+383 -58
View File
@@ -9,8 +9,15 @@ import (
"sync"
"time"
"github.com/werf/kubedog-for-werf-helm/pkg/tracker"
"github.com/werf/kubedog-for-werf-helm/pkg/trackers/rollout/multitrack"
"github.com/werf/kubedog/pkg/display"
"github.com/werf/kubedog/pkg/informer"
"github.com/werf/kubedog/pkg/tracker"
"github.com/werf/kubedog/pkg/tracker/canary"
"github.com/werf/kubedog/pkg/tracker/daemonset"
"github.com/werf/kubedog/pkg/tracker/deployment"
"github.com/werf/kubedog/pkg/tracker/job"
"github.com/werf/kubedog/pkg/tracker/statefulset"
"github.com/werf/kubedog/pkg/trackers/dyntracker/util"
"go.uber.org/zap"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/client-go/discovery"
@@ -20,6 +27,7 @@ import (
"k8s.io/client-go/rest"
"k8s.io/client-go/restmapper"
"k8s.io/client-go/tools/clientcmd"
watchtools "k8s.io/client-go/tools/watch"
"github.com/helmfile/helmfile/pkg/resource"
)
@@ -187,6 +195,12 @@ func getOrCreateClients(kubeContext, kubeconfig string, qps float32, burst int)
return cache, nil
}
type trackTarget struct {
kind string
name string
namespace string
}
func (t *Tracker) TrackResources(ctx context.Context, resources []*resource.Resource) error {
if len(resources) == 0 {
t.logger.Info("No resources to track")
@@ -201,81 +215,392 @@ func (t *Tracker) TrackResources(ctx context.Context, resources []*resource.Reso
t.logger.Infof("Tracking %d resources with kubedog (filtered from %d total)", len(filtered), len(resources))
specs := multitrack.MultitrackSpecs{}
targets := t.buildTargets(filtered)
if len(targets) == 0 {
t.logger.Info("No trackable resources found (only Deployment, StatefulSet, DaemonSet, Job, and Canary are supported)")
return nil
}
for _, res := range filtered {
t.logger.Infof("Tracking breakdown: %s", t.targetSummary(targets))
watchErrCh := make(chan error, len(targets))
informerFactory := informer.NewConcurrentInformerFactory(
ctx.Done(),
watchErrCh,
t.dynamicClient,
informer.ConcurrentInformerFactoryOptions{},
)
opts := tracker.Options{
ParentContext: ctx,
Timeout: t.trackOptions.Timeout,
LogsFromTime: time.Now().Add(-t.trackOptions.LogsSince),
IgnoreLogs: !t.trackOptions.Logs,
}
var wg sync.WaitGroup
errCh := make(chan error, len(targets))
for _, target := range targets {
wg.Add(1)
go func(tgt trackTarget) {
defer wg.Done()
if err := t.trackSingleResource(tgt, informerFactory, opts); err != nil {
errCh <- fmt.Errorf("%s/%s tracking failed: %w", tgt.kind, tgt.name, err)
}
}(target)
}
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case err := <-errCh:
return fmt.Errorf("tracking failed: %w", err)
case <-done:
t.logger.Info("All resources tracked successfully")
return nil
case <-ctx.Done():
return fmt.Errorf("tracking canceled: %w", ctx.Err())
}
}
func (t *Tracker) trackSingleResource(target trackTarget, informerFactory *util.Concurrent[*informer.InformerFactory], opts tracker.Options) error {
parentContext := opts.ParentContext
if parentContext == nil {
parentContext = context.Background()
}
ctx, cancel := watchtools.ContextWithOptionalTimeout(parentContext, opts.Timeout)
defer cancel()
trackErrCh := make(chan error, 1)
doneCh := make(chan struct{})
switch target.kind {
case "deploy":
tr := deployment.NewTracker(target.name, target.namespace, t.clientSet, informerFactory, opts)
go t.runDeploymentTracker(ctx, tr, trackErrCh, doneCh)
return t.waitDeploymentTracker(ctx, tr, trackErrCh, doneCh)
case "sts":
tr := statefulset.NewTracker(target.name, target.namespace, t.clientSet, informerFactory, opts)
go t.runStatefulSetTracker(ctx, tr, trackErrCh, doneCh)
return t.waitStatefulSetTracker(ctx, tr, trackErrCh, doneCh)
case "ds":
tr := daemonset.NewTracker(target.name, target.namespace, t.clientSet, informerFactory, opts)
go t.runDaemonSetTracker(ctx, tr, trackErrCh, doneCh)
return t.waitDaemonSetTracker(ctx, tr, trackErrCh, doneCh)
case "job":
tr := job.NewTracker(target.name, target.namespace, t.clientSet, informerFactory, opts)
go t.runJobTracker(ctx, tr, trackErrCh, doneCh)
return t.waitJobTracker(ctx, tr, trackErrCh, doneCh)
case "canary":
tr := canary.NewTracker(target.name, target.namespace, t.clientSet, t.dynamicClient, informerFactory, opts)
go t.runCanaryTracker(ctx, tr, trackErrCh, doneCh)
return t.waitCanaryTracker(ctx, tr, trackErrCh, doneCh)
default:
return fmt.Errorf("unsupported resource kind: %s", target.kind)
}
}
func (t *Tracker) runDeploymentTracker(ctx context.Context, tr *deployment.Tracker, errCh chan<- error, doneCh chan<- struct{}) {
if err := tr.Track(ctx); err != nil {
errCh <- err
} else {
close(doneCh)
}
}
func (t *Tracker) waitDeploymentTracker(ctx context.Context, tr *deployment.Tracker, trackErrCh <-chan error, doneCh <-chan struct{}) error {
var prevStatus deployment.DeploymentStatus
out := statusOutput()
resourceName := fmt.Sprintf("deploy/%s", tr.ResourceName)
for {
select {
case status := <-tr.Added:
displayDeploymentStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
prevStatus = status
case <-tr.Ready:
displayDeploymentStatusProgress(out, formatResourceCaption(resourceName, true, false), prevStatus, &prevStatus)
t.logger.Infof("Deployment %s/%s is ready", tr.Namespace, tr.ResourceName)
return nil
case status := <-tr.Failed:
displayDeploymentStatusProgress(out, formatResourceCaption(resourceName, false, true), status, &prevStatus)
return fmt.Errorf("deployment %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason)
case status := <-tr.Status:
if status.StatusGeneration > prevStatus.StatusGeneration {
displayDeploymentStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
prevStatus = status
}
case msg := <-tr.EventMsg:
t.logger.Infof("deploy/%s: %s", tr.ResourceName, msg)
case chunk := <-tr.PodLogChunk:
t.logPodLogChunk(chunk.PodName, chunk.LogLines)
case report := <-tr.PodError:
t.logger.Warnf("deploy/%s pod %s: %s: %s", tr.ResourceName, report.ReplicaSetPodError.PodName, report.ReplicaSetPodError.ContainerName, report.ReplicaSetPodError.Message)
case <-tr.AddedReplicaSet:
case <-tr.AddedPod:
case err := <-trackErrCh:
return err
case <-doneCh:
return nil
case <-ctx.Done():
return fmt.Errorf("tracking canceled for deployment %s/%s: %w", tr.Namespace, tr.ResourceName, ctx.Err())
}
}
}
func (t *Tracker) runStatefulSetTracker(ctx context.Context, tr *statefulset.Tracker, errCh chan<- error, doneCh chan<- struct{}) {
if err := tr.Track(ctx); err != nil {
errCh <- err
} else {
close(doneCh)
}
}
func (t *Tracker) waitStatefulSetTracker(ctx context.Context, tr *statefulset.Tracker, trackErrCh <-chan error, doneCh <-chan struct{}) error {
var prevStatus statefulset.StatefulSetStatus
out := statusOutput()
resourceName := fmt.Sprintf("sts/%s", tr.ResourceName)
for {
select {
case status := <-tr.Added:
displayStatefulSetStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
prevStatus = status
case <-tr.Ready:
displayStatefulSetStatusProgress(out, formatResourceCaption(resourceName, true, false), prevStatus, &prevStatus)
t.logger.Infof("StatefulSet %s/%s is ready", tr.Namespace, tr.ResourceName)
return nil
case status := <-tr.Failed:
displayStatefulSetStatusProgress(out, formatResourceCaption(resourceName, false, true), status, &prevStatus)
return fmt.Errorf("statefulset %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason)
case status := <-tr.Status:
if status.StatusGeneration > prevStatus.StatusGeneration {
displayStatefulSetStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
prevStatus = status
}
case msg := <-tr.EventMsg:
t.logger.Infof("sts/%s: %s", tr.ResourceName, msg)
case chunk := <-tr.PodLogChunk:
t.logPodLogChunk(chunk.PodName, chunk.LogLines)
case report := <-tr.PodError:
t.logger.Warnf("sts/%s pod %s: %s: %s", tr.ResourceName, report.ReplicaSetPodError.PodName, report.ReplicaSetPodError.ContainerName, report.ReplicaSetPodError.Message)
case <-tr.AddedPod:
case err := <-trackErrCh:
return err
case <-doneCh:
return nil
case <-ctx.Done():
return fmt.Errorf("tracking canceled for statefulset %s/%s: %w", tr.Namespace, tr.ResourceName, ctx.Err())
}
}
}
func (t *Tracker) runDaemonSetTracker(ctx context.Context, tr *daemonset.Tracker, errCh chan<- error, doneCh chan<- struct{}) {
if err := tr.Track(ctx); err != nil {
errCh <- err
} else {
close(doneCh)
}
}
func (t *Tracker) waitDaemonSetTracker(ctx context.Context, tr *daemonset.Tracker, trackErrCh <-chan error, doneCh <-chan struct{}) error {
var prevStatus daemonset.DaemonSetStatus
out := statusOutput()
resourceName := fmt.Sprintf("ds/%s", tr.ResourceName)
for {
select {
case status := <-tr.Added:
displayDaemonSetStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
prevStatus = status
case <-tr.Ready:
displayDaemonSetStatusProgress(out, formatResourceCaption(resourceName, true, false), prevStatus, &prevStatus)
t.logger.Infof("DaemonSet %s/%s is ready", tr.Namespace, tr.ResourceName)
return nil
case status := <-tr.Failed:
displayDaemonSetStatusProgress(out, formatResourceCaption(resourceName, false, true), status, &prevStatus)
return fmt.Errorf("daemonset %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason)
case status := <-tr.Status:
if status.StatusGeneration > prevStatus.StatusGeneration {
displayDaemonSetStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
prevStatus = status
}
case msg := <-tr.EventMsg:
t.logger.Infof("ds/%s: %s", tr.ResourceName, msg)
case chunk := <-tr.PodLogChunk:
t.logPodLogChunk(chunk.PodName, chunk.LogLines)
case report := <-tr.PodError:
t.logger.Warnf("ds/%s pod %s: %s: %s", tr.ResourceName, report.PodError.PodName, report.PodError.ContainerName, report.PodError.Message)
case <-tr.AddedPod:
case err := <-trackErrCh:
return err
case <-doneCh:
return nil
case <-ctx.Done():
return fmt.Errorf("tracking canceled for daemonset %s/%s: %w", tr.Namespace, tr.ResourceName, ctx.Err())
}
}
}
func (t *Tracker) runJobTracker(ctx context.Context, tr *job.Tracker, errCh chan<- error, doneCh chan<- struct{}) {
if err := tr.Track(ctx); err != nil {
errCh <- err
} else {
close(doneCh)
}
}
func (t *Tracker) waitJobTracker(ctx context.Context, tr *job.Tracker, trackErrCh <-chan error, doneCh <-chan struct{}) error {
var prevStatus job.JobStatus
out := statusOutput()
resourceName := fmt.Sprintf("job/%s", tr.ResourceName)
for {
select {
case status := <-tr.Added:
displayJobStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
prevStatus = status
case <-tr.Succeeded:
displayJobStatusProgress(out, formatResourceCaption(resourceName, true, false), prevStatus, &prevStatus)
t.logger.Infof("Job %s/%s succeeded", tr.Namespace, tr.ResourceName)
return nil
case status := <-tr.Failed:
displayJobStatusProgress(out, formatResourceCaption(resourceName, false, true), status, &prevStatus)
return fmt.Errorf("job %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason)
case status := <-tr.Status:
if status.StatusGeneration > prevStatus.StatusGeneration {
displayJobStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
prevStatus = status
}
case msg := <-tr.EventMsg:
t.logger.Infof("job/%s: %s", tr.ResourceName, msg)
case chunk := <-tr.PodLogChunk:
t.logPodLogChunk(chunk.PodName, chunk.LogLines)
case report := <-tr.PodError:
t.logger.Warnf("job/%s pod %s: %s: %s", tr.ResourceName, report.PodError.PodName, report.PodError.ContainerName, report.PodError.Message)
case <-tr.AddedPod:
case err := <-trackErrCh:
return err
case <-doneCh:
return nil
case <-ctx.Done():
return fmt.Errorf("tracking canceled for job %s/%s: %w", tr.Namespace, tr.ResourceName, ctx.Err())
}
}
}
func (t *Tracker) runCanaryTracker(ctx context.Context, tr *canary.Tracker, errCh chan<- error, doneCh chan<- struct{}) {
if err := tr.Track(ctx); err != nil {
errCh <- err
} else {
close(doneCh)
}
}
func (t *Tracker) waitCanaryTracker(ctx context.Context, tr *canary.Tracker, trackErrCh <-chan error, doneCh <-chan struct{}) error {
out := statusOutput()
resourceName := fmt.Sprintf("canary/%s", tr.ResourceName)
var lastView CanaryStatusView
for {
select {
case status := <-tr.Added:
view := CanaryStatusView{
Phase: string(status.CanaryStatus.Phase),
IsFailed: status.IsFailed,
}
displayCanaryStatus(out, formatResourceCaption(resourceName, false, false), view)
lastView = view
case <-tr.Succeeded:
displayCanaryStatus(out, formatResourceCaption(resourceName, true, false), CanaryStatusView{Phase: lastView.Phase})
t.logger.Infof("Canary %s/%s succeeded", tr.Namespace, tr.ResourceName)
return nil
case status := <-tr.Failed:
displayCanaryStatus(out, formatResourceCaption(resourceName, false, true), CanaryStatusView{
Phase: status.FailedReason,
IsFailed: true,
})
return fmt.Errorf("canary %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason)
case status := <-tr.Status:
view := CanaryStatusView{
Phase: func() string {
if status.StatusIndicator != nil {
return status.StatusIndicator.Value
}
return ""
}(),
Age: status.Age,
IsFailed: status.IsFailed,
}
displayCanaryStatus(out, formatResourceCaption(resourceName, false, false), view)
lastView = view
case msg := <-tr.EventMsg:
t.logger.Infof("canary/%s: %s", tr.ResourceName, msg)
case err := <-trackErrCh:
return err
case <-doneCh:
return nil
case <-ctx.Done():
return fmt.Errorf("tracking canceled for canary %s/%s: %w", tr.Namespace, tr.ResourceName, ctx.Err())
}
}
}
func (t *Tracker) logPodLogChunk(podName string, logLines []display.LogLine) {
for _, line := range logLines {
t.logger.Infof("po/%s [%s] %s", podName, line.Timestamp, line.Message)
}
}
func (t *Tracker) buildTargets(resources []*resource.Resource) []trackTarget {
var targets []trackTarget
for _, res := range resources {
namespace := res.Namespace
if namespace == "" {
namespace = t.namespace
}
kind := ""
switch strings.ToLower(res.Kind) {
case "deployment", "deploy":
specs.Deployments = append(specs.Deployments, multitrack.MultitrackSpec{
ResourceName: res.Name,
Namespace: namespace,
SkipLogs: !t.trackOptions.Logs,
})
kind = "deploy"
case "statefulset", "sts":
specs.StatefulSets = append(specs.StatefulSets, multitrack.MultitrackSpec{
ResourceName: res.Name,
Namespace: namespace,
SkipLogs: !t.trackOptions.Logs,
})
kind = "sts"
case "daemonset", "ds":
specs.DaemonSets = append(specs.DaemonSets, multitrack.MultitrackSpec{
ResourceName: res.Name,
Namespace: namespace,
SkipLogs: !t.trackOptions.Logs,
})
kind = "ds"
case "job":
specs.Jobs = append(specs.Jobs, multitrack.MultitrackSpec{
ResourceName: res.Name,
Namespace: namespace,
SkipLogs: !t.trackOptions.Logs,
})
kind = "job"
case "canary":
specs.Canaries = append(specs.Canaries, multitrack.MultitrackSpec{
ResourceName: res.Name,
Namespace: namespace,
SkipLogs: !t.trackOptions.Logs,
})
kind = "canary"
default:
t.logger.Debugf("Skipping unsupported kind %s for resource %s/%s", res.Kind, namespace, res.Name)
continue
}
targets = append(targets, trackTarget{
kind: kind,
name: res.Name,
namespace: namespace,
})
}
return targets
}
totalResources := len(specs.Deployments) + len(specs.StatefulSets) +
len(specs.DaemonSets) + len(specs.Jobs) + len(specs.Canaries)
if totalResources == 0 {
t.logger.Info("No trackable resources found (only Deployment, StatefulSet, DaemonSet, Job, and Canary are supported)")
return nil
func (t *Tracker) targetSummary(targets []trackTarget) string {
counts := make(map[string]int)
for _, tgt := range targets {
counts[tgt.kind]++
}
t.logger.Infof("Tracking breakdown: Deployments=%d, StatefulSets=%d, DaemonSets=%d, Jobs=%d, Canaries=%d",
len(specs.Deployments), len(specs.StatefulSets), len(specs.DaemonSets),
len(specs.Jobs), len(specs.Canaries))
opts := multitrack.MultitrackOptions{
Options: tracker.Options{
ParentContext: ctx,
Timeout: t.trackOptions.Timeout,
LogsFromTime: time.Now().Add(-t.trackOptions.LogsSince),
},
StatusProgressPeriod: 5 * time.Second,
DynamicClient: t.dynamicClient,
DiscoveryClient: t.discovery,
Mapper: t.mapper,
parts := make([]string, 0, len(counts))
for kind, count := range counts {
parts = append(parts, fmt.Sprintf("%ss=%d", kind, count))
}
err := multitrack.Multitrack(t.clientSet, specs, opts)
if err != nil {
return fmt.Errorf("tracking failed: %w", err)
}
t.logger.Info("All resources tracked successfully")
return nil
return strings.Join(parts, ", ")
}
func (t *Tracker) filterResources(resources []*resource.Resource) []*resource.Resource {
+671 -65
View File
@@ -1,14 +1,22 @@
package state
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"go.uber.org/zap"
helmchart "helm.sh/helm/v3/pkg/chart"
"github.com/helmfile/helmfile/pkg/filesystem"
"github.com/helmfile/helmfile/pkg/runtime"
"github.com/helmfile/helmfile/pkg/yaml"
)
func TestRewriteChartDependencies(t *testing.T) {
@@ -65,12 +73,10 @@ dependencies:
expectModified: true,
expectError: false,
validate: func(t *testing.T, modifiedChartYaml string) {
// Should have been converted to absolute path
if strings.Contains(modifiedChartYaml, "file://../relative-chart") {
t.Errorf("relative path should have been converted to absolute")
}
// Should now have an absolute path
if !strings.Contains(modifiedChartYaml, "file://") {
t.Errorf("should still have file:// prefix")
}
@@ -98,22 +104,18 @@ dependencies:
expectModified: true,
expectError: false,
validate: func(t *testing.T, modifiedChartYaml string) {
// HTTPS repo should remain unchanged
if !strings.Contains(modifiedChartYaml, "https://charts.example.com") {
t.Errorf("https repository should not be modified")
}
// Relative path should be converted
if strings.Contains(modifiedChartYaml, "file://../relative-chart") {
t.Errorf("relative file:// path should have been converted")
}
// Absolute path should remain unchanged
if !strings.Contains(modifiedChartYaml, "file:///absolute/chart") {
t.Errorf("absolute file:// path should not be modified")
}
// OCI repo should remain unchanged
if !strings.Contains(modifiedChartYaml, "oci://registry.example.com") {
t.Errorf("oci repository should not be modified")
}
@@ -138,7 +140,6 @@ dependencies:
expectModified: true,
expectError: false,
validate: func(t *testing.T, modifiedChartYaml string) {
// All relative paths should be converted
if strings.Contains(modifiedChartYaml, "file://../chart1") ||
strings.Contains(modifiedChartYaml, "file://./chart2") ||
strings.Contains(modifiedChartYaml, "file://../../../chart3") {
@@ -185,14 +186,12 @@ extra-field: aaa
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create temporary directory for test
tempDir, err := os.MkdirTemp("", "helmfile-test-")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
// Create Chart.yaml if provided
if tt.chartYaml != "" {
chartYamlPath := filepath.Join(tempDir, "Chart.yaml")
if err := os.WriteFile(chartYamlPath, []byte(tt.chartYaml), 0644); err != nil {
@@ -200,22 +199,13 @@ extra-field: aaa
}
}
// Create HelmState with logger
logger := zap.NewNop().Sugar()
st := &HelmState{
logger: logger,
fs: filesystem.DefaultFileSystem(),
}
// Read original content if it exists
var originalContent []byte
chartYamlPath := filepath.Join(tempDir, "Chart.yaml")
if _, err := os.Stat(chartYamlPath); err == nil {
originalContent, _ = os.ReadFile(chartYamlPath)
}
// Call rewriteChartDependencies
cleanup, err := st.rewriteChartDependencies(tempDir)
rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
if tt.expectError {
if err == nil {
@@ -224,44 +214,39 @@ extra-field: aaa
return
}
if tt.expectModified {
if rewrittenPath == tempDir {
t.Errorf("expected rewrittenPath != tempDir when modifications are needed, got same path")
}
} else {
if rewrittenPath != tempDir {
t.Errorf("expected rewrittenPath == tempDir when no modifications are needed, got %q", rewrittenPath)
}
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer cleanup()
if tt.chartYaml == "" {
return
}
modifiedChartBytes, err := os.ReadFile(filepath.Join(tempDir, "Chart.yaml"))
modifiedChartBytes, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.yaml"))
if err != nil {
t.Fatalf("failed to read Chart.yaml: %v", err)
}
modifiedChartYaml := string(modifiedChartBytes)
// Validate the modified Chart.yaml
if tt.validate != nil {
tt.validate(t, modifiedChartYaml)
}
// Call cleanup and verify restoration
cleanup()
// Read restored content
restoredContent, err := os.ReadFile(chartYamlPath)
if err != nil {
t.Fatalf("failed to read restored Chart.yaml: %v", err)
}
// Verify content was restored
if string(restoredContent) != string(originalContent) {
t.Errorf("cleanup did not restore original content\noriginal:\n%s\nrestored:\n%s",
string(originalContent), string(restoredContent))
}
})
}
}
func TestRewriteChartDependencies_CleanupRestoresOriginal(t *testing.T) {
func TestRewriteChartDependencies_OriginalNotModified(t *testing.T) {
tempDir, err := os.MkdirTemp("", "helmfile-test-")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
@@ -288,33 +273,44 @@ dependencies:
fs: filesystem.DefaultFileSystem(),
}
cleanup, err := st.rewriteChartDependencies(tempDir)
rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Verify modification happened
modifiedContent, err := os.ReadFile(chartYamlPath)
t.Cleanup(func() {
if rewrittenPath == tempDir {
return
}
cleanup()
if _, statErr := os.Stat(rewrittenPath); !os.IsNotExist(statErr) {
t.Errorf("expected rewritten chart path %q to be removed after cleanup", rewrittenPath)
}
})
if rewrittenPath == tempDir {
t.Errorf("expected a different path when modifications are needed, got same path")
}
modifiedContent, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.yaml"))
if err != nil {
t.Fatalf("failed to read modified Chart.yaml: %v", err)
}
if string(modifiedContent) == originalChart {
t.Errorf("Chart.yaml should have been modified")
t.Errorf("Chart.yaml in the copy should have been modified")
}
// Call cleanup
cleanup()
// Verify restoration
restoredContent, err := os.ReadFile(chartYamlPath)
originalContent, err := os.ReadFile(chartYamlPath)
if err != nil {
t.Fatalf("failed to read restored Chart.yaml: %v", err)
t.Fatalf("failed to read original Chart.yaml: %v", err)
}
if string(restoredContent) != originalChart {
t.Errorf("cleanup did not restore original content\nexpected:\n%s\ngot:\n%s",
originalChart, string(restoredContent))
if string(originalContent) != originalChart {
t.Errorf("original Chart.yaml should not have been modified\nexpected:\n%s\ngot:\n%s",
originalChart, string(originalContent))
}
}
@@ -353,21 +349,30 @@ dependencies:
fs: filesystem.DefaultFileSystem(),
}
cleanup, err := st.rewriteChartDependencies(tempDir)
rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer cleanup()
// Read modified content
modifiedContent, err := os.ReadFile(chartYamlPath)
t.Cleanup(func() {
if rewrittenPath == tempDir {
return
}
cleanup()
if _, statErr := os.Stat(rewrittenPath); !os.IsNotExist(statErr) {
t.Errorf("expected rewritten chart path %q to be removed after cleanup", rewrittenPath)
}
})
modifiedContent, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.yaml"))
if err != nil {
t.Fatalf("failed to read modified Chart.yaml: %v", err)
}
content := string(modifiedContent)
// Verify fields are preserved
requiredFields := []string{
"apiVersion: v2",
"name: test-chart",
@@ -384,10 +389,17 @@ dependencies:
}
}
// Verify the dependency was rewritten
if strings.Contains(content, "file://../relative-chart") {
t.Errorf("relative path should have been converted to absolute")
}
originalContent, err := os.ReadFile(chartYamlPath)
if err != nil {
t.Fatalf("failed to read original Chart.yaml: %v", err)
}
if string(originalContent) != chartYaml {
t.Errorf("original Chart.yaml should not have been modified")
}
}
func TestRewriteChartDependencies_ErrorHandling(t *testing.T) {
@@ -395,7 +407,6 @@ func TestRewriteChartDependencies_ErrorHandling(t *testing.T) {
name string
setupFunc func(tempDir string) error
expectError bool
errorMsg string
}{
{
name: "invalid yaml in Chart.yaml",
@@ -444,7 +455,7 @@ dependencies:
fs: filesystem.DefaultFileSystem(),
}
_, err = st.rewriteChartDependencies(tempDir)
_, _, err = st.rewriteChartDependencies(tempDir)
if tt.expectError && err == nil {
t.Errorf("expected error but got none")
@@ -463,8 +474,6 @@ func TestRewriteChartDependencies_WindowsStylePath(t *testing.T) {
}
defer os.RemoveAll(tempDir)
// Test with backslash (Windows-style) paths
// Note: file:// URLs should use forward slashes, but test handling of edge cases
chartYaml := `apiVersion: v2
name: test-chart
version: 1.0.0
@@ -485,21 +494,618 @@ dependencies:
fs: filesystem.DefaultFileSystem(),
}
cleanup, err := st.rewriteChartDependencies(tempDir)
rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer cleanup()
// Should handle the path correctly
data, err := os.ReadFile(chartYamlPath)
t.Cleanup(func() {
if rewrittenPath == tempDir {
return
}
cleanup()
if _, statErr := os.Stat(rewrittenPath); !os.IsNotExist(statErr) {
t.Errorf("expected rewritten chart path %q to be removed after cleanup", rewrittenPath)
}
})
data, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.yaml"))
if err != nil {
t.Fatalf("failed to read Chart.yaml: %v", err)
}
content := string(data)
// The relative path should have been converted to absolute
if strings.Contains(content, "file://./subdir/chart") {
t.Errorf("relative path with ./ should have been converted")
}
}
func TestRewriteChartDependencies_RaceCondition(t *testing.T) {
tempDir, err := os.MkdirTemp("", "helmfile-test-")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
chartYaml := `apiVersion: v2
name: test-chart
version: 1.0.0
dependencies:
- name: dep1
repository: file://../relative-chart
version: 1.0.0
`
chartYamlPath := filepath.Join(tempDir, "Chart.yaml")
if err := os.WriteFile(chartYamlPath, []byte(chartYaml), 0644); err != nil {
t.Fatalf("failed to write Chart.yaml: %v", err)
}
numGoroutines := 10
var wg sync.WaitGroup
var readyWg sync.WaitGroup
errCh := make(chan error, numGoroutines)
ready := make(chan struct{})
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
readyWg.Add(1)
go func() {
defer wg.Done()
readyWg.Done()
<-ready
logger := zap.NewNop().Sugar()
st := &HelmState{
logger: logger,
fs: filesystem.DefaultFileSystem(),
}
rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
if err != nil {
errCh <- err
return
}
defer cleanup()
data, readErr := os.ReadFile(filepath.Join(rewrittenPath, "Chart.yaml"))
if readErr != nil {
errCh <- readErr
return
}
type ChartDependency struct {
Name string `yaml:"name"`
Repository string `yaml:"repository"`
Version string `yaml:"version"`
}
type ChartMeta struct {
APIVersion string `yaml:"apiVersion"`
Name string `yaml:"name"`
Version string `yaml:"version"`
Dependencies []ChartDependency `yaml:"dependencies,omitempty"`
}
var meta ChartMeta
if unmarshalErr := yaml.Unmarshal(data, &meta); unmarshalErr != nil {
errCh <- unmarshalErr
return
}
if meta.Name != "test-chart" {
errCh <- fmt.Errorf("expected chart name 'test-chart', got %q", meta.Name)
return
}
}()
}
readyWg.Wait()
close(ready)
wg.Wait()
close(errCh)
for err := range errCh {
t.Errorf("goroutine error: %v", err)
}
data, err := os.ReadFile(chartYamlPath)
if err != nil {
t.Fatalf("failed to read original Chart.yaml: %v", err)
}
type ChartDependency struct {
Name string `yaml:"name"`
Repository string `yaml:"repository"`
Version string `yaml:"version"`
}
type ChartMeta struct {
APIVersion string `yaml:"apiVersion"`
Name string `yaml:"name"`
Version string `yaml:"version"`
Dependencies []ChartDependency `yaml:"dependencies,omitempty"`
}
var chartMeta ChartMeta
if err := yaml.Unmarshal(data, &chartMeta); err != nil {
t.Fatalf("original Chart.yaml is not valid YAML: %v", err)
}
if chartMeta.Name != "test-chart" {
t.Errorf("expected original chart name 'test-chart', got %q", chartMeta.Name)
}
if len(chartMeta.Dependencies) == 0 {
t.Fatalf("expected original Chart.yaml to contain at least one dependency, got %d", len(chartMeta.Dependencies))
}
const wantDependencyName = "dep1"
if chartMeta.Dependencies[0].Name != wantDependencyName {
t.Errorf("expected first dependency name %q, got %q", wantDependencyName, chartMeta.Dependencies[0].Name)
}
const wantRepository = "file://../relative-chart"
if chartMeta.Dependencies[0].Repository != wantRepository {
t.Errorf("expected original dependency repository %q, got %q", wantRepository, chartMeta.Dependencies[0].Repository)
}
}
// TestRewriteChartDependencies_RefreshesChartLock verifies that when Chart.yaml has
// its file:// dependencies rewritten to absolute paths, an existing Chart.lock is
// also updated in the temp copy: the digest is recomputed (otherwise `helm dep
// build` would error with "lock out of sync") and matching file:// repository URLs
// are mirrored over from the rewritten Chart.yaml (otherwise `helm dep build` would
// resolve the lock's relative file:// path against the temp directory and fail).
// Locked versions are preserved verbatim.
func TestRewriteChartDependencies_RefreshesChartLock(t *testing.T) {
tempDir := t.TempDir()
chartYaml := `apiVersion: v2
name: parent-chart
version: 1.0.0
dependencies:
- name: local-dep
repository: file://../local-dep
version: 1.0.0
- name: remote-dep
repository: https://example.com/charts
version: "*"
`
if err := os.WriteFile(filepath.Join(tempDir, "Chart.yaml"), []byte(chartYaml), 0644); err != nil {
t.Fatalf("writing Chart.yaml: %v", err)
}
const originalDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
chartLock := `dependencies:
- name: local-dep
repository: file://../local-dep
version: 1.0.0
- name: remote-dep
repository: https://example.com/charts
version: 1.2.3
digest: ` + originalDigest + `
generated: "2024-01-01T00:00:00Z"
`
if err := os.WriteFile(filepath.Join(tempDir, "Chart.lock"), []byte(chartLock), 0644); err != nil {
t.Fatalf("writing Chart.lock: %v", err)
}
logger := zap.NewNop().Sugar()
st := &HelmState{
basePath: tempDir,
fs: filesystem.DefaultFileSystem(),
logger: logger,
}
rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
if err != nil {
t.Fatalf("rewriteChartDependencies failed: %v", err)
}
defer cleanup()
if rewrittenPath == tempDir {
t.Fatalf("expected a temp copy to be created, got original path %q", rewrittenPath)
}
lockData, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.lock"))
if err != nil {
t.Fatalf("reading rewritten Chart.lock: %v", err)
}
var lock struct {
Dependencies []struct {
Name string `yaml:"name"`
Repository string `yaml:"repository"`
Version string `yaml:"version"`
} `yaml:"dependencies"`
Digest string `yaml:"digest"`
Generated string `yaml:"generated"`
}
if err := yaml.Unmarshal(lockData, &lock); err != nil {
t.Fatalf("parsing rewritten Chart.lock: %v", err)
}
if lock.Digest == originalDigest {
t.Errorf("expected digest to be recomputed; still %q", lock.Digest)
}
if !strings.HasPrefix(lock.Digest, "sha256:") {
t.Errorf("expected sha256 digest, got %q", lock.Digest)
}
if len(lock.Dependencies) != 2 {
t.Fatalf("expected 2 lock dependencies, got %d", len(lock.Dependencies))
}
// The local file:// dependency's repository must be mirrored to the absolute
// path so `helm dep build` can resolve it from the temp chart directory.
localDep := lock.Dependencies[0]
if localDep.Name != "local-dep" {
t.Fatalf("expected first lock dep name 'local-dep', got %q", localDep.Name)
}
if !filepath.IsAbs(strings.TrimPrefix(localDep.Repository, "file://")) {
t.Errorf("expected local-dep repository to be an absolute file:// path, got %q", localDep.Repository)
}
if localDep.Version != "1.0.0" {
t.Errorf("expected local-dep version preserved as 1.0.0, got %q", localDep.Version)
}
// Remote (non-file://) deps must be untouched.
remoteDep := lock.Dependencies[1]
if remoteDep.Repository != "https://example.com/charts" {
t.Errorf("expected remote dep repository unchanged, got %q", remoteDep.Repository)
}
if remoteDep.Version != "1.2.3" {
t.Errorf("expected remote dep version preserved as 1.2.3, got %q", remoteDep.Version)
}
// The original Chart.lock on disk must be untouched.
originalLock, err := os.ReadFile(filepath.Join(tempDir, "Chart.lock"))
if err != nil {
t.Fatalf("reading original Chart.lock: %v", err)
}
if string(originalLock) != chartLock {
t.Errorf("original Chart.lock was modified; expected unchanged content")
}
}
// TestRewriteChartDependencies_RefreshesChartLockWithExtraFields verifies that
// Chart.lock digest recomputation includes all dependency fields (alias, condition,
// tags, import-values, enabled) — not just name/repository/version — so the digest
// stays compatible with Helm's resolver.HashReq for charts using those fields.
// It proves field coverage by running two chart variants under a shared root
// (so file:// paths resolve to the same absolute location) and asserting the
// digests differ only due to extra fields.
func TestRewriteChartDependencies_RefreshesChartLockWithExtraFields(t *testing.T) {
// Use a shared root so both chart variants resolve file://../local-dep to the
// same absolute path — isolating the digest difference to field content only.
sharedRoot := t.TempDir()
chartDir := filepath.Join(sharedRoot, "parent")
if err := os.MkdirAll(chartDir, 0755); err != nil {
t.Fatalf("creating chart dir: %v", err)
}
// Run rewriteChartDependencies for a given Chart.yaml and return the recomputed digest.
getDigest := func(t *testing.T, chartYaml, chartLock string) string {
t.Helper()
if err := os.WriteFile(filepath.Join(chartDir, "Chart.yaml"), []byte(chartYaml), 0644); err != nil {
t.Fatalf("writing Chart.yaml: %v", err)
}
if err := os.WriteFile(filepath.Join(chartDir, "Chart.lock"), []byte(chartLock), 0644); err != nil {
t.Fatalf("writing Chart.lock: %v", err)
}
logger := zap.NewNop().Sugar()
st := &HelmState{
basePath: chartDir,
fs: filesystem.DefaultFileSystem(),
logger: logger,
}
rewrittenPath, cleanup, err := st.rewriteChartDependencies(chartDir)
if err != nil {
t.Fatalf("rewriteChartDependencies failed: %v", err)
}
defer cleanup()
lockData, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.lock"))
if err != nil {
t.Fatalf("reading rewritten Chart.lock: %v", err)
}
var lock struct {
Digest string `yaml:"digest"`
}
if err := yaml.Unmarshal(lockData, &lock); err != nil {
t.Fatalf("parsing rewritten Chart.lock: %v", err)
}
return lock.Digest
}
const originalDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
baseLock := `dependencies:
- name: local-dep
repository: file://../local-dep
version: 1.0.0
alias: my-local
- name: local-dep
repository: file://../local-dep-alt
version: 2.0.0
alias: my-local-alt
digest: ` + originalDigest + `
generated: "2024-01-01T00:00:00Z"
`
// Chart.yaml with extra fields (alias, condition, tags, import-values).
chartYamlWithExtras := `apiVersion: v2
name: parent-chart
version: 1.0.0
dependencies:
- name: local-dep
repository: file://../local-dep
version: 1.0.0
alias: my-local
condition: local-dep.enabled
tags:
- frontend
- optional
import-values:
- child: config
parent: global.config
- name: local-dep
repository: file://../local-dep-alt
version: 2.0.0
alias: my-local-alt
`
// Same chart without condition/tags/import-values — only alias remains.
chartYamlWithoutExtras := `apiVersion: v2
name: parent-chart
version: 1.0.0
dependencies:
- name: local-dep
repository: file://../local-dep
version: 1.0.0
alias: my-local
- name: local-dep
repository: file://../local-dep-alt
version: 2.0.0
alias: my-local-alt
`
digestWith := getDigest(t, chartYamlWithExtras, baseLock)
digestWithout := getDigest(t, chartYamlWithoutExtras, baseLock)
if !strings.HasPrefix(digestWith, "sha256:") {
t.Errorf("expected sha256 digest, got %q", digestWith)
}
if digestWith == originalDigest {
t.Errorf("expected digest to be recomputed; still %q", digestWith)
}
if digestWith == digestWithout {
t.Errorf("digest should differ when extra fields (condition, tags, import-values) are present, but both are %q", digestWith)
}
// Also verify alias-based matching: both deps have name "local-dep" but
// different aliases; both should get their file:// paths rewritten.
if err := os.WriteFile(filepath.Join(chartDir, "Chart.yaml"), []byte(chartYamlWithExtras), 0644); err != nil {
t.Fatalf("writing Chart.yaml: %v", err)
}
if err := os.WriteFile(filepath.Join(chartDir, "Chart.lock"), []byte(baseLock), 0644); err != nil {
t.Fatalf("writing Chart.lock: %v", err)
}
logger := zap.NewNop().Sugar()
st := &HelmState{
basePath: chartDir,
fs: filesystem.DefaultFileSystem(),
logger: logger,
}
rewrittenPath, cleanup, err := st.rewriteChartDependencies(chartDir)
if err != nil {
t.Fatalf("rewriteChartDependencies failed: %v", err)
}
defer cleanup()
lockData, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.lock"))
if err != nil {
t.Fatalf("reading rewritten Chart.lock: %v", err)
}
var lock struct {
Dependencies []struct {
Name string `yaml:"name"`
Repository string `yaml:"repository"`
Version string `yaml:"version"`
Alias string `yaml:"alias"`
} `yaml:"dependencies"`
}
if err := yaml.Unmarshal(lockData, &lock); err != nil {
t.Fatalf("parsing rewritten Chart.lock: %v", err)
}
if len(lock.Dependencies) != 2 {
t.Fatalf("expected 2 lock dependencies, got %d", len(lock.Dependencies))
}
dep1 := lock.Dependencies[0]
if dep1.Alias != "my-local" {
t.Errorf("expected first lock dep alias 'my-local', got %q", dep1.Alias)
}
if !filepath.IsAbs(strings.TrimPrefix(dep1.Repository, "file://")) {
t.Errorf("expected first dep repository to be an absolute file:// path, got %q", dep1.Repository)
}
dep2 := lock.Dependencies[1]
if dep2.Alias != "my-local-alt" {
t.Errorf("expected second lock dep alias 'my-local-alt', got %q", dep2.Alias)
}
if !filepath.IsAbs(strings.TrimPrefix(dep2.Repository, "file://")) {
t.Errorf("expected second dep repository to be an absolute file:// path, got %q", dep2.Repository)
}
}
// TestRewriteChartDependencies_GoYamlV2ImportValues verifies that Chart.lock
// refresh works under go-yaml v2 (HELMFILE_GO_YAML_V3=false), where nested
// maps in import-values decode as map[interface{}]interface{} which json.Marshal
// cannot handle without normalization.
func TestRewriteChartDependencies_GoYamlV2ImportValues(t *testing.T) {
prev := runtime.GoYamlV3
runtime.GoYamlV3 = false
t.Cleanup(func() {
runtime.GoYamlV3 = prev
})
tempDir := t.TempDir()
chartYaml := `apiVersion: v2
name: parent-chart
version: 1.0.0
dependencies:
- name: local-dep
repository: file://../local-dep
version: 1.0.0
import-values:
- child: config
parent: global.config
`
chartLock := `dependencies:
- name: local-dep
repository: file://../local-dep
version: 1.0.0
import-values:
- child: config
parent: global.config
digest: sha256:0000000000000000000000000000000000000000000000000000000000000000
generated: "2024-01-01T00:00:00Z"
`
if err := os.WriteFile(filepath.Join(tempDir, "Chart.yaml"), []byte(chartYaml), 0644); err != nil {
t.Fatalf("writing Chart.yaml: %v", err)
}
if err := os.WriteFile(filepath.Join(tempDir, "Chart.lock"), []byte(chartLock), 0644); err != nil {
t.Fatalf("writing Chart.lock: %v", err)
}
logger := zap.NewNop().Sugar()
st := &HelmState{
basePath: tempDir,
fs: filesystem.DefaultFileSystem(),
logger: logger,
}
rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
if err != nil {
t.Fatalf("rewriteChartDependencies failed: %v", err)
}
defer cleanup()
lockData, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.lock"))
if err != nil {
t.Fatalf("reading rewritten Chart.lock: %v", err)
}
var lock struct {
Digest string `yaml:"digest"`
}
if err := yaml.Unmarshal(lockData, &lock); err != nil {
t.Fatalf("parsing rewritten Chart.lock: %v", err)
}
if !strings.HasPrefix(lock.Digest, "sha256:") {
t.Errorf("expected sha256 digest, got %q", lock.Digest)
}
const originalDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
if lock.Digest == originalDigest {
t.Errorf("expected digest to be recomputed; still %q", lock.Digest)
}
}
// TestRewriteChartDependencies_DigestMatchesHelmHashReq verifies the recomputed
// digest matches what Helm's resolver.HashReq would produce for a known input.
// This guards against producing a digest that is "different" but still rejected
// by `helm dependency build`.
func TestRewriteChartDependencies_DigestMatchesHelmHashReq(t *testing.T) {
tempDir := t.TempDir()
chartYaml := `apiVersion: v2
name: test-chart
version: 1.0.0
dependencies:
- name: dep-a
repository: file://../dep-a
version: 2.0.0
condition: dep-a.enabled
tags:
- backend
`
chartLock := `dependencies:
- name: dep-a
repository: file://../dep-a
version: 2.0.0
digest: sha256:0000000000000000000000000000000000000000000000000000000000000000
generated: "2024-01-01T00:00:00Z"
`
if err := os.WriteFile(filepath.Join(tempDir, "Chart.yaml"), []byte(chartYaml), 0644); err != nil {
t.Fatalf("writing Chart.yaml: %v", err)
}
if err := os.WriteFile(filepath.Join(tempDir, "Chart.lock"), []byte(chartLock), 0644); err != nil {
t.Fatalf("writing Chart.lock: %v", err)
}
logger := zap.NewNop().Sugar()
st := &HelmState{
basePath: tempDir,
fs: filesystem.DefaultFileSystem(),
logger: logger,
}
rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
if err != nil {
t.Fatalf("rewriteChartDependencies failed: %v", err)
}
defer cleanup()
lockData, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.lock"))
if err != nil {
t.Fatalf("reading rewritten Chart.lock: %v", err)
}
var lock struct {
Dependencies []*helmchart.Dependency `yaml:"dependencies"`
Digest string `yaml:"digest"`
}
if err := yaml.Unmarshal(lockData, &lock); err != nil {
t.Fatalf("parsing rewritten Chart.lock: %v", err)
}
// Compute the expected digest independently using Helm's HashReq algorithm:
// sha256(json.Marshal([2][]*chart.Dependency{req, lock}))
// where req = rewritten Chart.yaml deps, lock = rewritten Chart.lock deps.
absDepA, err := filepath.Abs(filepath.Join(tempDir, "../dep-a"))
if err != nil {
t.Fatalf("resolving absolute path: %v", err)
}
req := []*helmchart.Dependency{
{
Name: "dep-a",
Repository: "file://" + absDepA,
Version: "2.0.0",
Condition: "dep-a.enabled",
Tags: []string{"backend"},
},
}
lockDeps := []*helmchart.Dependency{
{
Name: "dep-a",
Repository: "file://" + absDepA,
Version: "2.0.0",
},
}
payload, err := json.Marshal([2][]*helmchart.Dependency{req, lockDeps})
if err != nil {
t.Fatalf("marshaling expected digest payload: %v", err)
}
sum := sha256.Sum256(payload)
expectedDigest := "sha256:" + hex.EncodeToString(sum[:])
if lock.Digest != expectedDigest {
t.Errorf("digest mismatch with Helm's HashReq algorithm:\n got: %s\n want: %s", lock.Digest, expectedDigest)
}
}
+16
View File
@@ -129,6 +129,20 @@ func (d *ResolvedDependencies) Get(chart, versionConstraint string) (string, err
return "", fmt.Errorf("no resolved dependency found for \"%s\", running \"helmfile deps\" may resolve the issue", chart)
}
func dedupResolvedDependencies(deps []ResolvedChartDependency) []ResolvedChartDependency {
seen := map[string]bool{}
result := make([]ResolvedChartDependency, 0, len(deps))
for _, dep := range deps {
key := dep.ChartName + "|" + dep.Repository + "|" + dep.Version
if seen[key] {
continue
}
seen[key] = true
result = append(result, dep)
}
return result
}
func (st *HelmState) mergeLockedDependencies() (*HelmState, error) {
filename, unresolved := getUnresolvedDependenciess(st)
@@ -369,6 +383,8 @@ func (m *chartDependencyManager) doUpdate(chartLockFile string, unresolved *Unre
return lockedReqs.ResolvedDependencies[i].ChartName < lockedReqs.ResolvedDependencies[j].ChartName
})
lockedReqs.ResolvedDependencies = dedupResolvedDependencies(lockedReqs.ResolvedDependencies)
lockedReqs.Version = version.Version()
updatedLockFileContent, err = yaml.Marshal(lockedReqs)
+63
View File
@@ -6,6 +6,69 @@ import (
"github.com/stretchr/testify/require"
)
func TestDedupResolvedDependencies(t *testing.T) {
tests := []struct {
name string
input []ResolvedChartDependency
expected []ResolvedChartDependency
}{
{
name: "no duplicates",
input: []ResolvedChartDependency{
{ChartName: "app-template", Repository: "https://example.com", Version: "4.6.2"},
{ChartName: "redis", Repository: "https://charts.bitnami.com/bitnami", Version: "17.0.7"},
},
expected: []ResolvedChartDependency{
{ChartName: "app-template", Repository: "https://example.com", Version: "4.6.2"},
{ChartName: "redis", Repository: "https://charts.bitnami.com/bitnami", Version: "17.0.7"},
},
},
{
name: "duplicates removed",
input: []ResolvedChartDependency{
{ChartName: "app-template", Repository: "https://example.com", Version: "4.6.2"},
{ChartName: "app-template", Repository: "https://example.com", Version: "4.6.2"},
},
expected: []ResolvedChartDependency{
{ChartName: "app-template", Repository: "https://example.com", Version: "4.6.2"},
},
},
{
name: "same chart different versions kept",
input: []ResolvedChartDependency{
{ChartName: "app-template", Repository: "https://example.com", Version: "4.6.2"},
{ChartName: "app-template", Repository: "https://example.com", Version: "4.6.1"},
},
expected: []ResolvedChartDependency{
{ChartName: "app-template", Repository: "https://example.com", Version: "4.6.2"},
{ChartName: "app-template", Repository: "https://example.com", Version: "4.6.1"},
},
},
{
name: "same chart different repos kept",
input: []ResolvedChartDependency{
{ChartName: "app-template", Repository: "https://example.com", Version: "4.6.2"},
{ChartName: "app-template", Repository: "https://other.com", Version: "4.6.2"},
},
expected: []ResolvedChartDependency{
{ChartName: "app-template", Repository: "https://example.com", Version: "4.6.2"},
{ChartName: "app-template", Repository: "https://other.com", Version: "4.6.2"},
},
},
{
name: "empty input",
input: []ResolvedChartDependency{},
expected: []ResolvedChartDependency{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := dedupResolvedDependencies(tt.input)
require.Equal(t, tt.expected, result)
})
}
}
func TestGetUnresolvedDependenciess(t *testing.T) {
tests := []struct {
name string
+24 -9
View File
@@ -169,7 +169,7 @@ func (c *StateCreator) LoadEnvValues(target *HelmState, env string, failOnMissin
return nil, &StateLoadError{fmt.Sprintf("failed to read %s", state.FilePath), err}
}
newDefaults, err := state.loadValuesEntries(nil, state.DefaultValues, c.remote, ctxEnv, env)
newDefaults, err := state.loadValuesEntries(nil, state.DefaultValues, c.remote, ctxEnv, env, "")
if err != nil {
return nil, err
}
@@ -237,17 +237,12 @@ func mergeEnvironments(dst, src map[string]EnvironmentSpec) {
for envName, srcEnv := range src {
if dstEnv, exists := dst[envName]; exists {
// Environment exists in both - merge the Values slices
mergedValues := append([]any{}, dstEnv.Values...)
mergedValues = append(mergedValues, srcEnv.Values...)
// Merge Secrets slices
mergedSecrets := append([]string{}, dstEnv.Secrets...)
mergedSecrets = append(mergedSecrets, srcEnv.Secrets...)
// Create merged environment
merged := EnvironmentSpec{
Values: mergedValues,
Secrets: mergedSecrets,
}
@@ -272,6 +267,26 @@ func mergeEnvironments(dst, src map[string]EnvironmentSpec) {
merged.MissingFileHandlerConfig = dstEnv.MissingFileHandlerConfig
}
// Override MergeStrategy if src has it
if srcEnv.MergeStrategy != "" {
merged.MergeStrategy = srcEnv.MergeStrategy
} else {
merged.MergeStrategy = dstEnv.MergeStrategy
}
// Concatenate Values so the later layer (src, e.g. main helmfile)
// always takes precedence over the earlier one (dst, e.g. a base
// helmfile). Under override the natural append order achieves that
// (last-file-wins). Under fallback we have to prepend src so that
// "first-file-wins" still resolves to "later layer wins".
if merged.MergeStrategy == MergeStrategyFallback {
mergedValues := append([]any{}, srcEnv.Values...)
merged.Values = append(mergedValues, dstEnv.Values...)
} else {
mergedValues := append([]any{}, dstEnv.Values...)
merged.Values = append(mergedValues, srcEnv.Values...)
}
dst[envName] = merged
} else {
// Environment only exists in src - just copy it
@@ -394,7 +409,7 @@ func (c *StateCreator) loadEnvValues(st *HelmState, name string, failOnMissingEn
if err != nil {
return nil, err
}
valuesVals, err = st.loadValuesEntries(envSpec.MissingFileHandler, envValuesEntries, c.remote, loadValuesEntriesEnv, name)
valuesVals, err = st.loadValuesEntries(envSpec.MissingFileHandler, envValuesEntries, c.remote, loadValuesEntriesEnv, name, envSpec.MergeStrategy)
if err != nil {
return nil, err
}
@@ -550,13 +565,13 @@ func (c *StateCreator) scatterGatherEnvSecretFiles(st *HelmState, envSecretFiles
return decryptedFilesKeeper, nil
}
func (st *HelmState) loadValuesEntries(missingFileHandler *string, entries []any, remote *remote.Remote, ctxEnv *environment.Environment, envName string) (map[string]any, error) {
func (st *HelmState) loadValuesEntries(missingFileHandler *string, entries []any, remote *remote.Remote, ctxEnv *environment.Environment, envName string, mergeStrategy string) (map[string]any, error) {
var envVals map[string]any
valuesEntries := append([]any{}, entries...)
ld := NewEnvironmentValuesLoader(st.storage(), st.fs, st.logger, remote)
var err error
envVals, err = ld.LoadEnvironmentValues(missingFileHandler, valuesEntries, ctxEnv, envName)
envVals, err = ld.LoadEnvironmentValues(missingFileHandler, valuesEntries, ctxEnv, envName, mergeStrategy)
if err != nil {
return nil, err
}
+93
View File
@@ -932,3 +932,96 @@ releases:
})
}
}
// mergeEnvironments must keep the established "later layer wins over earlier
// layer" semantics even when the resolved strategy is fallback. Under override
// the natural append order (dst then src) achieves that because last-wins
// chooses src; under fallback we have to prepend src so that first-wins still
// resolves to src. Without this adjustment, a base helmfile's values would
// silently override values declared in the main helmfile that includes it.
func TestMergeEnvironments_LaterLayerWinsRegardlessOfStrategy(t *testing.T) {
tests := []struct {
name string
strategy string
wantOrdering []any
}{
{
name: "override appends src after dst (last wins)",
strategy: MergeStrategyOverride,
wantOrdering: []any{"base.yaml", "main.yaml"},
},
{
name: "empty strategy defaults to override semantics",
strategy: "",
wantOrdering: []any{"base.yaml", "main.yaml"},
},
{
name: "fallback prepends src (first wins) so main still beats base",
strategy: MergeStrategyFallback,
wantOrdering: []any{"main.yaml", "base.yaml"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dst := map[string]EnvironmentSpec{
"prod": {Values: []any{"base.yaml"}, MergeStrategy: tt.strategy},
}
src := map[string]EnvironmentSpec{
"prod": {Values: []any{"main.yaml"}, MergeStrategy: tt.strategy},
}
mergeEnvironments(dst, src)
got := dst["prod"].Values
if !reflect.DeepEqual(got, tt.wantOrdering) {
t.Errorf("Values ordering: want %v, got %v", tt.wantOrdering, got)
}
})
}
}
// mergeEnvironments must preserve MergeStrategy when layering multiple
// helmfiles (bases). Without this, an environment that opted into
// fallback in a base helmfile would silently fall back to the default
// override behavior in any helmfile that re-declares the environment.
func TestMergeEnvironments_PreservesMergeStrategy(t *testing.T) {
tests := []struct {
name string
dst EnvironmentSpec
src EnvironmentSpec
expected string
}{
{
name: "src declares fallback, dst empty",
dst: EnvironmentSpec{},
src: EnvironmentSpec{MergeStrategy: MergeStrategyFallback},
expected: MergeStrategyFallback,
},
{
name: "dst declares fallback, src empty preserves dst",
dst: EnvironmentSpec{MergeStrategy: MergeStrategyFallback},
src: EnvironmentSpec{},
expected: MergeStrategyFallback,
},
{
name: "src override wins over dst fallback",
dst: EnvironmentSpec{MergeStrategy: MergeStrategyFallback},
src: EnvironmentSpec{MergeStrategy: MergeStrategyOverride},
expected: MergeStrategyOverride,
},
{
name: "both empty stays empty",
dst: EnvironmentSpec{},
src: EnvironmentSpec{},
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dst := map[string]EnvironmentSpec{"prod": tt.dst}
src := map[string]EnvironmentSpec{"prod": tt.src}
mergeEnvironments(dst, src)
if got := dst["prod"].MergeStrategy; got != tt.expected {
t.Errorf("MergeStrategy after merge: want %q, got %q", tt.expected, got)
}
})
}
}
+13
View File
@@ -15,4 +15,17 @@ type EnvironmentSpec struct {
MissingFileHandler *string `yaml:"missingFileHandler,omitempty"`
// MissingFileHandlerConfig is composed of various settings for the MissingFileHandler
MissingFileHandlerConfig *MissingFileHandlerConfig `yaml:"missingFileHandlerConfig,omitempty"`
// MergeStrategy controls precedence when multiple values files are listed under `values`.
//
// "override" (default): later files override earlier files (the historical helmfile behavior).
// "fallback": earlier files take precedence; later files only fill gaps.
//
// Under the "fallback" strategy, an explicit non-nil value in an earlier file (including
// the zero values false, 0, "", and empty list) is preserved against any later file. Maps
// are deep-merged, so an earlier map does not block later files from adding nested keys.
// An explicit null in an earlier file falls through to a later file's value (matching how
// helmfile's MergeMaps treats nil from the override side elsewhere). Subsequent .gotmpl
// values files can also reference values from earlier files via .Values.
MergeStrategy string `yaml:"mergeStrategy,omitempty"`
}
+61 -28
View File
@@ -36,7 +36,13 @@ func NewEnvironmentValuesLoader(storage *Storage, fs *filesystem.FileSystem, log
}
}
func (ld *EnvironmentValuesLoader) LoadEnvironmentValues(missingFileHandler *string, valuesEntries []any, ctxEnv *environment.Environment, envName string) (map[string]any, error) {
func (ld *EnvironmentValuesLoader) LoadEnvironmentValues(missingFileHandler *string, valuesEntries []any, ctxEnv *environment.Environment, envName string, mergeStrategy string) (map[string]any, error) {
switch mergeStrategy {
case "", MergeStrategyOverride, MergeStrategyFallback:
default:
return nil, fmt.Errorf("environment %q: invalid mergeStrategy %q (must be %q or %q)",
envName, mergeStrategy, MergeStrategyOverride, MergeStrategyFallback)
}
var (
result = map[string]any{}
hclLoader = hcllang.NewHCLLoader(ld.fs, ld.logger)
@@ -44,8 +50,6 @@ func (ld *EnvironmentValuesLoader) LoadEnvironmentValues(missingFileHandler *str
)
for _, entry := range valuesEntries {
maps := []any{}
switch strOrMap := entry.(type) {
case string:
files, skipped, err := ld.storage.resolveFile(missingFileHandler, "environment values", entry.(string))
@@ -64,37 +68,56 @@ func (ld *EnvironmentValuesLoader) LoadEnvironmentValues(missingFileHandler *str
}
if strings.HasSuffix(f, ".hcl") {
hclLoader.AddFile(f)
} else {
// Use merged values (Defaults + Values + CLIOverrides) for template rendering
// so that CLI values are accessible via .Values in environment value files.
mergedVals, err := env.GetMergedValues()
if err != nil {
return nil, fmt.Errorf("failed to get merged values for environment file \"%s\": %v", f, err)
continue
}
// Use merged values (Defaults + Values + CLIOverrides) for template rendering
// so that CLI values are accessible via .Values in environment value files.
mergedVals, err := env.GetMergedValues()
if err != nil {
return nil, fmt.Errorf("failed to get merged values for environment file \"%s\": %v", f, err)
}
// Under fallback strategy, also expose values accumulated from earlier files
// in this same `values:` list, including earlier files in this same glob
// expansion, so a later .gotmpl can reference them via .Values (e.g.
// `{{ .Values.cluster.domain }}`). Env CLI overrides and values still win,
// layered on top with WithOverride.
if mergeStrategy == MergeStrategyFallback && len(result) > 0 {
enriched := map[string]any{}
if err := mergo.Merge(&enriched, result); err != nil {
return nil, fmt.Errorf("failed to build template context for \"%s\": %v", f, err)
}
tmplData := NewEnvironmentTemplateData(env, "", mergedVals)
r := tmpl.NewFileRenderer(ld.fs, filepath.Dir(f), tmplData)
bytes, err := r.RenderToBytes(f)
if err != nil {
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", f, err)
if err := mergo.Merge(&enriched, mergedVals, mergo.WithOverride); err != nil {
return nil, fmt.Errorf("failed to build template context for \"%s\": %v", f, err)
}
m := map[string]any{}
if err := yaml.Unmarshal(bytes, &m); err != nil {
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v\n\nOffending YAML:\n%s", f, err, bytes)
}
maps = append(maps, m)
ld.logger.Debugf("envvals_loader: loaded %s:%v", strOrMap, m)
mergedVals = enriched
}
tmplData := NewEnvironmentTemplateData(env, "", mergedVals)
r := tmpl.NewFileRenderer(ld.fs, filepath.Dir(f), tmplData)
bytes, err := r.RenderToBytes(f)
if err != nil {
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", f, err)
}
m := map[string]any{}
if err := yaml.Unmarshal(bytes, &m); err != nil {
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v\n\nOffending YAML:\n%s", f, err, bytes)
}
ld.logger.Debugf("envvals_loader: loaded %s:%v", strOrMap, m)
// Merge each file into result immediately so subsequent files in the same
// entry's expansion (e.g. a glob) can see prior files' values via .Values
// when rendered as templates.
result, err = mapMerge(result, []any{m}, mergeStrategy)
if err != nil {
return nil, err
}
}
case map[any]any, map[string]any:
maps = append(maps, strOrMap)
result, err = mapMerge(result, []any{strOrMap}, mergeStrategy)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unexpected type of value: value=%v, type=%T", strOrMap, strOrMap)
}
result, err = mapMerge(result, maps)
if err != nil {
return nil, err
}
}
maps := []any{}
if hclLoader.Length() > 0 {
@@ -104,14 +127,14 @@ func (ld *EnvironmentValuesLoader) LoadEnvironmentValues(missingFileHandler *str
}
maps = append(maps, m)
}
result, err = mapMerge(result, maps)
result, err = mapMerge(result, maps, mergeStrategy)
if err != nil {
return nil, err
}
return result, nil
}
func mapMerge(dest map[string]any, maps []any) (map[string]any, error) {
func mapMerge(dest map[string]any, maps []any, mergeStrategy string) (map[string]any, error) {
for _, m := range maps {
// All the nested map key should be string. Otherwise we get strange errors due to that
// mergo or reflect is unable to merge map[any]any with map[string]any or vice versa.
@@ -120,6 +143,16 @@ func mapMerge(dest map[string]any, maps []any) (map[string]any, error) {
if err != nil {
return nil, err
}
if mergeStrategy == MergeStrategyFallback {
// First-file-wins: the new file is the base and the
// accumulator overlays it, so keys already accumulated keep
// their value while keys only present in the new file fill
// in. MergeMaps is used instead of mergo because mergo's
// isEmptyValue rule would silently let a later fallback's
// `enabled: true` clobber an explicit `enabled: false`.
dest = maputil.MergeMaps(vals, dest)
continue
}
if err := mergo.Merge(&dest, &vals, mergo.WithOverride); err != nil {
return nil, fmt.Errorf("failed to merge %v: %v", m, err)
}
+298 -8
View File
@@ -2,6 +2,7 @@ package state
import (
"io"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
@@ -29,7 +30,7 @@ func newLoader() *EnvironmentValuesLoader {
func TestEnvValsLoad_SingleValuesFile(t *testing.T) {
l := newLoader()
actual, err := l.LoadEnvironmentValues(nil, []any{"testdata/values.5.yaml"}, nil, "")
actual, err := l.LoadEnvironmentValues(nil, []any{"testdata/values.5.yaml"}, nil, "", "")
if err != nil {
t.Fatal(err)
}
@@ -87,7 +88,7 @@ func TestEnvValsLoad_EnvironmentNameFile(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual, err := l.LoadEnvironmentValues(nil, []any{"testdata/values.6.yaml.gotmpl"}, tt.env, tt.envName)
actual, err := l.LoadEnvironmentValues(nil, []any{"testdata/values.6.yaml.gotmpl"}, tt.env, tt.envName, "")
if err != nil {
t.Fatal(err)
}
@@ -103,7 +104,7 @@ func TestEnvValsLoad_EnvironmentNameFile(t *testing.T) {
func TestEnvValsLoad_SingleValuesFileRemote(t *testing.T) {
l := newLoader()
actual, err := l.LoadEnvironmentValues(nil, []any{"git::https://github.com/helm/helm.git@cmd/helm/testdata/output/values.yaml?ref=v3.8.1"}, nil, "")
actual, err := l.LoadEnvironmentValues(nil, []any{"git::https://github.com/helm/helm.git@cmd/helm/testdata/output/values.yaml?ref=v3.8.1"}, nil, "", "")
if err != nil {
t.Fatal(err)
}
@@ -121,7 +122,7 @@ func TestEnvValsLoad_SingleValuesFileRemote(t *testing.T) {
func TestEnvValsLoad_OverwriteNilValue_Issue1150(t *testing.T) {
l := newLoader()
actual, err := l.LoadEnvironmentValues(nil, []any{"testdata/values.1.yaml", "testdata/values.2.yaml"}, nil, "")
actual, err := l.LoadEnvironmentValues(nil, []any{"testdata/values.1.yaml", "testdata/values.2.yaml"}, nil, "", "")
if err != nil {
t.Fatal(err)
}
@@ -143,7 +144,7 @@ func TestEnvValsLoad_OverwriteNilValue_Issue1150(t *testing.T) {
func TestEnvValsLoad_OverwriteWithNilValue_Issue1154(t *testing.T) {
l := newLoader()
actual, err := l.LoadEnvironmentValues(nil, []any{"testdata/values.3.yaml", "testdata/values.4.yaml"}, nil, "")
actual, err := l.LoadEnvironmentValues(nil, []any{"testdata/values.3.yaml", "testdata/values.4.yaml"}, nil, "", "")
if err != nil {
t.Fatal(err)
}
@@ -166,7 +167,7 @@ func TestEnvValsLoad_OverwriteWithNilValue_Issue1154(t *testing.T) {
func TestEnvValsLoad_OverwriteEmptyValue_Issue1168(t *testing.T) {
l := newLoader()
actual, err := l.LoadEnvironmentValues(nil, []any{"testdata/issues/1168/addons.yaml", "testdata/issues/1168/addons2.yaml"}, nil, "")
actual, err := l.LoadEnvironmentValues(nil, []any{"testdata/issues/1168/addons.yaml", "testdata/issues/1168/addons2.yaml"}, nil, "", "")
if err != nil {
t.Fatal(err)
}
@@ -191,7 +192,7 @@ func TestEnvValsLoad_OverwriteEmptyValue_Issue1168(t *testing.T) {
func TestEnvValsLoad_MultiHCL(t *testing.T) {
l := newLoader()
actual, err := l.LoadEnvironmentValues(nil, []any{"testdata/values.7.hcl", "testdata/values.8.hcl"}, nil, "")
actual, err := l.LoadEnvironmentValues(nil, []any{"testdata/values.7.hcl", "testdata/values.8.hcl"}, nil, "", "")
if err != nil {
t.Fatal(err)
}
@@ -234,7 +235,7 @@ func TestEnvValsLoad_EnvironmentValues(t *testing.T) {
env := environment.New("test")
env.Values["foo"] = "bar"
actual, err := l.LoadEnvironmentValues(nil, []any{"testdata/values.9.yaml.gotmpl"}, env, "")
actual, err := l.LoadEnvironmentValues(nil, []any{"testdata/values.9.yaml.gotmpl"}, env, "", "")
if err != nil {
t.Fatal(err)
}
@@ -247,3 +248,292 @@ func TestEnvValsLoad_EnvironmentValues(t *testing.T) {
t.Error(diff)
}
}
// --- mergeStrategy: fallback ---
// Earlier files take precedence. Same conflicting key in two files →
// the value from default.yaml (loaded first) wins.
func TestEnvValsLoad_FallbackStrategy_EarlierWins(t *testing.T) {
l := newLoader()
actual, err := l.LoadEnvironmentValues(nil,
[]any{"testdata/mergestrategy/default.yaml", "testdata/mergestrategy/fallback.yaml"},
nil, "", MergeStrategyFallback)
if err != nil {
t.Fatal(err)
}
cluster := actual["cluster"].(map[string]any)
if got := cluster["domain"]; got != "example.com" {
t.Errorf("cluster.domain: want %q (from default.yaml), got %v", "example.com", got)
}
}
// Later files only fill keys that are missing from earlier files.
func TestEnvValsLoad_FallbackStrategy_FillsGaps(t *testing.T) {
l := newLoader()
actual, err := l.LoadEnvironmentValues(nil,
[]any{"testdata/mergestrategy/default.yaml", "testdata/mergestrategy/fallback.yaml"},
nil, "", MergeStrategyFallback)
if err != nil {
t.Fatal(err)
}
cluster := actual["cluster"].(map[string]any)
if got := cluster["region"]; got != "us-east-1" {
t.Errorf("cluster.region: want %q (from fallback.yaml, missing in default.yaml), got %v", "us-east-1", got)
}
service := actual["service"].(map[string]any)
if got := service["port"]; got != 8080 {
t.Errorf("service.port: want 8080 (from fallback.yaml), got %v", got)
}
}
// Nested maps merge recursively: top-level cluster is not replaced
// wholesale; both files contribute keys.
func TestEnvValsLoad_FallbackStrategy_DeepMerge(t *testing.T) {
l := newLoader()
actual, err := l.LoadEnvironmentValues(nil,
[]any{"testdata/mergestrategy/default.yaml", "testdata/mergestrategy/fallback.yaml"},
nil, "", MergeStrategyFallback)
if err != nil {
t.Fatal(err)
}
expected := map[string]any{
"cluster": map[string]any{
"domain": "example.com", // from default (wins)
"region": "us-east-1", // from fallback (gap filled)
},
"service": map[string]any{
"port": 8080, // from fallback (gap filled)
},
}
if diff := cmp.Diff(expected, actual); diff != "" {
t.Errorf("deep merge mismatch (-want +got):\n%s", diff)
}
}
// First-wins precedence holds across an arbitrarily long chain, not just
// pairwise. Three files exercise the accumulator state across iterations.
func TestEnvValsLoad_FallbackStrategy_ChainedFiles(t *testing.T) {
l := newLoader()
actual, err := l.LoadEnvironmentValues(nil,
[]any{
"testdata/mergestrategy/chain_a.yaml",
"testdata/mergestrategy/chain_b.yaml",
"testdata/mergestrategy/chain_c.yaml",
},
nil, "", MergeStrategyFallback)
if err != nil {
t.Fatal(err)
}
expected := map[string]any{
"letter": "a", // only in a
"only_a": "from-a", // only in a
"only_b": "from-b", // only in b
"only_c": "from-c", // only in c
"both_ab": "from-a", // a and b → a wins (earlier)
"both_bc": "from-b", // b and c → b wins (earlier)
"all_three": "from-a", // a, b, c → a wins (earliest)
}
if diff := cmp.Diff(expected, actual); diff != "" {
t.Errorf("chain mismatch (-want +got):\n%s", diff)
}
}
// Explicit zero values in the earlier file MUST be preserved. Without the
// hand-rolled fallbackDeepMerge, mergo's isEmptyValue would silently let
// `enabled: true` from fallback overwrite `enabled: false` from default.
func TestEnvValsLoad_FallbackStrategy_PreservesExplicitZeroValues(t *testing.T) {
l := newLoader()
actual, err := l.LoadEnvironmentValues(nil,
[]any{"testdata/mergestrategy/zero_default.yaml", "testdata/mergestrategy/zero_fallback.yaml"},
nil, "", MergeStrategyFallback)
if err != nil {
t.Fatal(err)
}
expected := map[string]any{
"enabled": false,
"replicas": 0,
"name": "",
"tags": []any{},
}
if diff := cmp.Diff(expected, actual); diff != "" {
t.Errorf("explicit zero values not preserved (-want +got):\n%s", diff)
}
}
// Explicit nil in the earlier file does NOT win under fallback: it falls
// through to the fallback file's value. This matches helmfile's existing
// MergeMaps treatment of nil ("nil from the override side only fills missing
// keys"; here, by argument-swap, nil from the winner is treated as
// "no preference, let the fallback fill it"). Documented as the deliberate
// difference from override mode, where mergo.WithOverride lets nil overwrite
// (see TestEnvValsLoad_OverwriteWithNilValue_Issue1154).
func TestEnvValsLoad_FallbackStrategy_NilFallsThroughToFallback(t *testing.T) {
l := newLoader()
actual, err := l.LoadEnvironmentValues(nil,
[]any{"testdata/mergestrategy/nil_default.yaml", "testdata/mergestrategy/nil_fallback.yaml"},
nil, "", MergeStrategyFallback)
if err != nil {
t.Fatal(err)
}
expected := map[string]any{"value": "from-fallback"}
if diff := cmp.Diff(expected, actual); diff != "" {
t.Errorf("explicit nil should fall through to fallback (-want +got):\n%s", diff)
}
}
// Inline map entries (not file paths) also honor the fallback strategy.
func TestEnvValsLoad_FallbackStrategy_InlineMapEntry(t *testing.T) {
l := newLoader()
inline := map[string]any{
"cluster": map[string]any{"domain": "inline.example"},
"extra": "from-inline",
}
actual, err := l.LoadEnvironmentValues(nil,
[]any{inline, "testdata/mergestrategy/fallback.yaml"},
nil, "", MergeStrategyFallback)
if err != nil {
t.Fatal(err)
}
cluster := actual["cluster"].(map[string]any)
if got := cluster["domain"]; got != "inline.example" {
t.Errorf("cluster.domain: want inline value to win, got %v", got)
}
if got := actual["extra"]; got != "from-inline" {
t.Errorf("extra: want %q, got %v", "from-inline", got)
}
// fallback.yaml still fills gaps the inline map did not set.
if got := cluster["region"]; got != "us-east-1" {
t.Errorf("cluster.region: want %q from fallback file, got %v", "us-east-1", got)
}
}
// Regression guard: explicit "override" matches today's behavior
// (last file wins).
func TestEnvValsLoad_OverrideStrategy_PreservesCurrentBehavior(t *testing.T) {
l := newLoader()
actual, err := l.LoadEnvironmentValues(nil,
[]any{"testdata/mergestrategy/default.yaml", "testdata/mergestrategy/fallback.yaml"},
nil, "", MergeStrategyOverride)
if err != nil {
t.Fatal(err)
}
cluster := actual["cluster"].(map[string]any)
if got := cluster["domain"]; got != "cluster.local" {
t.Errorf("cluster.domain under override: want %q (from fallback.yaml), got %v", "cluster.local", got)
}
}
// Empty strategy is identical to explicit "override".
func TestEnvValsLoad_DefaultStrategy_MatchesOverride(t *testing.T) {
l := newLoader()
asDefault, err := l.LoadEnvironmentValues(nil,
[]any{"testdata/mergestrategy/default.yaml", "testdata/mergestrategy/fallback.yaml"},
nil, "", "")
if err != nil {
t.Fatal(err)
}
asOverride, err := l.LoadEnvironmentValues(nil,
[]any{"testdata/mergestrategy/default.yaml", "testdata/mergestrategy/fallback.yaml"},
nil, "", MergeStrategyOverride)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(asOverride, asDefault); diff != "" {
t.Errorf("default strategy diverges from override (-override +default):\n%s", diff)
}
}
// Within a single `values:` entry that expands to multiple files (a glob), a
// later .gotmpl in the expansion can reference earlier files in that same
// expansion. Matters because the inner file loop must merge each parsed file
// into the accumulator before rendering the next, not buffer the whole
// expansion and merge once at the end.
func TestEnvValsLoad_FallbackStrategy_GlobTemplateSeesPriorFileInSameExpansion(t *testing.T) {
l := newLoader()
actual, err := l.LoadEnvironmentValues(nil,
[]any{"testdata/mergestrategy/glob_*.yaml*"},
nil, "", MergeStrategyFallback)
if err != nil {
t.Fatal(err)
}
service := actual["service"].(map[string]any)
if got := service["domain"]; got != "service.example.com" {
t.Errorf("service.domain: want %q (templated from sibling glob match), got %v",
"service.example.com", got)
}
}
// The headline use case: under fallback, a later .gotmpl values file can
// reference values defined by earlier files in the same list via .Values.
// default.yaml sets cluster.domain; fallback.yaml.gotmpl renders
// `service.domain: "service.{{ .Values.cluster.domain }}"`.
func TestEnvValsLoad_FallbackStrategy_TemplateAccessesPriorFile(t *testing.T) {
l := newLoader()
actual, err := l.LoadEnvironmentValues(nil,
[]any{"testdata/mergestrategy/default.yaml", "testdata/mergestrategy/fallback.yaml.gotmpl"},
nil, "", MergeStrategyFallback)
if err != nil {
t.Fatal(err)
}
service := actual["service"].(map[string]any)
if got := service["domain"]; got != "service.example.com" {
t.Errorf("service.domain: want %q (templated from prior file), got %v",
"service.example.com", got)
}
}
// Symmetric guard: under override, the same .gotmpl reference does NOT
// see prior files in the same list. Documents the deliberate scoping:
// the cross-file template enrichment is opt-in via mergeStrategy: fallback.
func TestEnvValsLoad_OverrideStrategy_TemplateContextUnchanged(t *testing.T) {
l := newLoader()
_, err := l.LoadEnvironmentValues(nil,
[]any{"testdata/mergestrategy/default.yaml", "testdata/mergestrategy/fallback.yaml.gotmpl"},
nil, "", MergeStrategyOverride)
if err == nil {
t.Fatal("expected template render error: under override, .Values.cluster.domain should not resolve to a prior file's value")
}
// The exact error wording is owned by the template renderer; we only
// assert that we got an error rather than a bogus successful render.
}
// Unknown strategy values produce a clear error that names both the bad
// value and the valid options.
func TestEnvValsLoad_InvalidStrategy_Errors(t *testing.T) {
l := newLoader()
_, err := l.LoadEnvironmentValues(nil,
[]any{"testdata/mergestrategy/default.yaml"},
nil, "prod", "bogus")
if err == nil {
t.Fatal("expected error for invalid mergeStrategy, got nil")
}
for _, want := range []string{"prod", "bogus", MergeStrategyOverride, MergeStrategyFallback} {
if !strings.Contains(err.Error(), want) {
t.Errorf("error message missing %q: %v", want, err)
}
}
}
+84 -14
View File
@@ -13,10 +13,12 @@ import (
"github.com/helmfile/chartify"
"helm.sh/helm/v4/pkg/storage/driver"
"github.com/helmfile/helmfile/pkg/filesystem"
"github.com/helmfile/helmfile/pkg/helmexec"
"github.com/helmfile/helmfile/pkg/kubedog"
"github.com/helmfile/helmfile/pkg/remote"
"github.com/helmfile/helmfile/pkg/resource"
"github.com/helmfile/helmfile/pkg/tmpl"
)
type Dependency struct {
@@ -115,7 +117,7 @@ func (st *HelmState) appendPostRenderFlags(flags []string, release *ReleaseSpec,
}
// append post-renderer-args flags to helm flags
func (st *HelmState) appendPostRenderArgsFlags(flags []string, release *ReleaseSpec, postRendererArgs []string) []string {
func (st *HelmState) appendPostRenderArgsFlags(flags []string, release *ReleaseSpec, postRendererArgs []string) ([]string, error) {
postRendererArgsFlags := []string{}
switch {
case len(release.PostRendererArgs) != 0:
@@ -123,32 +125,70 @@ func (st *HelmState) appendPostRenderArgsFlags(flags []string, release *ReleaseS
case len(postRendererArgs) != 0:
postRendererArgsFlags = postRendererArgs
case len(st.HelmDefaults.PostRendererArgs) != 0:
postRendererArgsFlags = st.HelmDefaults.PostRendererArgs
rendered, err := st.renderPostRendererArgs(release, st.HelmDefaults.PostRendererArgs)
if err != nil {
return nil, err
}
postRendererArgsFlags = rendered
}
for _, arg := range postRendererArgsFlags {
if arg != "" {
flags = append(flags, "--post-renderer-args", arg)
flags = append(flags, "--post-renderer-args="+arg)
}
}
return flags
return flags, nil
}
func (st *HelmState) renderPostRendererArgs(release *ReleaseSpec, args []string) ([]string, error) {
vals := st.RenderedValues
if vals == nil {
vals = make(map[string]any)
}
fs := st.fs
if fs == nil {
fs = filesystem.DefaultFileSystem()
}
tmplData := st.createReleaseTemplateData(release, vals)
renderer := tmpl.NewFileRenderer(fs, st.basePath, tmplData)
result := make([]string, 0, len(args))
for _, arg := range args {
rendered, err := renderer.RenderTemplateContentToString([]byte(arg))
if err != nil {
return nil, fmt.Errorf("failed rendering postRendererArg %q for release %q: %w", arg, release.Name, err)
}
result = append(result, rendered)
}
return result, nil
}
// append skip-schema-validation flags to helm flags
func (st *HelmState) appendSkipSchemaValidationFlags(flags []string, release *ReleaseSpec, skipSchemaValidation bool) []string {
switch {
// Check if SkipSchemaValidation is true in the release spec.
case release.SkipSchemaValidation != nil && *release.SkipSchemaValidation:
flags = append(flags, "--skip-schema-validation")
// Check if skipSchemaValidation argument is true.
case skipSchemaValidation:
flags = append(flags, "--skip-schema-validation")
// Check if SkipSchemaValidation is true in HelmDefaults.
case st.HelmDefaults.SkipSchemaValidation != nil && *st.HelmDefaults.SkipSchemaValidation:
if st.shouldSkipSchemaValidation(release, skipSchemaValidation) {
flags = append(flags, "--skip-schema-validation")
}
return flags
}
func (st *HelmState) shouldSkipSchemaValidation(release *ReleaseSpec, skipSchemaValidation bool) bool {
switch {
// Check if SkipSchemaValidation is true in the release spec.
case release.SkipSchemaValidation != nil && *release.SkipSchemaValidation:
return true
// Check if skipSchemaValidation argument is true.
case skipSchemaValidation:
return true
// Check if SkipSchemaValidation is true in HelmDefaults.
case st.HelmDefaults.SkipSchemaValidation != nil && *st.HelmDefaults.SkipSchemaValidation:
return true
default:
return false
}
}
// append suppress-output-line-regex flags to helm diff flags
func (st *HelmState) appendSuppressOutputLineRegexFlags(flags []string, release *ReleaseSpec, suppressOutputLineRegex []string) []string {
suppressOutputLineRegexFlags := []string{}
@@ -189,6 +229,32 @@ func (st *HelmState) shouldUseKubedog(release *ReleaseSpec, ops *SyncOpts) bool
return st.getTrackMode(release, ops) == string(kubedog.TrackModeKubedog)
}
func (st *HelmState) shouldFailOnTrackError(release *ReleaseSpec, ops *SyncOpts) bool {
if release.TrackFailOnError != nil {
return *release.TrackFailOnError
}
if ops != nil {
return ops.TrackFailOnError
}
return false
}
// trackReleaseIfEnabled performs kubedog tracking for a release if trackMode is "kubedog".
// It returns a ReleaseError if tracking fails and shouldFailOnTrackError is true.
// The caller is responsible for mutating affectedReleases when needed.
func (st *HelmState) trackReleaseIfEnabled(ctx context.Context, release *ReleaseSpec, helm helmexec.Interface, opts *SyncOpts) *ReleaseError {
if !st.shouldUseKubedog(release, opts) {
return nil
}
if trackErr := st.trackWithKubedog(ctx, release, helm, opts); trackErr != nil {
st.logger.Warnf("kubedog tracking failed for release %s: %v", release.Name, trackErr)
if st.shouldFailOnTrackError(release, opts) {
return newReleaseFailedError(release, trackErr)
}
}
return nil
}
func (st *HelmState) getTrackMode(release *ReleaseSpec, ops *SyncOpts) string {
trackMode := release.TrackMode
if trackMode == "" && ops != nil && ops.TrackMode != "" {
@@ -382,7 +448,8 @@ func (st *HelmState) PrepareChartify(helm helmexec.Interface, release *ReleaseSp
for _, d := range release.Dependencies {
chart := d.Chart
if st.fs.DirectoryExistsAt(chart) {
normalizedChart := normalizeChart(st.basePath, chart)
if st.fs.DirectoryExistsAt(normalizedChart) {
var err error
// Otherwise helm-dependency-up on the temporary chart generated by chartify ends up errors like:
@@ -393,6 +460,9 @@ func (st *HelmState) PrepareChartify(helm helmexec.Interface, release *ReleaseSp
if err != nil {
return nil, clean, err
}
} else if rewritten, ok := st.resolveOCIAdhocDepChart(d.Chart); ok {
st.logger.Debugf("ad-hoc dependency %q rewritten to %q (matched OCI repo entry)", d.Chart, rewritten)
chart = rewritten
}
c.Opts.AdhocChartDependencies = append(c.Opts.AdhocChartDependencies, chartify.ChartDependency{
+86
View File
@@ -0,0 +1,86 @@
package state
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestAppendSkipSchemaValidationFlagToChartifyTemplateArgs(t *testing.T) {
enable := true
tests := []struct {
name string
defaults HelmSpec
release *ReleaseSpec
fromCLI bool
templateArgs string
want string
}{
{
name: "adds flag from release setting",
release: &ReleaseSpec{
SkipSchemaValidation: &enable,
},
want: "--skip-schema-validation",
},
{
name: "adds flag from helm defaults",
defaults: HelmSpec{
SkipSchemaValidation: &enable,
},
release: &ReleaseSpec{},
want: "--skip-schema-validation",
},
{
name: "appends flag to existing args",
release: &ReleaseSpec{
SkipSchemaValidation: &enable,
},
templateArgs: "--kube-context default",
want: "--kube-context default --skip-schema-validation",
},
{
name: "does not duplicate existing flag",
release: &ReleaseSpec{
SkipSchemaValidation: &enable,
},
templateArgs: "--skip-schema-validation --kube-context default",
want: "--skip-schema-validation --kube-context default",
},
{
name: "does not treat similar flag values as existing flag",
release: &ReleaseSpec{
SkipSchemaValidation: &enable,
},
templateArgs: "--set name=foo--skip-schema-validation",
want: "--set name=foo--skip-schema-validation --skip-schema-validation",
},
{
name: "adds flag from cli setting",
release: &ReleaseSpec{},
fromCLI: true,
templateArgs: "--kube-context default",
want: "--kube-context default --skip-schema-validation",
},
{
name: "does not add flag when disabled",
release: &ReleaseSpec{},
templateArgs: "--kube-context default",
want: "--kube-context default",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
HelmDefaults: tt.defaults,
},
}
got := st.appendSkipSchemaValidationFlagToChartifyTemplateArgs(tt.templateArgs, tt.release, tt.fromCLI)
require.Equal(t, tt.want, got)
})
}
}
+138
View File
@@ -0,0 +1,138 @@
package state
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/helmfile/helmfile/pkg/filesystem"
)
// TestLocalDependencyChartPathNormalization tests that relative chart paths in
// release dependencies (like "../chart") are normalized to absolute paths
// relative to basePath before checking if the directory exists.
// This is a regression test for issue #2596.
//
// Background: When helmfile.d/ contains multiple release files and one release
// has a local chart dependency (chart: ../chart), the dependency chart path was
// passed to DirectoryExistsAt without normalization, causing it to be resolved
// relative to the CWD instead of basePath. This made helmfile fail to detect
// the local chart and instead try to resolve it as a remote repo, resulting in
// "failed reading adhoc dependencies: no helm list entry found for repository".
func TestLocalDependencyChartPathNormalization(t *testing.T) {
tempDir := t.TempDir()
chartDir := filepath.Join(tempDir, "chart")
require.NoError(t, os.MkdirAll(chartDir, 0755))
require.NoError(t, os.WriteFile(filepath.Join(chartDir, "Chart.yaml"), []byte(`
apiVersion: v2
name: test-chart
version: 0.1.0
`), 0644))
helmfileDir := filepath.Join(tempDir, "helmfile.d")
require.NoError(t, os.MkdirAll(helmfileDir, 0755))
tests := []struct {
name string
chartPath string
basePath string
expectLocal bool
}{
{
name: "relative path ../chart normalized from helmfile.d",
chartPath: "../chart",
basePath: helmfileDir,
expectLocal: true,
},
{
name: "absolute path works unchanged",
chartPath: chartDir,
basePath: helmfileDir,
expectLocal: true,
},
{
name: "non-existent relative path not detected as local",
chartPath: "../nonexistent",
basePath: helmfileDir,
expectLocal: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
normalizedChart := normalizeChart(tt.basePath, tt.chartPath)
fs := filesystem.DefaultFileSystem()
isLocal := fs.DirectoryExistsAt(normalizedChart)
assert.Equal(t, tt.expectLocal, isLocal,
"normalizeChart(%q, %q) = %q, DirectoryExistsAt = %v, want %v",
tt.basePath, tt.chartPath, normalizedChart, isLocal, tt.expectLocal)
})
}
}
// TestDependencyChartPathResolutionWithPrepareChartify verifies that the dependency
// chart path is normalized using basePath before calling DirectoryExistsAt,
// which is the core of the fix for issue #2596.
func TestDependencyChartPathResolutionWithPrepareChartify(t *testing.T) {
tempDir := t.TempDir()
chartDir := filepath.Join(tempDir, "chart")
require.NoError(t, os.MkdirAll(chartDir, 0755))
require.NoError(t, os.WriteFile(filepath.Join(chartDir, "Chart.yaml"), []byte(`
apiVersion: v2
name: test-chart
version: 0.1.0
`), 0644))
helmfileDir := filepath.Join(tempDir, "helmfile.d")
require.NoError(t, os.MkdirAll(helmfileDir, 0755))
fs := filesystem.DefaultFileSystem()
tests := []struct {
name string
depChartPath string
basePath string
expectDetected bool
}{
{
name: "relative ../chart from helmfile.d detected as local",
depChartPath: "../chart",
basePath: helmfileDir,
expectDetected: true,
},
{
name: "absolute path detected as local",
depChartPath: chartDir,
basePath: helmfileDir,
expectDetected: true,
},
{
name: "non-existent relative path not detected",
depChartPath: "../nonexistent",
basePath: helmfileDir,
expectDetected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
normalizedChart := normalizeChart(tt.basePath, tt.depChartPath)
isLocal := fs.DirectoryExistsAt(normalizedChart)
assert.Equal(t, tt.expectDetected, isLocal,
"normalizeChart(%q, %q) = %q, DirectoryExistsAt = %v, want %v",
tt.basePath, tt.depChartPath, normalizedChart, isLocal, tt.expectDetected)
if tt.expectDetected && !filepath.IsAbs(tt.depChartPath) {
absChart, err := filepath.Abs(filepath.Join(tt.basePath, tt.depChartPath))
require.NoError(t, err)
assert.Equal(t, absChart, normalizedChart,
"normalized path should match expected absolute path")
}
})
}
}
+66 -5
View File
@@ -47,6 +47,69 @@ func (l LabelFilter) Match(r ReleaseSpec) bool {
return true
}
// SelectorsAreCompatible checks whether any pair of selectors from two sets could
// potentially match the same release. It compares only positive labels (key=value):
// two selectors conflict if they require the same key to have different values.
// Returns true if at least one pair is compatible, false only if all pairs conflict.
// On parse error, returns (true, err): the conservative true ensures subhelmfiles
// are never incorrectly skipped due to malformed input, while the non-nil error
// allows callers to log or handle the parse failure if desired.
func SelectorsAreCompatible(selectorsA, selectorsB []string) (bool, error) {
if len(selectorsA) == 0 || len(selectorsB) == 0 {
return true, nil
}
filtersA, err := parseLabelFilters(selectorsA)
if err != nil {
return true, err
}
filtersB, err := parseLabelFilters(selectorsB)
if err != nil {
return true, err
}
for _, a := range filtersA {
for _, b := range filtersB {
if a.positiveLabelsCompatibleWith(b) {
return true, nil
}
}
}
return false, nil
}
func parseLabelFilters(selectors []string) ([]LabelFilter, error) {
filters := make([]LabelFilter, 0, len(selectors))
for _, s := range selectors {
f, err := ParseLabels(s)
if err != nil {
return nil, err
}
filters = append(filters, f)
}
return filters, nil
}
// positiveLabelsCompatibleWith returns true if the positive labels of two filters
// do not conflict (i.e., no shared key with a different value).
func (l LabelFilter) positiveLabelsCompatibleWith(other LabelFilter) bool {
for _, a := range l.positiveLabels {
for _, b := range other.positiveLabels {
if a[0] == b[0] && a[1] != b[1] {
return false
}
}
}
return true
}
var (
reLabelMismatch = regexp.MustCompile(`^[a-zA-Z0-9_\.\/\+-]+!=[a-zA-Z0-9_\.\/\+-]+$`)
reLabelMatch = regexp.MustCompile(`^[a-zA-Z0-9_\.\/\+-]+=[a-zA-Z0-9_\.\/\+-]+$`)
)
// ParseLabels takes a label in the form foo=bar,baz!=bat and returns a LabelFilter that will match the labels
func ParseLabels(l string) (LabelFilter, error) {
lf := LabelFilter{}
@@ -54,16 +117,14 @@ func ParseLabels(l string) (LabelFilter, error) {
lf.negativeLabels = [][]string{}
var err error
labels := strings.Split(l, ",")
reMissmatch := regexp.MustCompile(`^[a-zA-Z0-9_\.\/\+-]+!=[a-zA-Z0-9_\.\/\+-]+$`)
reMatch := regexp.MustCompile(`^[a-zA-Z0-9_\.\/\+-]+=[a-zA-Z0-9_\.\/\+-]+$`)
for _, label := range labels {
if match := reMissmatch.MatchString(label); match { // k!=v case
if match := reLabelMismatch.MatchString(label); match {
kv := strings.Split(label, "!=")
lf.negativeLabels = append(lf.negativeLabels, kv)
} else if match := reMatch.MatchString(label); match { // k=v case
} else if match := reLabelMatch.MatchString(label); match {
kv := strings.Split(label, "=")
lf.positiveLabels = append(lf.positiveLabels, kv)
} else { // malformed case
} else {
return lf, fmt.Errorf("malformed label: %s. Expected label in form k=v or k!=v", label)
}
}
+102
View File
@@ -67,3 +67,105 @@ func TestParseLabelsInvalidFormat(t *testing.T) {
expectedErrorMsg := "malformed label: invalid_label. Expected label in form k=v or k!=v"
assert.EqualError(t, err, expectedErrorMsg, "unexpected error message")
}
func TestSelectorsAreCompatible(t *testing.T) {
tests := []struct {
name string
selectorsA []string
selectorsB []string
compatible bool
wantErr bool
}{
{
name: "same key different value",
selectorsA: []string{"name=b"},
selectorsB: []string{"name=a"},
compatible: false,
},
{
name: "same key same value",
selectorsA: []string{"name=b"},
selectorsB: []string{"name=b"},
compatible: true,
},
{
name: "different keys no conflict",
selectorsA: []string{"name=b"},
selectorsB: []string{"env=prod"},
compatible: true,
},
{
name: "one compatible pair among multiple selectors",
selectorsA: []string{"name=b"},
selectorsB: []string{"name=a", "name=b"},
compatible: true,
},
{
name: "all pairs conflict",
selectorsA: []string{"name=b"},
selectorsB: []string{"name=a", "name=c"},
compatible: false,
},
{
name: "one compatible pair with same key same value",
selectorsA: []string{"name=b", "env=prod"},
selectorsB: []string{"name=b"},
compatible: true,
},
{
name: "empty selectorsA always compatible",
selectorsA: []string{},
selectorsB: []string{"name=a"},
compatible: true,
},
{
name: "empty selectorsB always compatible",
selectorsA: []string{"name=a"},
selectorsB: []string{},
compatible: true,
},
{
name: "negative labels not compared treated as compatible",
selectorsA: []string{"name!=a"},
selectorsB: []string{"name=a"},
compatible: true,
},
{
name: "compound selector with conflicting key",
selectorsA: []string{"name=a,env=prod"},
selectorsB: []string{"name=a,env=staging"},
compatible: false,
},
{
name: "compound selector with matching keys",
selectorsA: []string{"name=a,env=prod"},
selectorsB: []string{"name=a,env=prod"},
compatible: true,
},
{
name: "compound selector partial overlap different key",
selectorsA: []string{"name=a,env=prod"},
selectorsB: []string{"name=a"},
compatible: true,
},
{
name: "malformed selector returns conservative true with error",
selectorsA: []string{"name=b"},
selectorsB: []string{"invalid_label"},
compatible: true,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := SelectorsAreCompatible(tt.selectorsA, tt.selectorsB)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.compatible, got)
})
}
}
+303 -61
View File
@@ -4,7 +4,9 @@ import (
"bytes"
gocontext "context"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
@@ -27,6 +29,7 @@ import (
"github.com/helmfile/vals"
"github.com/tatsushid/go-prettytable"
"go.uber.org/zap"
helmchart "helm.sh/helm/v3/pkg/chart"
cliv3 "helm.sh/helm/v3/pkg/cli"
cliv4 "helm.sh/helm/v4/pkg/cli"
@@ -36,6 +39,7 @@ import (
"github.com/helmfile/helmfile/pkg/event"
"github.com/helmfile/helmfile/pkg/filesystem"
"github.com/helmfile/helmfile/pkg/helmexec"
"github.com/helmfile/helmfile/pkg/maputil"
"github.com/helmfile/helmfile/pkg/remote"
"github.com/helmfile/helmfile/pkg/tmpl"
"github.com/helmfile/helmfile/pkg/yaml"
@@ -49,6 +53,12 @@ const (
// Valid enum for updateStrategy values
UpdateStrategyReinstallIfForbidden = "reinstallIfForbidden"
// Valid values for environment mergeStrategy.
// MergeStrategyOverride (default) makes later values files override earlier ones.
// MergeStrategyFallback flips the precedence: earlier files win and later files only fill gaps.
MergeStrategyOverride = "override"
MergeStrategyFallback = "fallback"
)
// ReleaseSetSpec is release set spec
@@ -85,6 +95,11 @@ type ReleaseSetSpec struct {
Templates map[string]TemplateSpec `yaml:"templates"`
// DefaultInherit is a list of template names that all releases inherit by default.
// Each release will automatically inherit these templates unless it already explicitly
// inherits from the same template.
DefaultInherit DefaultInherits `yaml:"defaultInherit,omitempty"`
Env environment.Environment `yaml:"-"`
// If set to "Error", return an error when a subhelmfile points to a
@@ -156,7 +171,7 @@ type SubHelmfileSpec struct {
// SubhelmfileEnvironmentSpec is the environment spec for a subhelmfile
type SubhelmfileEnvironmentSpec struct {
OverrideValues []any `yaml:"values,omitempty"`
OverrideValuesAreCLI bool `yaml:"-"`
OverrideCLISetValues []any `yaml:"-"` // CLI --state-values-set values only, merged element-by-element
}
// HelmSpec to defines helmDefault values
@@ -478,6 +493,8 @@ type ReleaseSpec struct {
KubedogQPS *float32 `yaml:"kubedogQPS,omitempty"`
// KubedogBurst specifies the burst for kubedog kubernetes client
KubedogBurst *int `yaml:"kubedogBurst,omitempty"`
// TrackFailOnError controls whether kubedog tracking failures cause a non-zero exit code
TrackFailOnError *bool `yaml:"trackFailOnError,omitempty"`
}
// TrackResourceSpec specifies a resource to track
@@ -502,6 +519,45 @@ func (r *Inherits) UnmarshalYAML(unmarshal func(any) error) error {
return nil
}
type DefaultInherits []string
func (r *DefaultInherits) UnmarshalYAML(unmarshal func(any) error) error {
var list []string
if err := unmarshal(&list); err == nil {
*r = normalizeDefaultInherits(list)
return nil
}
var single string
if err := unmarshal(&single); err != nil {
return err
}
*r = normalizeDefaultInherits([]string{single})
return nil
}
// normalizeDefaultInherits trims names, drops empty entries, and returns nil for an empty result.
func normalizeDefaultInherits(in []string) []string {
if len(in) == 0 {
return nil
}
out := make([]string, 0, len(in))
for _, name := range in {
name = strings.TrimSpace(name)
if name == "" {
continue
}
out = append(out, name)
}
if len(out) == 0 {
return nil
}
return out
}
// ChartPathOrName returns ChartPath if it is non-empty, and returns Chart otherwise.
// This is useful to redirect helm commands like `helm template`, `helm dependency update`, `helm diff`, and `helm upgrade --install` to
// our modified version of the chart, in case the user configured Helmfile to do modify the chart before being passed to Helm.
@@ -920,6 +976,7 @@ type SyncOpts struct {
TrackMode string
TrackTimeout int
TrackLogs bool
TrackFailOnError bool
Description string
}
@@ -1146,10 +1203,8 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme
}
} else if release.UpdateStrategy == UpdateStrategyReinstallIfForbidden {
relErr = st.performSyncOrReinstallOfRelease(affectedReleases, helm, context, release, chart, m, flags...)
if relErr == nil && st.shouldUseKubedog(release, opts) {
if trackErr := st.trackWithKubedog(gocontext.Background(), release, helm, opts); trackErr != nil {
st.logger.Warnf("kubedog tracking failed for release %s: %v", release.Name, trackErr)
}
if relErr == nil {
relErr = st.trackReleaseIfEnabled(gocontext.Background(), release, helm, opts)
}
} else {
if err := helm.SyncRelease(context, release.Name, chart, release.Namespace, flags...); err != nil {
@@ -1168,10 +1223,11 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme
release.installedVersion = installedVersion
}
if st.shouldUseKubedog(release, opts) {
if trackErr := st.trackWithKubedog(gocontext.Background(), release, helm, opts); trackErr != nil {
st.logger.Warnf("kubedog tracking failed for release %s: %v", release.Name, trackErr)
}
if trackErr := st.trackReleaseIfEnabled(gocontext.Background(), release, helm, opts); trackErr != nil {
m.Lock()
affectedReleases.Failed = append(affectedReleases.Failed, release)
m.Unlock()
relErr = trackErr
}
}
}
@@ -1349,6 +1405,8 @@ type ChartPrepareOptions struct {
SkipRefresh bool
SkipResolve bool
SkipCleanup bool
// SkipSchemaValidation configures chartify to pass --skip-schema-validation to helm-template run by it.
SkipSchemaValidation bool
// Validate configures chartify to pass --validate to helm-template run by it.
// It's required when one of your chart relies on Capabilities.APIVersions in a template
Validate bool
@@ -1397,6 +1455,28 @@ func (st *HelmState) GetRepositoryAndNameFromChartName(chartName string) (*Repos
return nil, chartName
}
// resolveOCIAdhocDepChart rewrites a release `dependencies[].chart` value that
// uses the named-repo prefix form ("repoName/chartName") into a full oci:// URL
// when the prefix matches a `repositories:` entry with `oci: true`. It returns
// (rewritten, true) on a hit and ("", false) otherwise.
//
// This avoids the chartify path that does `helm repo list` to look up the
// repository URL: that lookup never finds OCI repos because helm 3+ does not
// register OCI registries as named repos (it uses `helm registry login`
// instead). By the time chartify sees an `oci://` URL it already takes the
// correct branch, so rewriting here is enough to make the named-repo form
// behave the same as the explicit URL form.
func (st *HelmState) resolveOCIAdhocDepChart(chart string) (string, bool) {
if strings.HasPrefix(chart, "oci://") {
return "", false
}
repo, name := st.GetRepositoryAndNameFromChartName(chart)
if repo == nil || !repo.OCI {
return "", false
}
return "oci://" + strings.TrimSuffix(repo.URL, "/") + "/" + name, true
}
var rwMutexMap sync.Map
// getNamedRWMutex retrieves or creates a sync.RWMutex for a given name.
@@ -1416,42 +1496,28 @@ type PrepareChartKey struct {
Namespace, Name, KubeContext string
}
// PrepareCharts creates temporary directories of charts.
//
// Each resulting "chart" can be one of the followings:
//
// (1) local chart
// (2) temporary local chart generated from kustomization or manifests
// (3) remote chart
//
// When running `helmfile template` on helm v2, or `helmfile lint` on both helm v2 and v3,
// PrepareCharts will download and untar charts for linting and templating.
//
// rewriteChartDependencies rewrites relative file:// dependencies in Chart.yaml to absolute paths
// to ensure they can be resolved from chartify's temporary directory
func (st *HelmState) rewriteChartDependencies(chartPath string) (func(), error) {
// to ensure they can be resolved from chartify's temporary directory.
// Instead of modifying the original chart in-place (which causes race conditions when multiple
// releases reference the same local chart), it creates a temporary copy only when a rewrite is
// needed and rewrites that copy. When a temp copy is created, it reflects the current chart
// contents so prepare hooks or other steps that mutate the local chart directory are honored.
// The returned cleanup function removes the temporary directory when one was created and is
// otherwise a no-op.
func (st *HelmState) rewriteChartDependencies(chartPath string) (string, func(), error) {
chartYamlPath := filepath.Join(chartPath, "Chart.yaml")
// Check if Chart.yaml exists
if _, err := os.Stat(chartYamlPath); os.IsNotExist(err) {
return func() {}, nil
if _, err := st.fs.Stat(chartYamlPath); os.IsNotExist(err) {
return chartPath, func() {}, nil
} else if err != nil {
return chartPath, func() {}, err
}
// Read Chart.yaml
data, err := os.ReadFile(chartYamlPath)
data, err := st.fs.ReadFile(chartYamlPath)
if err != nil {
return func() {}, err
return chartPath, func() {}, err
}
originalContent := data
cleanup := func() {
// Restore original Chart.yaml
if err := os.WriteFile(chartYamlPath, originalContent, 0644); err != nil {
st.logger.Warnf("Failed to restore original Chart.yaml at %s: %v", chartYamlPath, err)
}
}
// Parse Chart.yaml
type ChartDependency struct {
Name string `yaml:"name"`
Repository string `yaml:"repository"`
@@ -1464,23 +1530,20 @@ func (st *HelmState) rewriteChartDependencies(chartPath string) (func(), error)
var chartMeta ChartMeta
if err := yaml.Unmarshal(data, &chartMeta); err != nil {
return cleanup, err
return chartPath, func() {}, err
}
// Rewrite relative file:// dependencies to absolute paths
modified := false
for i := range chartMeta.Dependencies {
dep := &chartMeta.Dependencies[i]
if strings.HasPrefix(dep.Repository, "file://") {
relPath := strings.TrimPrefix(dep.Repository, "file://")
// Check if it's a relative path
if !filepath.IsAbs(relPath) {
// Convert to absolute path relative to the chart directory
absPath := filepath.Join(chartPath, relPath)
absPath, err = filepath.Abs(absPath)
if err != nil {
return cleanup, fmt.Errorf("failed to resolve absolute path for dependency %s: %w", dep.Name, err)
return chartPath, func() {}, fmt.Errorf("failed to resolve absolute path for dependency %s: %w", dep.Name, err)
}
st.logger.Debugf("Rewriting Chart dependency %s from %s to file://%s", dep.Name, dep.Repository, absPath)
@@ -1490,21 +1553,159 @@ func (st *HelmState) rewriteChartDependencies(chartPath string) (func(), error)
}
}
// Write back if modified
if modified {
updatedData, err := yaml.Marshal(&chartMeta)
if err != nil {
return cleanup, fmt.Errorf("failed to marshal Chart.yaml: %w", err)
}
if err := os.WriteFile(chartYamlPath, updatedData, 0644); err != nil {
return cleanup, fmt.Errorf("failed to write Chart.yaml: %w", err)
}
st.logger.Debugf("Rewrote Chart.yaml with absolute dependency paths at %s", chartYamlPath)
if !modified {
return chartPath, func() {}, nil
}
return cleanup, nil
updatedData, err := yaml.Marshal(&chartMeta)
if err != nil {
return chartPath, func() {}, fmt.Errorf("failed to marshal Chart.yaml: %w", err)
}
tempDir, err := st.fs.MkdirTemp("", "chart-deps-rewrite-*")
if err != nil {
return chartPath, func() {}, fmt.Errorf("failed to create temp directory for chart rewrite: %w", err)
}
if err := st.fs.CopyDir(chartPath, tempDir); err != nil {
if removeErr := st.fs.RemoveAll(tempDir); removeErr != nil {
st.logger.Warnf("Failed to remove temp chart directory %s: %v", tempDir, removeErr)
}
return chartPath, func() {}, fmt.Errorf("failed to copy chart to temp directory: %w", err)
}
tempChartYamlPath := filepath.Join(tempDir, "Chart.yaml")
if err := st.fs.WriteFile(tempChartYamlPath, updatedData, 0644); err != nil {
if removeErr := st.fs.RemoveAll(tempDir); removeErr != nil {
st.logger.Warnf("Failed to remove temp chart directory %s: %v", tempDir, removeErr)
}
return chartPath, func() {}, fmt.Errorf("failed to write Chart.yaml: %w", err)
}
st.logger.Debugf("Rewrote Chart.yaml with absolute dependency paths at %s", tempChartYamlPath)
// Rewriting Chart.yaml invalidates Chart.lock's digest, since helm computes the
// digest over the JSON-marshaled dependencies block. If the lock isn't refreshed,
// downstream `helm dependency build` errors with "lock file is out of sync with
// the dependencies file" and falls back to `dependency update`, which re-resolves
// version constraints (e.g. `version: "*"`) against the chart repo and silently
// pulls newer dependency versions. The version pins in the lock are still the
// intended truth — only the rewritten file:// repository URL changed. Mirror the
// rewrite into the lock and recompute the digest so `dep build` accepts it.
tempChartLockPath := filepath.Join(tempDir, "Chart.lock")
lockData, lockErr := st.fs.ReadFile(tempChartLockPath)
if lockErr != nil && !os.IsNotExist(lockErr) {
st.logger.Warnf("Failed to read Chart.lock at %s: %v", tempChartLockPath, lockErr)
}
if lockErr == nil {
var lock struct {
Dependencies []*helmchart.Dependency `yaml:"dependencies,omitempty"`
Digest string `yaml:"digest,omitempty"`
Generated string `yaml:"generated,omitempty"`
}
if err := yaml.Unmarshal(lockData, &lock); err != nil {
st.logger.Warnf("Failed to parse Chart.lock at %s: %v", tempChartLockPath, err)
} else {
// Build the request slice (rewritten Chart.yaml dependencies) using helm's
// own chart.Dependency type so the JSON used for hashing matches helm's
// exactly. All supported fields must be mapped, not just name/repository/
// version, because helm's digest algorithm hashes the full Dependency struct.
req := make([]*helmchart.Dependency, 0, len(chartMeta.Dependencies))
for _, d := range chartMeta.Dependencies {
dep := &helmchart.Dependency{
Name: d.Name,
Repository: d.Repository,
}
if v, ok := d.Data["version"].(string); ok {
dep.Version = v
}
if v, ok := d.Data["condition"].(string); ok {
dep.Condition = v
}
if v, ok := d.Data["alias"].(string); ok {
dep.Alias = v
}
if v, ok := d.Data["enabled"].(bool); ok {
dep.Enabled = v
}
if v, ok := d.Data["tags"].([]interface{}); ok {
tags := make([]string, 0, len(v))
for _, t := range v {
if s, ok := t.(string); ok {
tags = append(tags, s)
}
}
dep.Tags = tags
}
if v, ok := d.Data["import-values"].([]interface{}); ok {
normalized, err := maputil.RecursivelyStringifyMapKey(v)
if err != nil {
st.logger.Warnf("Failed to normalize import-values for dependency %s: %v", d.Name, err)
} else {
dep.ImportValues = normalized.([]interface{})
}
}
req = append(req, dep)
}
// Mirror the rewritten file:// repository URLs onto matching lock entries.
// Without this, `helm dependency build` would resolve the lock's relative
// file:// paths against the (moved) chart directory and fail with
// "directory ... not found". Versions in the lock are left untouched.
// Match on Name + Alias to handle charts with duplicate dependency names
// distinguished by alias.
for _, ld := range lock.Dependencies {
if !strings.HasPrefix(ld.Repository, "file://") {
continue
}
for _, rd := range req {
if rd.Name == ld.Name && rd.Alias == ld.Alias && strings.HasPrefix(rd.Repository, "file://") {
ld.Repository = rd.Repository
break
}
}
}
// Normalize lock.Dependencies ImportValues to avoid json.Marshal failures
// when go-yaml v2 decodes nested maps as map[interface{}]interface{}.
for _, ld := range lock.Dependencies {
if ld.ImportValues != nil {
normalized, err := maputil.RecursivelyStringifyMapKey(ld.ImportValues)
if err != nil {
st.logger.Warnf("Failed to normalize import-values in Chart.lock for dependency %s: %v", ld.Name, err)
} else {
ld.ImportValues = normalized.([]interface{})
}
}
}
// Replicates helm's resolver.HashReq:
// json.Marshal([2][]*chart.Dependency{req, lock}) → sha256 hex.
// resolver.HashReq lives in helm.sh/helm/v3/internal/resolver, so we
// inline the (small, stable) algorithm rather than importing it.
if payload, err := json.Marshal([2][]*helmchart.Dependency{req, lock.Dependencies}); err != nil {
st.logger.Warnf("Failed to marshal deps for Chart.lock digest at %s: %v", tempChartLockPath, err)
} else {
sum := sha256.Sum256(payload)
lock.Digest = "sha256:" + hex.EncodeToString(sum[:])
if updated, err := yaml.Marshal(&lock); err != nil {
st.logger.Warnf("Failed to marshal Chart.lock at %s: %v", tempChartLockPath, err)
} else if err := st.fs.WriteFile(tempChartLockPath, updated, 0644); err != nil {
st.logger.Warnf("Failed to write Chart.lock at %s: %v", tempChartLockPath, err)
} else {
st.logger.Debugf("Refreshed Chart.lock digest at %s after Chart.yaml rewrite", tempChartLockPath)
}
}
}
}
cleanup := func() {
if removeErr := st.fs.RemoveAll(tempDir); removeErr != nil {
st.logger.Warnf("Failed to remove temp chart directory %s: %v", tempDir, removeErr)
}
}
return tempDir, cleanup, nil
}
// Otherwise, if a chart is not a helm chart, it will call "chartify" to turn it into a chart.
@@ -1516,11 +1717,12 @@ func (st *HelmState) processChartification(chartification *Chartify, release *Re
// This prevents errors like "Error: directory /tmp/chartify.../argocd-application not found"
// when Chart.yaml contains dependencies like "file://../argocd-application"
if st.fs.DirectoryExistsAt(chartPath) {
restoreChart, err := st.rewriteChartDependencies(chartPath)
rewrittenPath, cleanupTempChart, err := st.rewriteChartDependencies(chartPath)
if err != nil {
return "", false, fmt.Errorf("failed to rewrite chart dependencies: %w", err)
}
defer restoreChart()
chartPath = rewrittenPath
defer cleanupTempChart()
}
c := chartify.New(
@@ -1624,6 +1826,12 @@ func (st *HelmState) processChartification(chartification *Chartify, release *Re
}
}
chartifyOpts.TemplateArgs = st.appendSkipSchemaValidationFlagToChartifyTemplateArgs(
chartifyOpts.TemplateArgs,
release,
opts.SkipSchemaValidation,
)
out, err := c.Chartify(release.Name, chartPath, chartify.WithChartifyOpts(chartifyOpts))
if err != nil {
return "", false, err
@@ -1636,6 +1844,30 @@ func (st *HelmState) processChartification(chartification *Chartify, release *Re
return chartPath, buildDeps, nil
}
func (st *HelmState) appendSkipSchemaValidationFlagToChartifyTemplateArgs(templateArgs string, release *ReleaseSpec, skipSchemaValidation bool) string {
if !st.shouldSkipSchemaValidation(release, skipSchemaValidation) || hasTemplateArg(templateArgs, "--skip-schema-validation") {
return templateArgs
}
return appendTemplateArg(templateArgs, "--skip-schema-validation")
}
func hasTemplateArg(templateArgs, arg string) bool {
for _, token := range strings.Fields(templateArgs) {
if token == arg || strings.HasPrefix(token, arg+"=") {
return true
}
}
return false
}
func appendTemplateArg(templateArgs, arg string) string {
if templateArgs == "" {
return arg
}
return templateArgs + " " + arg
}
// processLocalChart handles local chart processing
func (st *HelmState) processLocalChart(normalizedChart, dir string, release *ReleaseSpec, helmfileCommand string, opts ChartPrepareOptions, isLocal bool) (string, error) {
chartPath := normalizedChart
@@ -3605,7 +3837,10 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp
if opt != nil {
postRendererArgs = opt.PostRendererArgs
}
flags = st.appendPostRenderArgsFlags(flags, release, postRendererArgs)
flags, err = st.appendPostRenderArgsFlags(flags, release, postRendererArgs)
if err != nil {
return nil, nil, err
}
skipSchemaValidation := false
if opt != nil {
@@ -3633,6 +3868,7 @@ func (st *HelmState) flagsForTemplate(helm helmexec.Interface, release *ReleaseS
var flags []string
flags = st.appendChartVersionFlags(flags, release)
flags = st.appendHelmXFlags(flags, release)
flags = st.appendEnableDNSFlags(flags, release)
var postRendererArgs []string
var showOnly []string
@@ -3647,7 +3883,10 @@ func (st *HelmState) flagsForTemplate(helm helmexec.Interface, release *ReleaseS
skipSchemaValidation = opt.SkipSchemaValidation
}
flags = st.appendPostRenderFlags(flags, release, postRenderer, helm)
flags = st.appendPostRenderArgsFlags(flags, release, postRendererArgs)
flags, err := st.appendPostRenderArgsFlags(flags, release, postRendererArgs)
if err != nil {
return nil, nil, err
}
flags = st.appendApiVersionsFlags(flags, release, kubeVersion)
flags = st.appendChartDownloadFlags(flags, release)
flags = st.appendShowOnlyFlags(flags, showOnly)
@@ -3770,7 +4009,11 @@ func (st *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec,
if opt != nil {
postRendererArgs = opt.PostRendererArgs
}
flags = st.appendPostRenderArgsFlags(flags, release, postRendererArgs)
var err error
flags, err = st.appendPostRenderArgsFlags(flags, release, postRendererArgs)
if err != nil {
return nil, nil, err
}
skipSchemaValidation := false
if opt != nil {
@@ -3800,7 +4043,6 @@ func (st *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec,
takeOwnership = opt.TakeOwnership
}
var err error
flags, err = st.appendTakeOwnershipFlagsForDiff(flags, release, takeOwnership, pluginsDir)
if err != nil {
return nil, nil, err
+37 -1
View File
@@ -85,7 +85,10 @@ func (st *HelmState) ExecuteTemplates() (*HelmState, error) {
vals := st.Values()
for i, rt := range st.Releases {
release, err := st.releaseWithInheritedTemplate(&rt, nil)
rtWithDefaults := rt
rtWithDefaults.Inherit = st.applyDefaultInherit(rt.Inherit)
release, err := st.releaseWithInheritedTemplate(&rtWithDefaults, nil)
if err != nil {
var cyclicInheritanceErr CyclicReleaseTemplateInheritanceError
if errors.As(err, &cyclicInheritanceErr) {
@@ -224,3 +227,36 @@ func (st *HelmState) releaseWithInheritedTemplate(r *ReleaseSpec, inheritancePat
return &merged, nil
}
// applyDefaultInherit prepends default inherit templates to the release's inherit list.
// Templates that are already explicitly referenced by the release are not duplicated.
func (st *HelmState) applyDefaultInherit(releaseInherit Inherits) Inherits {
if len(st.DefaultInherit) == 0 {
return releaseInherit
}
// Build the deduplication set and filter out blank entries in one pass.
existing := make(map[string]bool, len(releaseInherit))
filtered := make(Inherits, 0, len(releaseInherit))
for _, inh := range releaseInherit {
if name := strings.TrimSpace(inh.Template); name != "" {
existing[name] = true
filtered = append(filtered, inh)
}
}
result := make(Inherits, 0, len(st.DefaultInherit)+len(filtered))
for _, name := range st.DefaultInherit {
name = strings.TrimSpace(name)
if name == "" {
continue
}
if !existing[name] {
result = append(result, Inherit{Template: name})
existing[name] = true
}
}
result = append(result, filtered...)
return result
}
+210
View File
@@ -7,9 +7,12 @@ import (
"testing"
"github.com/go-test/deep"
"go.uber.org/zap"
"github.com/helmfile/helmfile/pkg/environment"
"github.com/helmfile/helmfile/pkg/filesystem"
"github.com/helmfile/helmfile/pkg/runtime"
"github.com/helmfile/helmfile/pkg/yaml"
)
func boolPtrToString(ptr *bool) string {
@@ -294,3 +297,210 @@ func TestHelmState_recursiveRefsTemplates(t *testing.T) {
})
}
}
func TestApplyDefaultInherit(t *testing.T) {
tests := []struct {
name string
defaultInherit DefaultInherits
releaseInherit Inherits
want Inherits
}{
{
name: "no default inherit",
defaultInherit: nil,
releaseInherit: Inherits{{Template: "foo"}},
want: Inherits{{Template: "foo"}},
},
{
name: "default inherit prepended",
defaultInherit: DefaultInherits{"default"},
releaseInherit: Inherits{{Template: "foo"}},
want: Inherits{{Template: "default"}, {Template: "foo"}},
},
{
name: "default inherit already in release inherit is not duplicated",
defaultInherit: DefaultInherits{"default"},
releaseInherit: Inherits{{Template: "default"}, {Template: "foo"}},
want: Inherits{{Template: "default"}, {Template: "foo"}},
},
{
name: "multiple default inherits",
defaultInherit: DefaultInherits{"a", "b"},
releaseInherit: Inherits{{Template: "c"}},
want: Inherits{{Template: "a"}, {Template: "b"}, {Template: "c"}},
},
{
name: "release inherit empty with defaults",
defaultInherit: DefaultInherits{"default"},
releaseInherit: nil,
want: Inherits{{Template: "default"}},
},
{
name: "default inherit deduplicates and skips empty values",
defaultInherit: DefaultInherits{"default", " ", "default", "ops"},
releaseInherit: Inherits{{Template: "foo"}},
want: Inherits{{Template: "default"}, {Template: "ops"}, {Template: "foo"}},
},
{
// Whitespace-only template names in releaseInherit are used verbatim for dedup
// (trimmed for map lookup), so the user's explicit entry is preserved in the output
// and the default is not prepended again.
name: "release inherit with whitespace template is deduplicated correctly",
defaultInherit: DefaultInherits{"default"},
releaseInherit: Inherits{{Template: " default "}, {Template: "foo"}},
want: Inherits{{Template: " default "}, {Template: "foo"}},
},
{
name: "release inherit with blank template is skipped",
defaultInherit: DefaultInherits{"default"},
releaseInherit: Inherits{{Template: ""}, {Template: "foo"}},
want: Inherits{{Template: "default"}, {Template: "foo"}},
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
st := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
DefaultInherit: tt.defaultInherit,
},
}
got := st.applyDefaultInherit(tt.releaseInherit)
if len(got) != len(tt.want) {
t.Fatalf("expected %d inherits, got %d", len(tt.want), len(got))
}
for j := range got {
if got[j].Template != tt.want[j].Template {
t.Errorf("inherit[%d]: expected template %q, got %q", j, tt.want[j].Template, got[j].Template)
}
if len(got[j].Except) != len(tt.want[j].Except) {
t.Errorf("inherit[%d]: expected %d except, got %d", j, len(tt.want[j].Except), len(got[j].Except))
}
}
})
}
}
func TestHelmState_executeTemplatesWithDefaultTemplates(t *testing.T) {
logger := zap.NewNop().Sugar()
state := &HelmState{
logger: logger,
fs: &filesystem.FileSystem{
Glob: func(s string) ([]string, error) { return nil, nil },
},
basePath: ".",
ReleaseSetSpec: ReleaseSetSpec{
HelmDefaults: HelmSpec{
KubeContext: "test_context",
},
Env: environment.Environment{Name: "test_env"},
Templates: map[string]TemplateSpec{
"default": {
ReleaseSpec: ReleaseSpec{
Namespace: "default-ns",
Labels: map[string]string{"managed": "true"},
},
},
},
DefaultInherit: DefaultInherits{"default"},
Releases: []ReleaseSpec{
{
Name: "app1",
Chart: "test-chart",
},
{
Name: "app2",
Chart: "test-chart-2",
Inherit: Inherits{
{Template: "default", Except: []string{"labels"}},
},
},
},
},
RenderedValues: map[string]any{},
}
r, err := state.ExecuteTemplates()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
app1 := r.Releases[0]
if app1.Namespace != "default-ns" {
t.Errorf("app1: expected namespace %q, got %q", "default-ns", app1.Namespace)
}
if app1.Labels["managed"] != "true" {
t.Errorf("app1: expected label managed=true, got %v", app1.Labels)
}
app2 := r.Releases[1]
if app2.Namespace != "default-ns" {
t.Errorf("app2: expected namespace %q, got %q", "default-ns", app2.Namespace)
}
if _, ok := app2.Labels["managed"]; ok {
t.Errorf("app2: expected labels to be excluded, but got %v", app2.Labels)
}
}
func TestDefaultInherits_UnmarshalYAML(t *testing.T) {
tests := []struct {
name string
input string
want DefaultInherits
}{
{
name: "single string",
input: `default`,
want: DefaultInherits{"default"},
},
{
name: "list of strings",
input: `["a", "b"]`,
want: DefaultInherits{"a", "b"},
},
{
name: "null value",
input: `null`,
want: nil,
},
{
name: "empty string value",
input: `""`,
want: nil,
},
{
name: "list trims and drops empty names",
input: `[" a ", "", " ", "b"]`,
want: DefaultInherits{"a", "b"},
},
}
for _, enableGoYamlV3 := range []bool{true, false} {
t.Run(fmt.Sprintf("GoYamlV3=%t", enableGoYamlV3), func(t *testing.T) {
prev := runtime.GoYamlV3
runtime.GoYamlV3 = enableGoYamlV3
defer func() {
runtime.GoYamlV3 = prev
}()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got DefaultInherits
err := yaml.Unmarshal([]byte(tt.input), &got)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(got) != len(tt.want) {
t.Fatalf("expected %d items, got %d", len(tt.want), len(got))
}
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("item[%d]: expected %q, got %q", i, tt.want[i], got[i])
}
}
})
}
})
}
}
+396
View File
@@ -908,6 +908,191 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-flags-use-helmdefault",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRendererArgs: []string{"--arg1", "--arg2"},
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
},
want: []string{
"--version", "0.1",
"--post-renderer-args=--arg1",
"--post-renderer-args=--arg2",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-flags-use-release",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRendererArgs: []string{"--release-arg"},
},
want: []string{
"--version", "0.1",
"--post-renderer-args=--release-arg",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-flags-use-release-prior-helmdefault",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRendererArgs: []string{"--default-arg"},
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRendererArgs: []string{"--release-arg"},
},
want: []string{
"--version", "0.1",
"--post-renderer-args=--release-arg",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-cli-overrides-helmdefault",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRendererArgs: []string{"--default-arg"},
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
},
syncOpts: &SyncOpts{
PostRendererArgs: []string{"--cli-arg"},
},
want: []string{
"--version", "0.1",
"--post-renderer-args=--cli-arg",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-release-overrides-cli",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRendererArgs: []string{"--release-arg"},
},
syncOpts: &SyncOpts{
PostRendererArgs: []string{"--cli-arg"},
},
want: []string{
"--version", "0.1",
"--post-renderer-args=--release-arg",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-short-flag-value",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRendererArgs: []string{"-v"},
},
want: []string{
"--version", "0.1",
"--post-renderer-args=-v",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-helmdefault-templated-with-release-name",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRendererArgs: []string{"{{ .Release.Name }}", "--chart={{ .Release.Chart }}"},
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "my-release",
Namespace: "test-namespace",
CreateNamespace: &disable,
},
want: []string{
"--version", "0.1",
"--post-renderer-args=my-release",
"--post-renderer-args=--chart=test/chart",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-helmdefault-templated-with-namespace",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRendererArgs: []string{"{{ .Release.Namespace }}/{{ .Release.Name }}"},
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "my-release",
Namespace: "test-namespace",
CreateNamespace: &disable,
},
want: []string{
"--version", "0.1",
"--post-renderer-args=test-namespace/my-release",
"--namespace", "test-namespace",
},
},
{
name: "description-from-release",
defaults: HelmSpec{
@@ -1204,6 +1389,146 @@ func TestHelmState_flagsForTemplate(t *testing.T) {
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-flags-use-helmdefault",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRendererArgs: []string{"--arg1", "--arg2"},
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
},
want: []string{
"--version", "0.1",
"--post-renderer-args=--arg1",
"--post-renderer-args=--arg2",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-flags-use-release",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRendererArgs: []string{"--release-arg"},
},
want: []string{
"--version", "0.1",
"--post-renderer-args=--release-arg",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-flags-use-release-prior-helmdefault",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRendererArgs: []string{"--default-arg"},
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRendererArgs: []string{"--release-arg"},
},
want: []string{
"--version", "0.1",
"--post-renderer-args=--release-arg",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-cli-overrides-helmdefault",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRendererArgs: []string{"--default-arg"},
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
},
templateOpts: TemplateOpts{
PostRendererArgs: []string{"--cli-arg"},
},
want: []string{
"--version", "0.1",
"--post-renderer-args=--cli-arg",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-release-overrides-cli",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRendererArgs: []string{"--release-arg"},
},
templateOpts: TemplateOpts{
PostRendererArgs: []string{"--cli-arg"},
},
want: []string{
"--version", "0.1",
"--post-renderer-args=--release-arg",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-short-flag-value",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRendererArgs: []string{"-v"},
},
want: []string{
"--version", "0.1",
"--post-renderer-args=-v",
"--namespace", "test-namespace",
},
},
{
name: "kube-version-flag-should-be-used",
defaults: HelmSpec{
@@ -5759,3 +6084,74 @@ func TestHelmState_getKubeContext(t *testing.T) {
})
}
}
// resolveOCIAdhocDepChart should rewrite a release `dependencies[].chart` value
// that uses the named-repo prefix form into a full oci:// URL whenever the
// matching `repositories:` entry has `oci: true`. All other inputs must pass
// through unchanged so we never disturb existing behavior.
func TestResolveOCIAdhocDepChart(t *testing.T) {
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{
{Name: "ociregistry", URL: "registry.example.com:5000/charts", OCI: true},
{Name: "ociregistry-trailing", URL: "registry.example.com:5000/charts/", OCI: true},
{Name: "stable", URL: "https://charts.helm.sh/stable"},
},
},
}
tests := []struct {
name string
chart string
wantOK bool
wantChart string
}{
{
name: "named OCI repo prefix is rewritten to oci:// URL",
chart: "ociregistry/redis",
wantOK: true,
wantChart: "oci://registry.example.com:5000/charts/redis",
},
{
name: "trailing slash on repo URL does not produce a double slash",
chart: "ociregistry-trailing/redis",
wantOK: true,
wantChart: "oci://registry.example.com:5000/charts/redis",
},
{
name: "non-OCI repo prefix is left alone for chartify's helm-repo-list path",
chart: "stable/nginx",
wantOK: false,
},
{
name: "explicit oci:// URL is left alone (already in chartify's OCI branch)",
chart: "oci://registry.example.com:5000/charts/redis",
wantOK: false,
},
{
name: "unknown repo prefix is left alone",
chart: "unknownrepo/something",
wantOK: false,
},
{
name: "single-segment chart (no slash) is left alone",
chart: "localchart",
wantOK: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := state.resolveOCIAdhocDepChart(tt.chart)
if ok != tt.wantOK {
t.Errorf("ok: want %v, got %v", tt.wantOK, ok)
}
if tt.wantOK && got != tt.wantChart {
t.Errorf("rewritten chart: want %q, got %q", tt.wantChart, got)
}
if !tt.wantOK && got != "" {
t.Errorf("expected empty rewrite when ok=false, got %q", got)
}
})
}
}
+6 -6
View File
@@ -38,39 +38,39 @@ func TestGenerateID(t *testing.T) {
run(testcase{
subject: "baseline",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
want: "foo-values-6ccb848dcd",
want: "foo-values-7f6f8d74dd",
})
run(testcase{
subject: "different bytes content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: []byte(`{"k":"v"}`),
want: "foo-values-5bcbbc4c85",
want: "foo-values-5fc74c864c",
})
run(testcase{
subject: "different map content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: map[string]any{"k": "v"},
want: "foo-values-7c6468f955",
want: "foo-values-77df88dd65",
})
run(testcase{
subject: "different chart",
release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"},
want: "foo-values-8645f5847f",
want: "foo-values-77c96457f7",
})
run(testcase{
subject: "different name",
release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"},
want: "bar-values-54bd8c865",
want: "bar-values-6695f7ff4c",
})
run(testcase{
subject: "specific ns",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"},
want: "myns-foo-values-b4849b445",
want: "myns-foo-values-9b9484d4c",
})
for id, n := range ids {
+4
View File
@@ -0,0 +1,4 @@
letter: a
only_a: from-a
both_ab: from-a
all_three: from-a
+4
View File
@@ -0,0 +1,4 @@
only_b: from-b
both_ab: from-b
both_bc: from-b
all_three: from-b
+3
View File
@@ -0,0 +1,3 @@
only_c: from-c
both_bc: from-c
all_three: from-c
+2
View File
@@ -0,0 +1,2 @@
cluster:
domain: example.com
+5
View File
@@ -0,0 +1,5 @@
cluster:
domain: cluster.local
region: us-east-1
service:
port: 8080
+3
View File
@@ -0,0 +1,3 @@
service:
domain: "service.{{ .Values.cluster.domain }}"
port: 8080
+2
View File
@@ -0,0 +1,2 @@
cluster:
domain: example.com
+2
View File
@@ -0,0 +1,2 @@
service:
domain: "service.{{ .Values.cluster.domain }}"
+1
View File
@@ -0,0 +1 @@
value: ~
+1
View File
@@ -0,0 +1 @@
value: from-fallback
+4
View File
@@ -0,0 +1,4 @@
enabled: false
replicas: 0
name: ""
tags: []
+6
View File
@@ -0,0 +1,6 @@
enabled: true
replicas: 3
name: from-fallback
tags:
- a
- b
+3 -4
View File
@@ -2,6 +2,7 @@ package state
import (
"github.com/helmfile/helmfile/pkg/environment"
"github.com/helmfile/helmfile/pkg/maputil"
)
// TemplateSpec defines the structure of a reusable and composable template for helm releases.
@@ -21,11 +22,9 @@ type EnvironmentTemplateData struct {
}
func NewEnvironmentTemplateData(env environment.Environment, namespace string, values map[string]any) *EnvironmentTemplateData {
// Create a copy of the environment with merged values for template access.
// This ensures templates accessing .Environment.Values see the same merged values
// (Defaults + Values + CLIOverrides) as templates accessing .Values directly.
envCopy := env
envCopy.Values = values
envCopy.Values = maputil.MergeMaps(env.Values, env.CLIOverrides,
maputil.MergeOptions{ArrayStrategy: maputil.ArrayMergeStrategyMerge})
d := EnvironmentTemplateData{envCopy, namespace, values, nil}
d.StateValues = &d.Values
return &d
+52
View File
@@ -0,0 +1,52 @@
package state
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/helmfile/helmfile/pkg/environment"
)
func TestNewEnvironmentTemplateData_EnvironmentValuesExcludesDefaults_Issue2527(t *testing.T) {
env := environment.Environment{
Name: "myenv",
Defaults: map[string]any{
"helmDefaults": map[string]any{
"atomic": true,
"wait": true,
"timeout": 300,
},
},
Values: map[string]any{
"appName": "my-app",
},
CLIOverrides: map[string]any{
"cliFlag": "cliValue",
},
}
mergedVals := map[string]any{
"appName": "my-app",
"cliFlag": "cliValue",
"helmDefaults": map[string]any{
"atomic": true,
"wait": true,
"timeout": 300,
},
}
tmplData := NewEnvironmentTemplateData(env, "ns", mergedVals)
require.NotNil(t, tmplData)
assert.Equal(t, mergedVals, tmplData.Values, ".Values should contain full merged values")
_, hasDefaults := tmplData.Environment.Values["helmDefaults"]
assert.False(t, hasDefaults, ".Environment.Values should NOT contain Defaults (helmDefaults)")
assert.Equal(t, "my-app", tmplData.Environment.Values["appName"],
".Environment.Values should contain env Values")
assert.Equal(t, "cliValue", tmplData.Environment.Values["cliFlag"],
".Environment.Values should contain CLI overrides")
}
+16 -8
View File
@@ -1,6 +1,6 @@
# Helmfile Agent Skill
Expert guidance for Helmfile, a declarative spec for deploying Helm charts to Kubernetes.
Expert guidance for Helmfile v1.1, a declarative spec for deploying Helm charts to Kubernetes.
## Installation
@@ -32,14 +32,17 @@ cp -r skills/helmfile ~/.agents/skills/
## What This Skill Covers
- **Configuration Structure**: Basic helmfile.yaml format and release configuration
- **CLI Commands**: sync, apply, diff, destroy, template, and more
- **Templating**: Built-in objects, template functions, values files templates
- **Environments**: Multi-environment setup and conditional releases
- **Status**: Helmfile v1.0/v1.1 released, supports Helm 3.x and Helm 4.x
- **Configuration Structure**: Full helmfile.yaml reference with all release fields
- **CLI Commands**: sync, apply, diff, destroy, template, fetch, unittest, show-dag, write-values, and more
- **Templating**: Built-in objects, template functions (env, exec, readFile, fetchSecretValue, expandSecretRefs), partials
- **Environments**: Multi-environment setup, HCL values, conditional releases
- **Values Merging**: Data flow and precedence (bases -> root values -> env values -> HCL -> secrets -> CLI overrides)
- **Layering**: Bases, release templates, nested helmfiles
- **Advanced Features**: Kustomize integration, strategic merge patches, transformers, chart dependencies, remote secrets
- **Best Practices**: Directory structure, DRY configuration, labels filtering
- **Troubleshooting**: Common issues and solutions
- **Hooks**: Lifecycle hooks (prepare, preapply, presync, preuninstall, postuninstall, postsync, cleanup) with kubectlApply
- **Advanced Features**: Kubedog resource tracking, Kustomize integration, strategic merge patches, JSON patches, transformers, chart dependencies, remote secrets (vals)
- **Best Practices**: Directory structure, DRY configuration, labels filtering, missing keys handling
- **Troubleshooting**: Common issues and solutions, Helm 4 compatibility
## Usage
@@ -49,9 +52,14 @@ Once installed, simply ask your AI agent questions about Helmfile:
- "How do I set up multi-environment deployments?"
- "Explain release templates and layering"
- "Help me troubleshoot a Helmfile sync issue"
- "How do I use kubedog for resource tracking?"
- "Set up hooks for CRD installation before sync"
- "How do I use vals for remote secrets?"
## References
- [Helmfile Documentation](https://helmfile.readthedocs.io)
- [Helmfile GitHub](https://github.com/helmfile/helmfile)
- [Helm Documentation](https://helm.sh)
- [vals - Secret References](https://github.com/helmfile/vals)
- [kubedog - Resource Tracking](https://github.com/werf/kubedog)
+335 -38
View File
@@ -7,6 +7,10 @@ description: Expert guidance for Helmfile declarative Helm chart deployment
You are an expert in Helmfile, a declarative spec for deploying Helm charts to Kubernetes clusters.
## Status
Helmfile v1.0 and v1.1 have been released (May 2025). We recommend upgrading directly to v1.1 if you are still using v0.x. Helmfile supports both Helm 3.x and Helm 4.x.
## What is Helmfile
Helmfile is a declarative configuration tool that manages Helm releases. It allows you to:
@@ -22,6 +26,24 @@ Helmfile is a declarative configuration tool that manages Helm releases. It allo
## Configuration Structure
### Quick Reference
A `helmfile.yaml` has these top-level sections:
| Section | Purpose |
|---------|---------|
| `repositories` | Helm chart repositories to use |
| `releases` | The Helm releases to deploy (core of helmfile) |
| `helmDefaults` | Default Helm options for all releases |
| `environments` | Environment-specific values (dev, staging, prod) |
| `helmfiles` | Include other helmfile.yaml files (nesting) |
| `bases` | Shared base files merged before this helmfile |
| `values` | Default values available in templates |
| `commonLabels` | Labels applied to all releases |
| `templates` | Reusable release templates |
| `hooks` | Global lifecycle hooks |
| `apiVersions` / `kubeVersion` | Kubernetes version capabilities |
### Basic helmfile.yaml
```yaml
repositories:
@@ -37,21 +59,65 @@ releases:
- values.yaml
```
### Repository Configuration
```yaml
repositories:
- name: stable
url: https://charts.helm.sh/stable
# Git-based repository
- name: polaris
url: git+https://github.com/reactiveops/polaris@deploy/helm?ref=master
# OCI registry with auth
- name: roboll
url: roboll.io/charts
certFile: optional_client_cert
keyFile: optional_client_key
username: optional_username
password: optional_password
oci: true
passCredentials: true
verify: true
keyring: path/to/keyring.gpg
# Self-signed certificate
- name: insecure
url: https://charts.example.com
caFile: optional_ca_file
```
### Release Configuration Fields
| Field | Description |
|-------|-------------|
| `name` | Release name |
| `namespace` | Target namespace |
| `chart` | Chart reference (repo/chart or local path) |
| `version` | Semver constraint |
| `values` | Values files or inline values |
| `set`/`setString` | Override specific values |
| `secrets` | Encrypted values files (requires helm-secrets plugin) |
| `installed` | Set false to uninstall on sync |
| `wait` | Wait for resources to be ready |
| `timeout` | Operation timeout in seconds |
| `kubeContext` | Kubernetes context to use |
| `labels` | Key-value pairs for filtering |
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `name` | string | | Release name |
| `namespace` | string | | Target namespace |
| `chart` | string | | Chart reference (repo/chart or local path) |
| `version` | string | | Semver constraint |
| `values` | list | | Values files or inline values |
| `set`/`setString` | list | | Override specific values |
| `secrets` | list | | Encrypted values files (requires helm-secrets plugin) |
| `installed` | bool | | Set false to uninstall on sync |
| `condition` | string | | Values lookup key for filtering releases |
| `wait` | bool | false | Wait for resources to be ready |
| `waitForJobs` | bool | false | Wait until all Jobs have completed |
| `timeout` | int | 300 | Operation timeout in seconds |
| `kubeContext` | string | | Kubernetes context to use |
| `labels` | map | | Key-value pairs for filtering |
| `createNamespace` | bool | true | Automatically create release namespace |
| `missingFileHandler` | string | | "Error" or "Warn" for missing files |
| `missingFileHandlerConfig` | map | | Additional missing file handler config |
| `valuesTemplate` | list | | Like `values` but template expressions rendered before passing to Helm |
| `setTemplate` | list | | Like `set` but template expressions rendered before passing to Helm |
| `apiVersions` | list | | Per-release API versions |
| `kubeVersion` | string | | Per-release kube version |
| `valuesPathPrefix` | string | | Prefix for values file paths |
| `verifyTemplate` | string | | Templated verify flag |
| `waitTemplate` | string | | Templated wait flag |
| `installedTemplate` | string | | Templated installed flag |
| `adopt` | list | | Resources to adopt (passes `--adopt` to Helm) |
| `forceGoGetter` | bool | false | Force go-getter URL parsing for chart field |
| `forceNamespace` | string | | Force namespace on all K8s resources |
| `skipRefresh` | bool | false | Per-release skip for `helm dependency up` |
| `disableAutoDetectedKubeVersionForDiff` | bool | false | Disable auto-detected kubeVersion for diff |
| `takeOwnership` | bool | false | Take ownership of existing resources |
### Helm Defaults
```yaml
@@ -63,6 +129,28 @@ helmDefaults:
force: false
atomic: true
cleanupOnFail: false
verify: false
keyring: path/to/keyring.gpg
skipSchemaValidation: false
waitForJobs: true
recreatePods: false
historyMax: 10
devel: false
skipDeps: false
reuseValues: false
enableDNS: false
skipCRDs: false
skipRefresh: false
forceConflicts: false
takeOwnership: false
trackMode: ""
disableAutoDetectedKubeVersionForDiff: false
args:
- "--set k=v"
diffArgs:
- "--suppress-secrets"
syncArgs:
- "--labels=app.kubernetes.io/managed-by=helmfile"
```
## CLI Commands
@@ -79,6 +167,15 @@ helmfile lint # Lint charts
helmfile test # Run helm tests
helmfile list # List releases
helmfile deps # Lock dependencies
helmfile repos # Add chart repositories
helmfile fetch # Fetch charts from state file
helmfile status # Retrieve status of releases
helmfile build # Build all resources from state file
helmfile write-values # Write values files (like template but for values)
helmfile unittest # Unit test charts using helm-unittest plugin
helmfile show-dag # Show release dependency graph (GROUP, RELEASE, DEPENDENCIES)
helmfile cache # Cache management
helmfile create # Create a helmfile deployment project scaffold
```
### Common Flags
@@ -91,29 +188,90 @@ helmfile deps # Lock dependencies
| `--kube-context` | Kubernetes context |
| `--interactive` | Confirm before changes |
| `--skip-deps` | Skip dependency updates |
| `--allow-no-matching-release` | Don't error if selector has no matches |
| `-c, --chart` | Set chart (available in template as {{ .Chart }}) |
| `--color` | Output with color |
| `--debug` | Enable verbose output |
| `--state-values-set` | Override state values from CLI |
| `--state-values-file` | Override state values from file |
| `--track-mode` | Resource tracking mode (helm, helm-legacy, kubedog) |
| `--track-timeout` | Tracking timeout in seconds |
| `--track-logs` | Enable real-time log streaming |
### Fetch Command (Air-gapped Environments)
```bash
helmfile fetch --output-dir ./charts --write-output
```
| Flag | Default | Description |
|------|---------|-------------|
| `--output-dir` | temp dir | Directory to store charts |
| `--output-dir-template` | default template | Go template for output dir (`.OutputDir`, `.ChartName`, `.Release.*`, `.Environment.*`) |
| `--write-output` | false | Write helmfile.yaml with updated chart paths to stdout |
| `--concurrency` | 0 | Max concurrent helm processes |
### Show DAG
```bash
helmfile show-dag
```
Prints a table with GROUP, RELEASE, and DEPENDENCIES. Releases in the same GROUP are deployed concurrently. GROUP 2 starts only after GROUP 1 completes.
### Unit Tests
```bash
helmfile unittest # Requires helm-unittest plugin
```
```yaml
releases:
- name: my-app
chart: ./charts/my-app
values:
- values.yaml
unitTests:
- tests # Relative to chart dir, /*_test.yaml appended
```
## Templating
### Built-in Objects
- `.Environment.Name` - Current environment name
- `.Environment.KubeContext` - Environment's kube context
- `.Values` / `.StateValues` - Environment values
- `.Values` / `.StateValues` - Environment values (`.StateValues` is an alias)
- `.Release.Name` - Release name
- `.Release.Namespace` - Release namespace
- `.Release.Labels` - Release labels
- `.Namespace` - Target namespace
- `.Chart` - Chart set via `--chart` flag
- `.HelmfileCommand` - The helmfile command being run
### Helmfile .Values vs Helm .Values
Helmfile uses the same `.Values` name as Helm. To distinguish, use `.StateValues` for Helmfile's values:
```yaml
app:
project: {{.Environment.Name}}-{{.StateValues.project}}
{{`
extraEnvVars:
- name: APP_PROJECT
value: {{.Values.app.project}}
`}}
```
### Template Functions
| Function | Description |
|----------|-------------|
| `env "VAR"` | Get optional env var (returns empty if unset) |
| `requiredEnv "VAR"` | Get required env var (fails if unset) |
| `exec "cmd" (list "args")` | Execute command |
| `exec "cmd" (list "args")` | Execute command, return stdout |
| `envExec (dict "k" "v") "cmd" (list "args")` | Execute command with custom env vars |
| `readFile "path"` | Read file contents |
| `readDir "path"` | List file paths in directory |
| `readDirEntries "path"` | List all entries including folders |
| `isFile "path"` | Check if file exists |
| `isDir "path"` | Check if directory exists |
| `toYaml` / `fromYaml` | YAML conversion |
| `get .Values "key" default` | Get nested value with default |
| `required "msg" value` | Fail if value is empty |
| `fetchSecretValue "ref"` | Fetch secret from vals backend |
| `fetchSecretValue "ref"` | Fetch single secret from vals backend |
| `expandSecretRefs` | Fetch map of secrets from vals refs |
| `tpl "{{ .Value.key }}" .` | Render template string |
### Values Files Templates
@@ -125,6 +283,15 @@ db:
password: {{ requiredEnv "DB_PASSWORD" }}
```
### Template Partials
Files matching `_*.tpl` in the same directory are auto-loaded as helpers:
```
{{- define "myapp.labels" -}}
app: myapp
env: {{ .Environment.Name }}
{{- end -}}
```
## Environments
### Environment Configuration
@@ -133,12 +300,18 @@ environments:
default:
values:
- environments/default/values.yaml
- environments/default/values.hcl
- myChartVer: 1.0.0-dev
production:
values:
- environments/production/values.yaml
- myChartVer: 1.0.0
- vault:
enabled: false
secrets:
- environments/production/secrets.yaml
kubeContext: prod-cluster
missingFileHandler: Error
```
### Using Environments
@@ -155,6 +328,31 @@ releases:
chart: stable/prometheus
```
## Values Merging and Data Flow
Values are merged in this order (lowest to highest priority):
```
┌─────────────────────────────────────────────────────────────────┐
│ VALUES MERGING ORDER │
├─────────────────────────────────────────────────────────────────┤
│ 1. Base files (from `bases:`) │
│ 2. Root-level `values:` block (Defaults) │
│ 3. Environment values (yaml/yaml.gotmpl) │
│ 4. Environment values (HCL, including HCL secrets) │
│ 5. Environment secrets (non-HCL, decrypted) │
│ 6. CLI overrides (--state-values-set, --state-values-file) │
└─────────────────────────────────────────────────────────────────┘
```
**Later values override earlier values** at the map level (deep merge). Arrays use smart merging (sparse auto-detection by default).
```bash
# CLI overrides (highest priority)
helmfile --state-values-set image.tag=v2.0.0 sync
helmfile --state-values-file overrides.yaml sync
```
## Layering and Inheritance
### Bases (Layering)
@@ -188,8 +386,81 @@ helmfiles:
- {{ toYaml .Values | nindent 4 }}
```
## Hooks
### Hook Events
| Event | Description |
|-------|-------------|
| `prepare` | After release loaded from YAML, before execution |
| `preapply` | Before uninstall/install/upgrade during `apply` (only if changes exist) |
| `presync` | Before each release is synced (installed or upgraded) |
| `preuninstall` | Immediately before a release is uninstalled |
| `postuninstall` | After successful uninstall of a release |
| `postsync` | After each release is synced, regardless of success |
| `cleanup` | After each release is processed (counterpart to `prepare`) |
### Hook Configuration
```yaml
releases:
- name: myapp
chart: mychart
hooks:
- events: ["prepare", "cleanup"]
showlogs: true
command: "echo"
args: ["{{`{{.Environment.Name}}`}}", "{{`{{.Release.Name}}`}}"]
- events: ["presync"]
showlogs: true
command: "kubectl"
args: ["apply", "-f", "crds.yaml"]
- events: ["postsync"]
showlogs: true
command: "kubectl"
args: ["rollout", "status", "deployment/myapp"]
```
### kubectlApply Hook
Alternative to `command`/`args`, directly apply manifests:
```yaml
hooks:
- events: ["presync"]
kubectlApply:
- apiVersion: v1
kind: ConfigMap
metadata:
name: my-config
data:
key: value
```
## Advanced Features
### Resource Tracking with Kubedog
```yaml
releases:
- name: myapp
chart: ./charts/myapp
trackMode: kubedog
trackTimeout: 300
trackLogs: true
trackKinds:
- Deployment
- StatefulSet
skipKinds:
- ConfigMap
trackResources:
- kind: Deployment
name: myapp-deployment
namespace: default
```
**Track Modes:**
| Mode | Description |
|------|-------------|
| `helm` (default) | Uses Helm's built-in `--wait` |
| `helm-legacy` | Uses Helm v4's `--wait=legacy` for compatibility |
| `kubedog` | Advanced tracking with detailed feedback |
### Kustomize Integration
Deploy kustomizations as Helm releases:
```yaml
@@ -208,13 +479,36 @@ releases:
releases:
- name: raw1
chart: incubator/raw
values:
- resources:
- apiVersion: v1
kind: ConfigMap
metadata:
name: raw1
data:
foo: FOO
strategicMergePatches:
- apiVersion: v1
kind: ConfigMap
metadata:
name: raw1
data:
extra: value
bar: BAR
```
### JSON Patches
```yaml
releases:
- name: myapp
chart: mychart
jsonPatches:
- target:
version: v1
kind: ConfigMap
name: myconfig
patch:
- op: remove
path: /data/old-key
```
### Transformers
@@ -246,13 +540,30 @@ releases:
### Remote Secrets (vals)
```yaml
# Single key
releases:
- name: app
values:
- db:
password: ref+awssecrets://my-secret/db-password
password: {{ .Values.db.password | fetchSecretValue | quote }}
# Multiple keys
environments:
default:
values:
- service:
password: ref+vault://svc/#pass
login: ref+vault://svc/#login
```
```yaml
# values.yaml.gotmpl
service:
{{ .Values.service | expandSecretRefs | toYaml | nindent 2 }}
```
Supported backends: Vault, AWS SSM, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, and more via [vals](https://github.com/helmfile/vals).
## Best Practices
### Directory Structure
@@ -277,6 +588,7 @@ releases:
2. Use `bases` for shared configuration
3. Use `environments` for environment-specific values
4. Use `.gotmpl` files for templated values
5. Use `_*.tpl` partials for shared template logic
### Missing Keys Handling
```yaml
@@ -327,13 +639,6 @@ releases:
- replicaCount: {{ .Values.replicas }}
```
### Git-based Charts
```yaml
repositories:
- name: polaris
url: git+https://github.com/reactiveops/polaris@deploy/helm?ref=master
```
### OCI Charts
```yaml
repositories:
@@ -347,18 +652,6 @@ releases:
version: 1.0.0
```
### Hooks
```yaml
releases:
- name: crds
chart: ./crds
hooks:
- events: ["presync"]
showlogs: true
command: "kubectl"
args: ["apply", "-f", "crds.yaml"]
```
## Troubleshooting
### Debug Template Rendering
@@ -378,6 +671,7 @@ helmfile list
2. **Chart not found**: Run `helmfile deps` or check repository config
3. **Diff plugin missing**: Install with `helm plugin install https://github.com/databus23/helm-diff`
4. **Secrets not decrypting**: Install helm-secrets plugin
5. **Helm v4 compatibility**: Use `trackMode: helm-legacy` for charts with broken `livenessProbe` configs
## Environment Variables
| Variable | Description |
@@ -399,3 +693,6 @@ Invoke this skill when:
- Implementing best practices for Helm chart management
- Configuring remote secrets with vals
- Using advanced features like strategic merge patches and transformers
- Setting up resource tracking with kubedog
- Managing Helm 4 compatibility
- Writing hooks for lifecycle management
+3 -3
View File
@@ -1,8 +1,8 @@
{
"version": "1.0.0",
"version": "1.1.0",
"organization": "Helmfile",
"date": "February 2026",
"abstract": "Comprehensive guide for Helmfile, a declarative spec for deploying Helm charts to Kubernetes. Covers configuration structure, CLI commands, templating, environments, layering, release templates, Kustomize integration, strategic merge patches, transformers, chart dependencies, remote secrets, and best practices. Designed for AI agents working with Helmfile configurations and Kubernetes deployments.",
"date": "May 2026",
"abstract": "Comprehensive guide for Helmfile v1.1, a declarative spec for deploying Helm charts to Kubernetes. Covers configuration structure, CLI commands, templating, environments, values merging and data flow, layering, release templates, hooks (prepare, presync, postsync, cleanup), Kustomize integration, strategic merge patches, JSON patches, transformers, chart dependencies, remote secrets (vals), resource tracking with kubedog, Helm 4 support, and best practices. Designed for AI agents working with Helmfile configurations and Kubernetes deployments.",
"references": [
"https://helmfile.readthedocs.io",
"https://github.com/helmfile/helmfile",
@@ -0,0 +1,33 @@
Adding repo myrepo http://localhost:18080/
"myrepo" has been added to your repositories
Building dependency release=foo, chart=$WD/temp1/foo
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "myrepo" chart repository
Update Complete. ⎈Happy Helming!⎈
Saving 1 charts
Downloading raw from repo http://localhost:18080/
Deleting outdated charts
Templating release=foo, chart=$WD/temp1/foo
---
# Source: raw/charts/dep/templates/resources.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: foo-2
namespace: default
data:
bar: BAR
---
# Source: raw/templates/resources.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: foo-1
namespace: default
data:
foo: FOO
@@ -0,0 +1,34 @@
Live output is enabled
Adding repo myrepo http://localhost:18081/
"myrepo" has been added to your repositories
Building dependency release=foo, chart=$WD/temp1/foo
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "myrepo" chart repository
Update Complete. ⎈Happy Helming!⎈
Saving 1 charts
Downloading raw from repo http://localhost:18081/
Deleting outdated charts
Templating release=foo, chart=$WD/temp1/foo
---
# Source: raw/charts/dep/templates/resources.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: foo-2
namespace: default
data:
bar: BAR
---
# Source: raw/templates/resources.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: foo-1
namespace: default
data:
foo: FOO
@@ -0,0 +1,27 @@
Building dependency release=foo, chart=$WD/temp1/foo
Saving 1 charts
Downloading raw from repo oci://localhost:$REGISTRY_PORT/myrepo
Deleting outdated charts
Templating release=foo, chart=$WD/temp1/foo
---
# Source: raw/charts/dep/templates/resources.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: foo-2
namespace: default
data:
bar: BAR
---
# Source: raw/templates/resources.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: foo-1
namespace: default
data:
foo: FOO
@@ -13,6 +13,7 @@ data:
two: TWO
metadata:
name: cm2
---
# Source: raw/templates/resources.yaml
apiVersion: v1
@@ -32,6 +33,7 @@ data:
one: ONE
metadata:
name: cm1
---
# Source: raw/templates/resources.yaml
apiVersion: v1
+8 -1
View File
@@ -27,7 +27,7 @@ export HELM_DATA_HOME="${helm_dir}/data"
export HELM_HOME="${HELM_DATA_HOME}"
export HELM_PLUGINS="${HELM_DATA_HOME}/plugins"
export HELM_CONFIG_HOME="${helm_dir}/config"
HELM_DIFF_VERSION="${HELM_DIFF_VERSION:-3.15.3}"
HELM_DIFF_VERSION="${HELM_DIFF_VERSION:-3.15.7}"
HELM_GIT_VERSION="${HELM_GIT_VERSION:-1.4.1}"
HELM_SECRETS_VERSION="${HELM_SECRETS_VERSION:-4.7.4}"
export GNUPGHOME="${PWD}/${dir}/.gnupg"
@@ -96,8 +96,10 @@ ${kubectl} create namespace ${test_ns} || fail "Could not create namespace ${tes
# TEST CASES----------------------------------------------------------------------------------------------------------
. ${dir}/test-cases/issue-2502-race-condition-local-chart.sh
. ${dir}/test-cases/chart-deps-condition.sh
. ${dir}/test-cases/fetch-forl-local-chart.sh
. ${dir}/test-cases/fetch-write-output.sh
. ${dir}/test-cases/suppress-output-line-regex.sh
. ${dir}/test-cases/chartify-jsonPatches-and-strategicMergePatches.sh
. ${dir}/test-cases/include-template-func.sh
@@ -114,6 +116,8 @@ ${kubectl} create namespace ${test_ns} || fail "Could not create namespace ${tes
. ${dir}/test-cases/yaml-overwrite.sh
. ${dir}/test-cases/chart-needs.sh
. ${dir}/test-cases/postrender.sh
. ${dir}/test-cases/postrender-defaults-args.sh
. ${dir}/test-cases/issue-2515.sh
. ${dir}/test-cases/chartify.sh
. ${dir}/test-cases/deps-mr-1011.sh
. ${dir}/test-cases/deps-kustomization-i-1402.sh
@@ -138,6 +142,9 @@ ${kubectl} create namespace ${test_ns} || fail "Could not create namespace ${tes
. ${dir}/test-cases/issue-2418.sh
. ${dir}/test-cases/issue-2424-sequential-values-paths.sh
. ${dir}/test-cases/issue-2431.sh
. ${dir}/test-cases/issue-2544.sh
. ${dir}/test-cases/issue-2596-local-deps-multiple-files.sh
. ${dir}/test-cases/issue-2599-default-inherit.sh
. ${dir}/test-cases/kubedog-tracking.sh
. ${dir}/test-cases/include-needs-transitive.sh

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