Merge branch 'main' into fix/issue-1904-patch-template-values

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-05-16 23:31:11 +00:00 committed by GitHub
commit e947fddfcd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
92 changed files with 5750 additions and 2571 deletions

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

View File

@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
helm-version: [v3.18.6, v3.20.2, v4.1.4]
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.4'
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.4'
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.4'
if: matrix.helm-version == 'v4.2.0'
run: ls -l helmfile diff-yamls dyff
integration_tests:
@ -105,23 +105,23 @@ jobs:
plugin-secrets-version: 4.7.4
plugin-diff-version: 3.15.3
extra-helmfile-flags: '--enable-live-output'
- helm-version: v3.20.2
- helm-version: v3.21.0
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.4
plugin-diff-version: 3.15.3
extra-helmfile-flags: ''
- helm-version: v3.20.2
- helm-version: v3.21.0
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.4
plugin-diff-version: 3.15.3
extra-helmfile-flags: '--enable-live-output'
# Helmfile now supports both Helm 3.x and Helm 4.x
- helm-version: v4.1.4
- helm-version: v4.2.0
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.4
plugin-diff-version: 3.15.3
extra-helmfile-flags: ''
- helm-version: v4.1.4
- helm-version: v4.2.0
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.4
plugin-diff-version: 3.15.3

View File

@ -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.4"
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="70b2c30a19da4db264dfd68c8a3664e05093a361cefd89572ffb36f8abfa3d09" ;; \
"linux/arm64") HELM_SHA256="13d03672be289045d2ff00e4e345d61de1c6f21c1257a45955a30e8ae036d8f1" ;; \
"linux/amd64") HELM_SHA256="97dbeb971be4ac4b27e3839976d9564c0fb35c6f3b1da89dd1e292d236af4096" ;; \
"linux/arm64") HELM_SHA256="1f8de130dfbd04de64978e7b852a7a547be1404956a366608276d2520b678670" ;; \
esac && \
echo "${HELM_SHA256} ${HELM_FILENAME}" | sha256sum -c && \
echo Extracting ${HELM_FILENAME}... && \

View File

@ -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.4"
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="70b2c30a19da4db264dfd68c8a3664e05093a361cefd89572ffb36f8abfa3d09" ;; \
"linux/arm64") HELM_SHA256="13d03672be289045d2ff00e4e345d61de1c6f21c1257a45955a30e8ae036d8f1" ;; \
"linux/amd64") HELM_SHA256="97dbeb971be4ac4b27e3839976d9564c0fb35c6f3b1da89dd1e292d236af4096" ;; \
"linux/arm64") HELM_SHA256="1f8de130dfbd04de64978e7b852a7a547be1404956a366608276d2520b678670" ;; \
esac && \
echo "${HELM_SHA256} ${HELM_FILENAME}" | sha256sum -c && \
echo Extracting ${HELM_FILENAME}... && \

View File

@ -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.4"
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="70b2c30a19da4db264dfd68c8a3664e05093a361cefd89572ffb36f8abfa3d09" ;; \
"linux/arm64") HELM_SHA256="13d03672be289045d2ff00e4e345d61de1c6f21c1257a45955a30e8ae036d8f1" ;; \
"linux/amd64") HELM_SHA256="97dbeb971be4ac4b27e3839976d9564c0fb35c6f3b1da89dd1e292d236af4096" ;; \
"linux/arm64") HELM_SHA256="1f8de130dfbd04de64978e7b852a7a547be1404956a366608276d2520b678670" ;; \
esac && \
echo "${HELM_SHA256} ${HELM_FILENAME}" | sha256sum -c && \
echo Extracting ${HELM_FILENAME}... && \

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:

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:

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
cmd/create.go Normal file
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
}

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
}

View File

@ -92,6 +92,7 @@ func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) {
}
cmd.AddCommand(
NewCreateCmd(globalImpl),
NewInitCmd(globalImpl),
NewApplyCmd(globalImpl),
NewBuildCmd(globalImpl),

View File

@ -57,6 +57,7 @@ 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"`)
return cmd

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:

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
docs/cli.md Normal file
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
--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 (default "helm")
-h, --help help for helmfile
-i, --interactive Request confirmation before attempting to modify clusters
--kube-context string Set kubectl context. Uses current context by default
-k, --kustomize-binary string Path to the kustomize binary (default "kustomize")
--log-level string Set log level, default info (default "info")
-n, --namespace string Set namespace. Uses the namespace set in the context by default, and is available in templates as {{ .Namespace }}
--no-color Output without color
-q, --quiet Silence output. Equivalent to log-level warn
-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 |

528
docs/configuration.md Normal file
View File

@ -0,0 +1,528 @@
# 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 |
| `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
docs/environments.md Normal file
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).

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
docs/hooks.md Normal file
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

File diff suppressed because it is too large Load Diff

136
docs/integrations.md Normal file
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
docs/releases.md Normal file
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`

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.47
GitPython==3.1.50
importlib-metadata==4.11.3
Jinja2==3.1.6
Markdown==3.8.1

236
docs/templating.md Normal file
View File

@ -0,0 +1,236 @@
# 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_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

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:

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.

174
go.mod
View File

@ -4,10 +4,10 @@ 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.16
github.com/aws/aws-sdk-go-v2/service/s3 v1.100.0
github.com/aws/aws-sdk-go-v2/config v1.32.17
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
@ -18,37 +18,45 @@ require (
github.com/hashicorp/go-getter/v2 v2.2.3
github.com/hashicorp/hcl/v2 v2.24.0
github.com/helmfile/chartify v0.26.3
github.com/helmfile/vals v0.43.9
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/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.42.0
golang.org/x/term v0.43.0
gopkg.in/yaml.v3 v3.0.1
helm.sh/helm/v3 v3.20.2
helm.sh/helm/v4 v4.1.4
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.35.4
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.7.0 // indirect
cloud.google.com/go/storage v1.62.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,10 +66,10 @@ 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.21.0 // indirect
github.com/goware/prefixer v0.0.0-20160118172347-395022866408 // indirect
@ -69,20 +77,20 @@ require (
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.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,12 +105,12 @@ 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.52.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.35.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.275.0 // 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
@ -117,18 +125,18 @@ require (
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.27.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.18.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,14 +145,14 @@ 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.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.7 // indirect
@ -155,32 +163,33 @@ 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.6 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.15 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 // 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.16 // 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.22 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.14 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.22 // indirect
github.com/aws/aws-sdk-go-v2/service/kms v1.50.4 // indirect
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.5 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.10 // indirect
github.com/aws/aws-sdk-go-v2/service/ssm v1.68.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.16 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 // indirect
github.com/aws/smithy-go v1.25.0 // 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.35.21 // 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
@ -193,7 +202,7 @@ require (
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
@ -201,7 +210,7 @@ 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
@ -212,25 +221,25 @@ require (
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
@ -241,7 +250,7 @@ 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
@ -251,14 +260,14 @@ require (
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.171.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.7.0 // 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
@ -287,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
@ -309,11 +317,9 @@ 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.69.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
@ -321,35 +327,35 @@ require (
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.67.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp 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.9.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/tools v0.42.0 // 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-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.4 // 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/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.35.1 // 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

433
go.sum
View File

@ -15,18 +15,18 @@ cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdB
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
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.27.0 h1:iYYgoD0HJIqz35A+He1G0dS5qTQzQsDXFsyXwzkUCXM=
cloud.google.com/go/kms v1.27.0/go.mod h1:KPxrdf61iYEOZ86uPwR86muBpSik2y4Ion6e83fVl1Q=
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.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
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.18.0 h1:VA/ynUUapUF3+xrm0R1dMx8i21p2jfRAWFpokYPncKU=
cloud.google.com/go/secretmanager v1.18.0/go.mod h1:9OmSuOeiiUicANglrbdKWSnT3gYkRcXuUQDk7dDW0zU=
cloud.google.com/go/storage v1.62.0 h1:w2pQJhpUqVerMON45vatE2FpCYsNTf7OHjkn6ux5mMU=
cloud.google.com/go/storage v1.62.0/go.mod h1:T5hz3qzcpnxZ5LdKc7y8Tw7lh4v9zeeVyrD/cLJAzZU=
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,8 +96,8 @@ 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.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
@ -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,8 +126,8 @@ 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=
@ -152,50 +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.6 h1:1AX0AthnBQzMx1vbmir3Y4WsnJgiydmnJjiLu+LvXOg=
github.com/aws/aws-sdk-go-v2 v1.41.6/go.mod h1:dy0UzBIfwSeot4grGvY1AqFWN5zgziMmWGzysDnHFcQ=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9 h1:adBsCIIpLbLmYnkQU+nAChU5yhVTvu5PerROm+/Kq2A=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.9/go.mod h1:uOYhgfgThm/ZyAuJGNQ5YgNyOlYfqnGpTHXvk3cpykg=
github.com/aws/aws-sdk-go-v2/config v1.32.16 h1:Q0iQ7quUgJP0F/SCRTieScnaMdXr9h/2+wze1u3cNeM=
github.com/aws/aws-sdk-go-v2/config v1.32.16/go.mod h1:duCCnJEFqpt2RC6no1iK6q+8HpwOAkiUua0pY507dQc=
github.com/aws/aws-sdk-go-v2/credentials v1.19.15 h1:fyvgWTszojq8hEnMi8PPBTvZdTtEVmAVyo+NFLHBhH4=
github.com/aws/aws-sdk-go-v2/credentials v1.19.15/go.mod h1:gJiYyMOjNg8OEdRWOf3CrFQxM2a98qmrtjx1zuiQfB8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 h1:IOGsJ1xVWhsi+ZO7/NW8OuZZBtMJLZbk4P5HDjJO0jQ=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22/go.mod h1:b+hYdbU+jGKfXE8kKM6g1+h+L/Go3vMvzlxBsiuGsxg=
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.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU=
github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE=
github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU=
github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug=
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.22 h1:GmLa5Kw1ESqtFpXsx5MmC84QWa/ZrLZvlJGa2y+4kcQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22/go.mod h1:6sW9iWm9DK9YRpRGga/qzrzNLgKpT2cIxb7Vo2eNOp0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 h1:dY4kWZiSaXIzxnKlj17nHnBcXXBfac6UlsAx2qL6XrU=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22/go.mod h1:KIpEUx0JuRZLO7U6cbV204cWAEco2iC3l061IxlwLtI=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23 h1:FPXsW9+gMuIeKmz7j6ENWcWtBGTe1kH8r9thNt5Uxx4=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23/go.mod h1:7J8iGMdRKk6lw2C+cMIphgAnT8uTwBwNOsGkyOCm80U=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 h1:HtOTYcbVcGABLOVuPYaIihj6IlkqubBwFj10K5fxRek=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8/go.mod h1:VsK9abqQeGlzPgUr+isNWzPlK2vKe9INMLWnY65f5Xs=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.14 h1:xnvDEnw+pnj5mctWiYuFbigrEzSm35x7k4KS/ZkCANg=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.14/go.mod h1:yS5rNogD8e0Wu9+l3MUwr6eENBzEeGejvINpN5PAYfY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 h1:PUmZeJU6Y1Lbvt9WFuJ0ugUK2xn6hIWUBBbKuOWF30s=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22/go.mod h1:nO6egFBoAaoXze24a2C0NjQCvdpk8OueRoYimvEB9jo=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.22 h1:SE+aQ4DEqG53RRCAIHlCf//B2ycxGH7jFkpnAh/kKPM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.22/go.mod h1:ES3ynECd7fYeJIL6+oax+uIEljmfps0S70BaQzbMd/o=
github.com/aws/aws-sdk-go-v2/service/kms v1.50.4 h1:PgD1y0ZagPokGIZPmejCBUySBzOFDN+leZxCOfb1OEQ=
github.com/aws/aws-sdk-go-v2/service/kms v1.50.4/go.mod h1:FfXDb5nXrsoGgxsBFxwxr3vdHXheC2tV+6lmuLghhjQ=
github.com/aws/aws-sdk-go-v2/service/s3 v1.100.0 h1:7G26Sae6PMKn4kMcU5JzNfrm1YrKwyOhowXPYR2WiWY=
github.com/aws/aws-sdk-go-v2/service/s3 v1.100.0/go.mod h1:Fw9aqhJicIVee1VytBBjH+l+5ov6/PhbtIK/u3rt/ls=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.5 h1:z2ayoK3pOvf8ODj/vPR0FgAS5ONruBq0F94SRoW/BIU=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.5/go.mod h1:mpZB5HAl4ZIISod9qCi12xZ170TbHX9CCJV5y7nb7QU=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.10 h1:a1Fq/KXn75wSzoJaPQTgZO0wHGqE9mjFnylnqEPTchA=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.10/go.mod h1:p6+MXNxW7IA6dMgHfTAzljuwSKD0NCm/4lbS4t6+7vI=
github.com/aws/aws-sdk-go-v2/service/ssm v1.68.4 h1:5Wg8AAAnIWM2LE/0KFGqllZff96bm4dBs+uerYFfReE=
github.com/aws/aws-sdk-go-v2/service/ssm v1.68.4/go.mod h1:nph0ypDLWm9D9iA9zOX39W/N+A4GqwzlxA13jzXVD4k=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.16 h1:x6bKbmDhsgSZwv6q19wY/u3rLk/3FGjJWyqKcIRufpE=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.16/go.mod h1:CudnEVKRtLn0+3uMV0yEXZ+YZOKnAtUJ5DmDhilVnIw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 h1:oK/njaL8GtyEihkWMD4k3VgHCT64RQKkZwh0DG5j8ak=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20/go.mod h1:JHs8/y1f3zY7U5WcuzoJ/yAYGYtNIVPKLIbp61euvmg=
github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 h1:ks8KBcZPh3PYISr5dAiXCM5/Thcuxk8l+PG4+A0exds=
github.com/aws/aws-sdk-go-v2/service/sts v1.42.0/go.mod h1:pFw33T0WLvXU3rw1WBkpMlkgIn54eCB5FYLhjDc9Foo=
github.com/aws/smithy-go v1.25.0 h1:Sz/XJ64rwuiKtB6j98nDIPyYrV1nVNJ4YU74gttcl5U=
github.com/aws/smithy-go v1.25.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
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.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/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=
@ -206,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=
@ -217,9 +217,11 @@ 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=
@ -238,8 +240,8 @@ github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3
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=
@ -260,20 +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 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.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
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-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/go-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=
@ -282,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=
@ -308,16 +310,16 @@ 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=
@ -339,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=
@ -349,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=
@ -413,7 +415,6 @@ github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14j
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/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=
@ -446,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=
@ -479,8 +481,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=
@ -504,16 +506,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=
@ -524,16 +526,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.171.0 h1:4jD9R6shs3vFh/FfJPeX1qev20YUMcpiWN4joiSBdAk=
github.com/hashicorp/hcp-sdk-go v0.171.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.23.0 h1:gXgluBsSECfRWTSW9niY2jwg2e9mMJc4WoHNv4g3h6A=
github.com/hashicorp/vault/api v1.23.0/go.mod h1:zransKiB9ftp+kgY8ydjnvCU7Wk8i9L0DYWpXeMj9ko=
github.com/helmfile/chartify v0.26.3 h1:2wR0yfqtP/yG9y6uqM6nSKZ7W0E+nhhGGRsl14TOVVs=
github.com/helmfile/chartify v0.26.3/go.mod h1:/ReUGTnbNHIV5tKAGXODkRtS7HwnUiJi2EXbJ34RzgY=
github.com/helmfile/vals v0.43.9 h1:S1hjtq6OtF5tusREQH7KlImr1W11xmFV3q1+HhWLBnY=
github.com/helmfile/vals v0.43.9/go.mod h1:9QB7tH1yN8HcIPYo5kxzh6vP0VI4rTmiQqIP6NVudMs=
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=
@ -546,12 +548,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.7.0 h1:x9/1PczL+ioVD1jCp4LHQzPpBawatbmkjAC0S1OAtUA=
github.com/infisical/go-sdk v0.7.0/go.mod h1:yEfXF+3YDDXiJ9zzJUSzW6me6XXPPEDK52fSU6JfpCA=
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=
@ -563,8 +565,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=
@ -577,8 +579,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=
@ -592,8 +594,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=
@ -642,8 +644,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=
@ -687,8 +689,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=
@ -696,9 +698,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=
@ -779,8 +778,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=
@ -788,7 +787,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=
@ -799,8 +797,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.69.0 h1:gfl5l9KqntQ0oShbDHCD60HB76Dm3IXETUro1mg0Rjs=
github.com/yandex-cloud/go-genproto v0.69.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=
@ -822,54 +820,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/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.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/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.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.42.0 h1:lSZHgNHfbmQTPfuTmWVkEu8J8qXaQwuV30pjCcAUvP8=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0/go.mod h1:so9ounLcuoRDu033MW/E0AD4hhUjVqswrMF5FoZlBcw=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.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/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.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI=
go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4=
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.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/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=
@ -878,8 +876,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=
@ -893,22 +891,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.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
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.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
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=
@ -925,8 +924,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.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
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=
@ -960,21 +959,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.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.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.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
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=
@ -985,8 +986,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.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
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=
@ -1000,16 +1001,16 @@ 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.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
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.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.275.0 h1:vfY5d9vFVJeWEZT65QDd9hbndr7FyZ2+6mIzGAh71NI=
google.golang.org/api v0.275.0/go.mod h1:Fnag/EWUPIcJXuIkP1pjoTgS5vdxlk3eeemL7Do6bvw=
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=
@ -1018,8 +1019,8 @@ google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgn
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-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/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=
@ -1053,38 +1054,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.2 h1:binM4rvPx5DcNsa1sIt7UZi55lRbu3pZUFmQkSoRh48=
helm.sh/helm/v3 v3.20.2/go.mod h1:Fl1kBaWCpkUrM6IYXPjQ3bdZQfFrogKArqptvueZ6Ww=
helm.sh/helm/v4 v4.1.4 h1:zwTrNkalG4f7SYigRSdQnYrTj0QEz1qzetzAlYoDVSo=
helm.sh/helm/v4 v4.1.4/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.4 h1:P7nFYKl5vo9AGUp1Z+Pmd3p2tA7bX2wbFWCvDeRv988=
k8s.io/api v0.35.4/go.mod h1:yl4lqySWOgYJJf9RERXKUwE9g2y+CkuwG+xmcOK8wXU=
k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w=
k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ=
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.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/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.35.1 h1:XgvpRf4srp037QWfGBLFsYMUQJkE5yMa94UsJU7pmcE=
k8s.io/component-base v0.35.1/go.mod h1:HI/6jXlwkiOL5zL9bqA3en1Ygv60F03oEpnuU1G56Bs=
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.35.1 h1:zP3Er8C5i1dcAFUMh9Eva0kVvZHptXIn/+8NtRWMxwg=
k8s.io/kubectl v0.35.1/go.mod h1:cQ2uAPs5IO/kx8R5s5J3Ihv3VCYwrx0obCXum0CvnXo=
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=

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

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(),
@ -233,10 +234,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(),
@ -261,7 +263,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(),
@ -313,7 +315,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(),
@ -355,7 +357,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(),
@ -391,8 +393,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(),
@ -401,6 +445,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
})
@ -408,18 +473,25 @@ func (a *App) Fetch(c FetchConfigProvider) error {
errs = append(errs, prepErr)
}
return
return ok, errs
}, 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) {
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(),
@ -428,7 +500,7 @@ func (a *App) Sync(c SyncConfigProvider) error {
Validate: c.Validate(),
Concurrency: c.Concurrency(),
}, func() []error {
ok, errs = a.sync(run, c)
ok, errs = a.SyncState(run, c)
return errs
})
@ -452,10 +524,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(),
@ -497,7 +570,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(),
@ -517,7 +590,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(),
@ -546,7 +619,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(),
@ -567,7 +640,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,
@ -584,7 +657,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,
@ -657,7 +730,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,
@ -1635,39 +1708,60 @@ func (a *App) getSelectedReleases(r *Run, includeTransitiveNeeds bool) ([]state.
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.IncludeTransitiveNeeds())
selectedReleases, selectedAndNeededReleases, err := a.getSelectedReleases(r, 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 false, false, []error{err}
return nil, nil, err
}
var releasesWithNeeds []state.ReleaseSpec
for _, rs := range plan {
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}
}
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 = releasesWithNeeds
@ -1818,6 +1912,7 @@ 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)
@ -2115,37 +2210,18 @@ 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, []error) {
st := r.state
helm := r.helm
selectedReleases, selectedAndNeededReleases, err := a.getSelectedReleases(r, c.IncludeTransitiveNeeds())
releasesWithNeeds, selectedAndNeededReleases, err := a.GetPlannedAndSelectedReleasesWithNeeds(r, c.SkipNeeds(), c.IncludeNeeds(), c.IncludeTransitiveNeeds())
if err != nil {
return false, []error{err}
}
if len(selectedReleases) == 0 {
if len(releasesWithNeeds) == 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, SkipNeeds: c.SkipNeeds(), IncludeNeeds: c.IncludeNeeds(), IncludeTransitiveNeeds: c.IncludeTransitiveNeeds()})
if err != nil {
return false, []error{err}
}
var releasesWithNeeds []state.ReleaseSpec
for _, rs := range batches {
for _, r := range rs {
releasesWithNeeds = append(releasesWithNeeds, r.ReleaseSpec)
}
}
// 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 = releasesWithNeeds
@ -2193,13 +2269,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))
@ -2288,6 +2357,7 @@ 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(), syncOpts)
@ -2301,6 +2371,13 @@ Do you really want to sync?
affectedReleases.DisplayAffectedReleases(c.Logger())
for id := range releasesWithNoChange {
r := releasesWithNoChange[id]
if _, err := st.TriggerCleanupEvent(&r, "sync"); err != nil {
a.Logger.Warnf("warn: %v\n", err)
}
}
return true, errs
}

View File

@ -2478,6 +2478,10 @@ func (c configImpl) EnforceNeedsAreInstalled() bool {
return c.enforceNeedsAreInstalled
}
func (c configImpl) WriteOutput() bool {
return false
}
type applyConfig struct {
args string
cascade string
@ -2530,6 +2534,7 @@ type applyConfig struct {
trackMode string
trackTimeout int
trackLogs bool
trackFailOnError bool
// template-only options
includeCRDs, skipTests bool
@ -2756,6 +2761,10 @@ func (a applyConfig) TrackLogs() bool {
return a.trackLogs
}
func (a applyConfig) TrackFailOnError() bool {
return a.trackFailOnError
}
func (a applyConfig) Description() string {
return ""
}
@ -2805,8 +2814,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 {
@ -2846,6 +2856,7 @@ func (helm *mockHelmExec) SetHelmBinary(bin string) {
}
func (helm *mockHelmExec) SetEnableLiveOutput(enableLiveOutput bool) {
helm.enableLiveOutput = enableLiveOutput
}
func (helm *mockHelmExec) SetDisableForceUpdate(forceUpdate bool) {
@ -3100,12 +3111,16 @@ func newPostRendererTestApp(t *testing.T, files map[string]string) (*App, *mockH
return app, helm
}
// hasFlagWithValue reports whether flags contains "--flagName value" as adjacent entries.
// 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
}
@ -3162,10 +3177,10 @@ releases:
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 in flags, got %v", flags)
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 in flags, got %v", flags)
t.Errorf("expected --post-renderer-args=--arg2 or --post-renderer-args --arg2 in flags, got %v", flags)
}
})
}
@ -3219,7 +3234,7 @@ releases:
flags := helm.templated[0].flags
if !hasFlagWithValue(flags, "--post-renderer-args", "--cli-arg") {
t.Errorf("expected --post-renderer-args --cli-arg in flags (CLI should override helmDefaults), got %v", flags)
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)
@ -3276,7 +3291,7 @@ releases:
flags := helm.templated[0].flags
if !hasFlagWithValue(flags, "--post-renderer-args", "--release-arg") {
t.Errorf("expected --post-renderer-args --release-arg in flags (release should override CLI), got %v", flags)
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)
@ -4583,6 +4598,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": `

View File

@ -91,6 +91,7 @@ type ApplyConfigProvider interface {
TrackMode() string
TrackTimeout() int
TrackLogs() bool
TrackFailOnError() bool
Description() string
@ -130,6 +131,7 @@ type SyncConfigProvider interface {
TrackMode() string
TrackTimeout() int
TrackLogs() bool
TrackFailOnError() bool
Description() string
@ -242,6 +244,7 @@ type FetchConfigProvider interface {
SkipRefresh() bool
OutputDir() string
OutputDirTemplate() string
WriteOutput() bool
concurrencyConfig
}
@ -325,6 +328,14 @@ type InitConfigProvider interface {
Force() bool
}
type CreateConfigProvider interface {
Name() string
OutputDir() string
Force() bool
loggingConfig
}
type PrintEnvConfigProvider interface {
Output() string
}

145
pkg/app/create.go Normal file
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
pkg/app/create_test.go Normal file
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")
}

View File

@ -69,7 +69,7 @@ func (ld *desiredStateLoader) Load(f string, opts LoadOpts) (*state.HelmState, e
// --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)
fileVals, err := envld.LoadEnvironmentValues(&handler, fileArgs, environment.New(ld.env), ld.env, "")
if err != nil {
return nil, err
}
@ -78,7 +78,7 @@ func (ld *desiredStateLoader) Load(f string, opts LoadOpts) (*state.HelmState, e
// --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)
setVals, err := envld.LoadEnvironmentValues(&handler, setArgs, environment.New(ld.env), ld.env, "")
if err != nil {
return nil, err
}

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"

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
}

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
pkg/config/create.go Normal file
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
pkg/config/create_test.go Normal file
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")
}

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
}

View File

@ -59,6 +59,8 @@ 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
}
@ -216,6 +218,11 @@ 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

View File

@ -646,17 +646,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.

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)

View File

@ -9,8 +9,14 @@ 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/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 +26,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 +194,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 +214,269 @@ 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),
}
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 {
for {
select {
case <-tr.Ready:
t.logger.Debugf("Deployment %s/%s is ready", tr.Namespace, tr.ResourceName)
return nil
case status := <-tr.Failed:
return fmt.Errorf("deployment %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason)
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 {
for {
select {
case <-tr.Ready:
t.logger.Debugf("StatefulSet %s/%s is ready", tr.Namespace, tr.ResourceName)
return nil
case status := <-tr.Failed:
return fmt.Errorf("statefulset %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason)
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 {
for {
select {
case <-tr.Ready:
t.logger.Debugf("DaemonSet %s/%s is ready", tr.Namespace, tr.ResourceName)
return nil
case status := <-tr.Failed:
return fmt.Errorf("daemonset %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason)
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 {
for {
select {
case <-tr.Succeeded:
t.logger.Debugf("Job %s/%s succeeded", tr.Namespace, tr.ResourceName)
return nil
case status := <-tr.Failed:
return fmt.Errorf("job %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason)
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 {
for {
select {
case <-tr.Succeeded:
t.logger.Debugf("Canary %s/%s succeeded", tr.Namespace, tr.ResourceName)
return nil
case status := <-tr.Failed:
return fmt.Errorf("canary %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason)
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) 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 {

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)

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

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
}

View File

@ -1381,3 +1381,96 @@ func TestGenerateTemporaryReleaseValuesFilesWithData_UnknownTypeError(t *testing
require.Error(t, err)
assert.Contains(t, err.Error(), "unexpected type of value")
}
// 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)
}
})
}
}

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"`
}

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

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

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 != "" {
@ -393,6 +459,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{

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

View File

@ -50,6 +50,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
@ -479,6 +485,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
@ -913,6 +921,7 @@ type SyncOpts struct {
TrackMode string
TrackTimeout int
TrackLogs bool
TrackFailOnError bool
Description string
}
@ -1139,10 +1148,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 {
@ -1161,10 +1168,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
}
}
}
@ -1342,6 +1350,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
@ -1389,6 +1399,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.
@ -1623,6 +1655,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
@ -1635,6 +1673,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
@ -3566,7 +3628,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 {
@ -3609,7 +3674,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)
@ -3732,7 +3800,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 {
@ -3762,7 +3834,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

View File

@ -926,8 +926,8 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
},
want: []string{
"--version", "0.1",
"--post-renderer-args", "--arg1",
"--post-renderer-args", "--arg2",
"--post-renderer-args=--arg1",
"--post-renderer-args=--arg2",
"--namespace", "test-namespace",
},
},
@ -949,7 +949,7 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
},
want: []string{
"--version", "0.1",
"--post-renderer-args", "--release-arg",
"--post-renderer-args=--release-arg",
"--namespace", "test-namespace",
},
},
@ -972,7 +972,7 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
},
want: []string{
"--version", "0.1",
"--post-renderer-args", "--release-arg",
"--post-renderer-args=--release-arg",
"--namespace", "test-namespace",
},
},
@ -997,7 +997,7 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
},
want: []string{
"--version", "0.1",
"--post-renderer-args", "--cli-arg",
"--post-renderer-args=--cli-arg",
"--namespace", "test-namespace",
},
},
@ -1022,7 +1022,74 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
},
want: []string{
"--version", "0.1",
"--post-renderer-args", "--release-arg",
"--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",
},
},
@ -1340,8 +1407,8 @@ func TestHelmState_flagsForTemplate(t *testing.T) {
},
want: []string{
"--version", "0.1",
"--post-renderer-args", "--arg1",
"--post-renderer-args", "--arg2",
"--post-renderer-args=--arg1",
"--post-renderer-args=--arg2",
"--namespace", "test-namespace",
},
},
@ -1363,7 +1430,7 @@ func TestHelmState_flagsForTemplate(t *testing.T) {
},
want: []string{
"--version", "0.1",
"--post-renderer-args", "--release-arg",
"--post-renderer-args=--release-arg",
"--namespace", "test-namespace",
},
},
@ -1386,7 +1453,7 @@ func TestHelmState_flagsForTemplate(t *testing.T) {
},
want: []string{
"--version", "0.1",
"--post-renderer-args", "--release-arg",
"--post-renderer-args=--release-arg",
"--namespace", "test-namespace",
},
},
@ -1411,7 +1478,7 @@ func TestHelmState_flagsForTemplate(t *testing.T) {
},
want: []string{
"--version", "0.1",
"--post-renderer-args", "--cli-arg",
"--post-renderer-args=--cli-arg",
"--namespace", "test-namespace",
},
},
@ -1436,7 +1503,29 @@ func TestHelmState_flagsForTemplate(t *testing.T) {
},
want: []string{
"--version", "0.1",
"--post-renderer-args", "--release-arg",
"--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",
},
},
@ -5995,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)
}
})
}
}

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 {

View File

@ -0,0 +1,4 @@
letter: a
only_a: from-a
both_ab: from-a
all_three: from-a

View File

@ -0,0 +1,4 @@
only_b: from-b
both_ab: from-b
both_bc: from-b
all_three: from-b

View File

@ -0,0 +1,3 @@
only_c: from-c
both_bc: from-c
all_three: from-c

View File

@ -0,0 +1,2 @@
cluster:
domain: example.com

View File

@ -0,0 +1,5 @@
cluster:
domain: cluster.local
region: us-east-1
service:
port: 8080

View File

@ -0,0 +1,3 @@
service:
domain: "service.{{ .Values.cluster.domain }}"
port: 8080

View File

@ -0,0 +1,2 @@
cluster:
domain: example.com

View File

@ -0,0 +1,2 @@
service:
domain: "service.{{ .Values.cluster.domain }}"

View File

@ -0,0 +1 @@
value: ~

View File

@ -0,0 +1 @@
value: from-fallback

View File

@ -0,0 +1,4 @@
enabled: false
replicas: 0
name: ""
tags: []

View File

@ -0,0 +1,6 @@
enabled: true
replicas: 3
name: from-fallback
tags:
- a
- b

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)

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -99,6 +99,7 @@ ${kubectl} create namespace ${test_ns} || fail "Could not create namespace ${tes
. ${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
@ -115,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

View File

@ -0,0 +1,39 @@
fetch_write_output_input_dir="${cases_dir}/fetch-write-output/input"
fetch_write_output_tmp=$(mktemp -d)
case_title="fetch with --write-output for air-gapped environments"
test_start "$case_title"
info "Testing helmfile fetch --write-output with local chart"
output=$(${helmfile} -f "${fetch_write_output_input_dir}/helmfile.yaml.gotmpl" fetch --output-dir "${fetch_write_output_tmp}" --write-output 2>/dev/null) \
|| fail "\"helmfile fetch --write-output\" shouldn't fail"
info "Verifying stdout does not contain non-YAML status messages"
echo "${output}" | grep -q "^Charts will be downloaded to:" && fail "stdout should not contain 'Charts will be downloaded to:' (should be on stderr)" || true
info "Verifying output contains YAML document separator"
echo "${output}" | grep -q "^---" || fail "output should contain YAML document separator"
info "Verifying output contains source helmfile reference"
echo "${output}" | grep -q "# Source:" || fail "output should contain source helmfile reference"
info "Verifying output contains release name"
echo "${output}" | grep -q "name: local-chart" || fail "output should contain release name"
info "Verifying output contains updated chart path pointing to output dir"
echo "${output}" | grep -q "chart:" || fail "output should contain chart field"
info "Verifying chart files exist in output directory"
cat "${fetch_write_output_tmp}/helmfile-tests/local-chart/raw/latest/Chart.yaml" || fail "Chart.yaml should exist in fetched output directory"
info "Verifying the chart path in output matches the actual downloaded location"
chart_path=$(echo "${output}" | grep -E "^[[:space:]]+(-[[:space:]]+)?chart:" | head -1 | sed 's/.*chart: *//' | tr -d '"')
if [ ! -f "${chart_path}/Chart.yaml" ]; then
fail "chart path '${chart_path}' from output should point to a directory containing Chart.yaml"
fi
rm -rf "${fetch_write_output_tmp}"
test_pass "$case_title"

View File

@ -0,0 +1,4 @@
releases:
- name: local-chart
chart: ../../../charts/raw
namespace: local-chart

View File

@ -1,5 +1,10 @@
// server.go is a small HTTP server used by the issue-2103 integration test.
// It serves different YAML content based on the "ref" query parameter.
// It is excluded from normal `go test ./...` runs; the integration test builds it
// explicitly via its file path.
//go:build ignore
package main
import (

View File

@ -0,0 +1,62 @@
issue_2515_case_dir="$(cd "${cases_dir}/issue-2515" && pwd)"
issue_2515_tmp=$(mktemp -d)
# Determine the post-renderer argument.
# Helm 3 accepts an executable script; Helm 4 requires a plugin name.
if [ "${HELMFILE_HELM4}" = "1" ]; then
test_start "issue-2515 post-renderer with output-dir-template (Helm 4)"
info "Installing filter post-renderer plugin for Helm 4"
${helm} plugin uninstall filter &>/dev/null || true
${helm} plugin install ${issue_2515_case_dir}/input/helm-plugin-filter ${PLUGIN_INSTALL_FLAGS} || fail "Failed to install filter plugin"
issue_2515_postrenderer_arg="filter"
else
test_start "issue-2515 post-renderer with output-dir-template"
issue_2515_postrenderer_arg="${issue_2515_case_dir}/input/filter.bash"
fi
info "Testing that --post-renderer output is written to files when --output-dir-template is set"
issue_2515_output_dir="${issue_2515_tmp}/output"
${helmfile} -f ${issue_2515_case_dir}/input/helmfile.yaml \
template \
--post-renderer ${issue_2515_postrenderer_arg} \
--output-dir-template "${issue_2515_output_dir}/{{.Release.Name}}" \
&> ${issue_2515_tmp}/log || fail "helmfile template should not fail"
if [ "${HELMFILE_HELM4}" = "1" ]; then
# Helm 4 natively applies --post-renderer to --output-dir output.
# The directory structure may differ from Helm 3 (no guaranteed templates/ subdir),
# so search recursively for any YAML file. Fall back to stdout (log) if no files written.
issue_2515_output_file=$(find "${issue_2515_output_dir}" -maxdepth 5 -type f \( -name '*.yaml' -o -name '*.yml' \) 2>/dev/null | head -n 1)
if [ -z "${issue_2515_output_file}" ]; then
# Helm 4 may write post-rendered output to stdout rather than files
issue_2515_output_file="${issue_2515_tmp}/log"
if ! grep -q "postrendered" "${issue_2515_output_file}"; then
fail "Expected post-rendered YAML (namespace postrendered) in output files under ${issue_2515_output_dir} or stdout. Dir: $(find ${issue_2515_output_dir} 2>/dev/null || echo 'not found'). Log (last 50 lines): $(tail -50 ${issue_2515_output_file})"
fi
fi
else
issue_2515_templates_dir="${issue_2515_output_dir}/issue-2515/templates"
if [ ! -d "${issue_2515_templates_dir}" ]; then
fail "Expected templates directory ${issue_2515_templates_dir} to exist"
fi
issue_2515_output_file=$(find "${issue_2515_templates_dir}" -type f \( -name '*.yaml' -o -name '*.yml' \) | head -n 1)
if [ -z "${issue_2515_output_file}" ]; then
fail "Expected rendered YAML file under ${issue_2515_templates_dir}"
fi
fi
if grep -q "original-cm" "${issue_2515_output_file}"; then
fail "Output should contain post-renderer output (Namespace), not original templates (original-cm). File contents: $(cat ${issue_2515_output_file})"
fi
if ! grep -q "postrendered" "${issue_2515_output_file}"; then
fail "Output should contain post-renderer content (namespace postrendered). File contents: $(cat ${issue_2515_output_file})"
fi
if [ "${HELMFILE_HELM4}" = "1" ]; then
test_pass "issue-2515 post-renderer with output-dir-template (Helm 4)"
else
test_pass "issue-2515 post-renderer with output-dir-template"
fi

View File

@ -0,0 +1,2 @@
#!/usr/bin/env bash
printf -- "---\napiVersion: v1\nkind: Namespace\nmetadata:\n name: postrendered\n"

View File

@ -0,0 +1,6 @@
#!/usr/bin/env bash
# Discard stdin (pre-rendered content) and output a fixed Namespace resource.
# This verifies that the post-renderer output — not the original templates — is
# written to the output directory.
cat > /dev/null
printf -- "---\napiVersion: v1\nkind: Namespace\nmetadata:\n name: postrendered\n"

View File

@ -0,0 +1,8 @@
apiVersion: v1
type: postrenderer/v1
name: filter
version: 0.1.0
runtime: subprocess
runtimeConfig:
platformCommand:
- command: ${HELM_PLUGIN_DIR}/filter.sh

View File

@ -0,0 +1,13 @@
releases:
- name: issue-2515
chart: ../../../charts/raw
values:
- templates:
- |
apiVersion: v1
kind: ConfigMap
metadata:
name: original-cm
namespace: {{ .Release.Namespace }}
data:
key: value

View File

@ -0,0 +1,25 @@
postrender_defaults_args_case_input_dir="${cases_dir}/postrender-defaults-args/input"
postrender_defaults_args_case_output_dir="${cases_dir}/postrender-defaults-args/output"
# Helm 4 requires post-renderers to be plugins
if [ "${HELMFILE_HELM4}" = "1" ]; then
info "Installing echo-args post-renderer plugin for Helm 4"
${helm} plugin uninstall echo-args &>/dev/null || true
${helm} plugin install ${postrender_defaults_args_case_input_dir}/helm-plugin-echo-args ${PLUGIN_INSTALL_FLAGS} || fail "Failed to install echo-args plugin"
fi
config_file="helmfile.yaml.gotmpl"
postrender_defaults_args_tmp=$(mktemp -d)
test_start "postrender-defaults-args template"
info "Running helmfile template with helmDefaults.postRendererArgs containing {{ .Release.Name }}"
${helmfile} -f ${postrender_defaults_args_case_input_dir}/${config_file} template --concurrency 1 &> ${postrender_defaults_args_tmp}/template.out || fail "\"helmfile template\" shouldn't fail"
info "Verifying that helmDefaults.postRendererArgs were templated with release names"
grep -q "name: rendered-arg-foo" ${postrender_defaults_args_tmp}/template.out || fail "Expected postRendererArg 'foo' for release foo, but not found in output"
grep -q "name: rendered-arg-bar" ${postrender_defaults_args_tmp}/template.out || fail "Expected postRendererArg 'bar' for release bar, but not found in output"
info "Verifying that literal template expression was NOT passed"
grep -q "rendered-arg-{{ .Release.Name }}" ${postrender_defaults_args_tmp}/template.out && fail "Template expression was NOT rendered (found literal {{ .Release.Name }})"
test_pass "postrender-defaults-args template"

View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
arg=$1
cat
echo "---"
cat <<EOS
apiVersion: v1
kind: ConfigMap
metadata:
name: rendered-arg-${arg}
data:
arg: ${arg}
EOS

View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -e
arg=$2
input=$(cat)
echo "$input"
echo "---"
cat <<EOS
apiVersion: v1
kind: ConfigMap
metadata:
name: rendered-arg-${arg}
data:
arg: ${arg}
EOS

View File

@ -0,0 +1,10 @@
apiVersion: v1
type: postrenderer/v1
name: echo-args
version: 0.1.0
runtime: subprocess
runtimeConfig:
platformCommand:
- command: ${HELM_PLUGIN_DIR}/echo-args.sh
args:
- ${HELM_POST_RENDERER_ARGS}

View File

@ -0,0 +1,30 @@
helmDefaults:
postRenderer: {{ if eq (env "HELMFILE_HELM4") "1" }}echo-args{{ else }}./echo-args.bash{{ end }}
postRendererArgs:
- "{{ `{{ .Release.Name }}` }}"
releases:
- name: foo
chart: ../../../charts/raw
values:
- templates:
- |
apiVersion: v1
kind: ConfigMap
metadata:
name: {{`{{ .Release.Name }}`}}-1
namespace: {{`{{ .Release.Namespace }}`}}
data:
foo: FOO
- name: bar
chart: ../../../charts/raw
values:
- templates:
- |
apiVersion: v1
kind: ConfigMap
metadata:
name: {{`{{ .Release.Name }}`}}-2
namespace: {{`{{ .Release.Namespace }}`}}
data:
bar: BAR

View File

@ -9,6 +9,7 @@ data:
name: cm2
metadata:
name: cm2
---
# Source: raw/templates/resources.yaml
apiVersion: v1
@ -28,6 +29,7 @@ data:
name: cm1
metadata:
name: cm1
---
# Source: raw/templates/resources.yaml
apiVersion: v1

View File

@ -10,6 +10,7 @@ data:
name: cm2
metadata:
name: cm2
---
# Source: raw/templates/resources.yaml
apiVersion: v1
@ -29,6 +30,7 @@ data:
name: cm1
metadata:
name: cm1
---
# Source: raw/templates/resources.yaml
apiVersion: v1