mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-22 12:05:19 +02:00
Compare commits
72
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfce9cf570 | ||
|
|
0d5311ba4d | ||
|
|
54c5944fce | ||
|
|
d5a354144a | ||
|
|
5d4db7ab89 | ||
|
|
254b2aa617 | ||
|
|
b2644c6ec2 | ||
|
|
8fc23a4d80 | ||
|
|
5c225fd200 | ||
|
|
b71d27e85b | ||
|
|
735c484ff5 | ||
|
|
706ceda804 | ||
|
|
fa878f5ebb | ||
|
|
eeb913472a | ||
|
|
e088d125f6 | ||
|
|
b7f71a0ce3 | ||
|
|
12fb6887c0 | ||
|
|
3216c02743 | ||
|
|
892f2f9604 | ||
|
|
eeec9e17a2 | ||
|
|
f1c86aa9dd | ||
|
|
25cd13e148 | ||
|
|
443cef0dc1 | ||
|
|
c25d16a438 | ||
|
|
000a6fa660 | ||
|
|
226c16b88e | ||
|
|
5ed585e223 | ||
|
|
9263200ed7 | ||
|
|
79baa2b70e | ||
|
|
5d901f8ac1 | ||
|
|
c36d150a3a | ||
|
|
5168ed9187 | ||
|
|
a219b5dbd2 | ||
|
|
ca58090af4 | ||
|
|
58417fd068 | ||
|
|
44c9e8d958 | ||
|
|
a2cc8e97d4 | ||
|
|
ad81e4231e | ||
|
|
88be4012b1 | ||
|
|
c390dadce6 | ||
|
|
0d14c5c0de | ||
|
|
d4a4135f9b | ||
|
|
fff84390b1 | ||
|
|
aa18bd0613 | ||
|
|
f7016d1d39 | ||
|
|
bc0c103d79 | ||
|
|
0d6f3ab160 | ||
|
|
daa43e1081 | ||
|
|
6e7aafd5e5 | ||
|
|
9d1a953ec2 | ||
|
|
5a241f68dc | ||
|
|
0605da273a | ||
|
|
2bf38beab1 | ||
|
|
99351f1010 | ||
|
|
d6353a89df | ||
|
|
d29478328b | ||
|
|
aa5d748600 | ||
|
|
822fa3afef | ||
|
|
7e4c0920da | ||
|
|
de7b58cd13 | ||
|
|
0050dfd79b | ||
|
|
677d374502 | ||
|
|
165e7a394f | ||
|
|
16259008d5 | ||
|
|
c36dbfd417 | ||
|
|
9d6cc4d84f | ||
|
|
3fd00b3683 | ||
|
|
34ead21107 | ||
|
|
2e81265f2e | ||
|
|
d40bfcea6f | ||
|
|
2aaae60524 | ||
|
|
55efdd8a35 |
@@ -1,4 +1,4 @@
|
||||
HELM_VERSION ?= v4.2.4
|
||||
HELM_VERSION ?= v4.3.1
|
||||
KUSTOMIZE_VERSION ?= v5.8.0
|
||||
K8S_VERSION ?= v1.34.0
|
||||
MINIKUBE_VERSION ?= v1.37.0
|
||||
|
||||
+21
-17
@@ -31,13 +31,19 @@ jobs:
|
||||
run: df -h
|
||||
- uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.11.4
|
||||
version: v2.13.2
|
||||
- name: Check for outdated Go patterns (go fix modernizers)
|
||||
run: |
|
||||
# Fails (exit 1) when any `go fix` modernizer produces a diff, i.e. the
|
||||
# code contains outdated Go patterns that `go fix ./...` would rewrite.
|
||||
# Fix locally with: go fix ./... && make fmt
|
||||
go fix -diff ./...
|
||||
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
helm-version: [v3.18.6, v3.21.4, v4.2.4]
|
||||
helm-version: [v3.18.6, v3.22.1, v4.3.1]
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
@@ -67,16 +73,16 @@ jobs:
|
||||
env:
|
||||
HELMFILE_HELM4: ${{ startsWith(matrix.helm-version, 'v4') && '1' || '0' }}
|
||||
- name: Archive built binaries
|
||||
if: matrix.helm-version == 'v4.2.4'
|
||||
if: matrix.helm-version == 'v4.3.1'
|
||||
run: tar -cvf built-binaries.tar helmfile diff-yamls dyff
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: matrix.helm-version == 'v4.2.4'
|
||||
if: matrix.helm-version == 'v4.3.1'
|
||||
with:
|
||||
name: built-binaries-${{ github.run_id }}
|
||||
path: built-binaries.tar
|
||||
retention-days: 1
|
||||
- name: Display built binaries
|
||||
if: matrix.helm-version == 'v4.2.4'
|
||||
if: matrix.helm-version == 'v4.3.1'
|
||||
run: ls -l helmfile diff-yamls dyff
|
||||
|
||||
integration_tests:
|
||||
@@ -96,35 +102,35 @@ jobs:
|
||||
- helm-version: v3.18.6
|
||||
kustomize-version: v5.8.0
|
||||
plugin-secrets-version: 4.7.4
|
||||
plugin-diff-version: 3.15.12
|
||||
plugin-diff-version: 3.15.13
|
||||
extra-helmfile-flags: ''
|
||||
# In case you need to test some optional helmfile features,
|
||||
# enable it via extra-helmfile-flags below.
|
||||
- helm-version: v3.18.6
|
||||
kustomize-version: v5.8.0
|
||||
plugin-secrets-version: 4.7.4
|
||||
plugin-diff-version: 3.15.12
|
||||
plugin-diff-version: 3.15.13
|
||||
extra-helmfile-flags: '--enable-live-output'
|
||||
- helm-version: v3.21.4
|
||||
- helm-version: v3.22.1
|
||||
kustomize-version: v5.8.0
|
||||
plugin-secrets-version: 4.7.4
|
||||
plugin-diff-version: 3.15.12
|
||||
plugin-diff-version: 3.15.13
|
||||
extra-helmfile-flags: ''
|
||||
- helm-version: v3.21.4
|
||||
- helm-version: v3.22.1
|
||||
kustomize-version: v5.8.0
|
||||
plugin-secrets-version: 4.7.4
|
||||
plugin-diff-version: 3.15.12
|
||||
plugin-diff-version: 3.15.13
|
||||
extra-helmfile-flags: '--enable-live-output'
|
||||
# Helmfile now supports both Helm 3.x and Helm 4.x
|
||||
- helm-version: v4.2.4
|
||||
- helm-version: v4.3.1
|
||||
kustomize-version: v5.8.0
|
||||
plugin-secrets-version: 4.7.4
|
||||
plugin-diff-version: 3.15.12
|
||||
plugin-diff-version: 3.15.13
|
||||
extra-helmfile-flags: ''
|
||||
- helm-version: v4.2.4
|
||||
- helm-version: v4.3.1
|
||||
kustomize-version: v5.8.0
|
||||
plugin-secrets-version: 4.7.4
|
||||
plugin-diff-version: 3.15.12
|
||||
plugin-diff-version: 3.15.13
|
||||
extra-helmfile-flags: '--enable-live-output'
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
@@ -146,8 +152,6 @@ jobs:
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: built-binaries-${{ github.run_id }}
|
||||
- name: install semver
|
||||
run: go install github.com/ffurrer2/semver/v2/cmd/semver@v2.12.0
|
||||
- name: Extract tar to get built binaries
|
||||
run: tar -xvf built-binaries.tar
|
||||
- name: Display built binaries
|
||||
|
||||
@@ -45,3 +45,24 @@ jobs:
|
||||
with:
|
||||
version: latest
|
||||
args: release --clean ${{ env.SNAPSHOT }}
|
||||
|
||||
# Publish a signed packslip (https://packslip.dev) listing each archive's
|
||||
# digest, the helmfile executable inside it, its shell completions, and the
|
||||
# skill in skills/helmfile, signed with this workflow's identity so
|
||||
# installers can verify a download without a key this project must hold.
|
||||
packslip:
|
||||
needs: goreleaser
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- uses: jdx/packslip@v1
|
||||
with:
|
||||
download: helmfile_*.tar.gz
|
||||
bin: helmfile
|
||||
resources: |
|
||||
completion/bash,zsh,fish,powershell=exec:helmfile completion {shell}
|
||||
skill/helmfile=repo:skills/helmfile
|
||||
|
||||
@@ -49,6 +49,7 @@ linters:
|
||||
goconst:
|
||||
min-len: 3
|
||||
min-occurrences: 8
|
||||
ignore-tests: true
|
||||
gocritic:
|
||||
settings:
|
||||
captLocal:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
### Essential Setup
|
||||
```bash
|
||||
# Check Go version (requires 1.26.2+)
|
||||
# Check Go version (requires 1.27.1+)
|
||||
go version
|
||||
|
||||
# Check Helm dependency (required at runtime)
|
||||
@@ -39,6 +39,10 @@ make fmt
|
||||
|
||||
# Run golangci-lint
|
||||
golangci-lint run
|
||||
|
||||
# Detect outdated Go patterns (fails if `go fix` would rewrite anything;
|
||||
# enforced in CI)
|
||||
make check-modernize
|
||||
```
|
||||
|
||||
### Testing Commands
|
||||
|
||||
@@ -2,7 +2,14 @@
|
||||
|
||||
### Added
|
||||
|
||||
- Add opt-in OpenTelemetry tracing and metrics (`--otel-tracing` / `HELMFILE_OTEL_TRACING`, experimental): command/state-load/release/hook spans with one span per helm subprocess, plus `helmfile.helm.exec.duration`, `helmfile.release.duration`, and `helmfile.release.count` metrics (per-release dimensions opt-in via `HELMFILE_OTEL_METRICS_PER_RELEASE`), exported via standard `OTEL_*` environment variables. See [docs/otel.md](docs/otel.md) and the [design proposal](docs/proposals/otel-tracing.md) (#2767)
|
||||
- Add support for `conditionTemplate` and allow `condition` to be set directly to `true` or `false`.
|
||||
- Add `--allow-failed-releases` global flag to continue preparing charts for the remaining releases when chart preparation fails for a release; failed releases are skipped and all failures are reported at the end (#2616)
|
||||
- Add the `dir=` selector for path-based release filtering and sub-helmfile traversal skip (#2609)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- The label key `dir` is now reserved for the `dir=` selector. User-defined `dir` labels in `commonLabels` or release `labels` log a warning and are ignored for selector matching; they will be rejected with an error in a future release (#2609)
|
||||
|
||||
## [1.4.1] - 2026-03-03
|
||||
|
||||
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
|
||||
FROM --platform=$BUILDPLATFORM golang:1.27-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache make git
|
||||
WORKDIR /workspace/helmfile
|
||||
@@ -30,7 +30,7 @@ ENV HELM_CONFIG_HOME="${HELM_CONFIG_HOME}"
|
||||
ARG HELM_DATA_HOME="${HOME}/.local/share/helm"
|
||||
ENV HELM_DATA_HOME="${HELM_DATA_HOME}"
|
||||
|
||||
ARG HELM_VERSION="v4.2.4"
|
||||
ARG HELM_VERSION="v4.3.1"
|
||||
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="c306b46f719b0a4da32d0f78ee21bf90ce8d602f15b22ab753f0674d1670a7f3" ;; \
|
||||
"linux/arm64") HELM_SHA256="564de2191b881e9f71b5606b25345821ea1682f06ab90499d3ab22b530176da1" ;; \
|
||||
"linux/amd64") HELM_SHA256="0403a88ae20913d11b10a2467b50087721fea7581736da6b9d3992f5357fe54b" ;; \
|
||||
"linux/arm64") HELM_SHA256="8cf8f66ecbc252ac8f7f06cca68aba88508ddfde3a6a34feb1678ad1694b4f88" ;; \
|
||||
esac && \
|
||||
echo "${HELM_SHA256} ${HELM_FILENAME}" | sha256sum -c && \
|
||||
echo Extracting ${HELM_FILENAME}... && \
|
||||
@@ -95,7 +95,7 @@ RUN set -x && \
|
||||
[ "$(age-keygen --version)" = "${AGE_VERSION}" ]
|
||||
|
||||
ARG HELM_SECRETS_VERSION="4.7.4"
|
||||
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.15.12 --verify=false && \
|
||||
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.15.13 --verify=false && \
|
||||
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-${HELM_SECRETS_VERSION}.tgz --verify=false && \
|
||||
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-getter-${HELM_SECRETS_VERSION}.tgz --verify=false && \
|
||||
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-post-renderer-${HELM_SECRETS_VERSION}.tgz --verify=false && \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
|
||||
FROM --platform=$BUILDPLATFORM golang:1.27-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache make git
|
||||
WORKDIR /workspace/helmfile
|
||||
@@ -38,7 +38,7 @@ ENV HELM_CONFIG_HOME="${HELM_CONFIG_HOME}"
|
||||
ARG HELM_DATA_HOME="${HOME}/.local/share/helm"
|
||||
ENV HELM_DATA_HOME="${HELM_DATA_HOME}"
|
||||
|
||||
ARG HELM_VERSION="v4.2.4"
|
||||
ARG HELM_VERSION="v4.3.1"
|
||||
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="c306b46f719b0a4da32d0f78ee21bf90ce8d602f15b22ab753f0674d1670a7f3" ;; \
|
||||
"linux/arm64") HELM_SHA256="564de2191b881e9f71b5606b25345821ea1682f06ab90499d3ab22b530176da1" ;; \
|
||||
"linux/amd64") HELM_SHA256="0403a88ae20913d11b10a2467b50087721fea7581736da6b9d3992f5357fe54b" ;; \
|
||||
"linux/arm64") HELM_SHA256="8cf8f66ecbc252ac8f7f06cca68aba88508ddfde3a6a34feb1678ad1694b4f88" ;; \
|
||||
esac && \
|
||||
echo "${HELM_SHA256} ${HELM_FILENAME}" | sha256sum -c && \
|
||||
echo Extracting ${HELM_FILENAME}... && \
|
||||
@@ -104,7 +104,7 @@ RUN set -x && \
|
||||
[ "$(age-keygen --version)" = "${AGE_VERSION}" ]
|
||||
|
||||
ARG HELM_SECRETS_VERSION="4.7.4"
|
||||
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.15.12 --verify=false && \
|
||||
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.15.13 --verify=false && \
|
||||
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-${HELM_SECRETS_VERSION}.tgz --verify=false && \
|
||||
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-getter-${HELM_SECRETS_VERSION}.tgz --verify=false && \
|
||||
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-post-renderer-${HELM_SECRETS_VERSION}.tgz --verify=false && \
|
||||
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
|
||||
FROM --platform=$BUILDPLATFORM golang:1.27-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache make git
|
||||
WORKDIR /workspace/helmfile
|
||||
@@ -38,7 +38,7 @@ ENV HELM_CONFIG_HOME="${HELM_CONFIG_HOME}"
|
||||
ARG HELM_DATA_HOME="${HOME}/.local/share/helm"
|
||||
ENV HELM_DATA_HOME="${HELM_DATA_HOME}"
|
||||
|
||||
ARG HELM_VERSION="v4.2.4"
|
||||
ARG HELM_VERSION="v4.3.1"
|
||||
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="c306b46f719b0a4da32d0f78ee21bf90ce8d602f15b22ab753f0674d1670a7f3" ;; \
|
||||
"linux/arm64") HELM_SHA256="564de2191b881e9f71b5606b25345821ea1682f06ab90499d3ab22b530176da1" ;; \
|
||||
"linux/amd64") HELM_SHA256="0403a88ae20913d11b10a2467b50087721fea7581736da6b9d3992f5357fe54b" ;; \
|
||||
"linux/arm64") HELM_SHA256="8cf8f66ecbc252ac8f7f06cca68aba88508ddfde3a6a34feb1678ad1694b4f88" ;; \
|
||||
esac && \
|
||||
echo "${HELM_SHA256} ${HELM_FILENAME}" | sha256sum -c && \
|
||||
echo Extracting ${HELM_FILENAME}... && \
|
||||
@@ -104,7 +104,7 @@ RUN set -x && \
|
||||
[ "$(age-keygen --version)" = "${AGE_VERSION}" ]
|
||||
|
||||
ARG HELM_SECRETS_VERSION="4.7.4"
|
||||
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.15.12 --verify=false && \
|
||||
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.15.13 --verify=false && \
|
||||
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-${HELM_SECRETS_VERSION}.tgz --verify=false && \
|
||||
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-getter-${HELM_SECRETS_VERSION}.tgz --verify=false && \
|
||||
helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-post-renderer-${HELM_SECRETS_VERSION}.tgz --verify=false && \
|
||||
|
||||
@@ -172,6 +172,23 @@ releases:
|
||||
trackLogs: true # Show pod logs during tracking
|
||||
```
|
||||
|
||||
### trackLogsUntilReady
|
||||
|
||||
Limit pod log output to startup while continuing readiness and failure tracking:
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
- name: my-app
|
||||
trackMode: kubedog
|
||||
trackTimeout: 600
|
||||
trackLogs: true
|
||||
trackLogsUntilReady: true
|
||||
```
|
||||
|
||||
Each pod stops printing routine logs once it first becomes ready, even while other replicas are still starting. Logs captured before readiness are flushed. The same pod does not resume routine log output if it later becomes unready or fails. Job logging is unchanged. The default is `false`, which keeps full streaming when `trackLogs` is enabled. This option does not change `trackFailedLogs` when `trackLogs` is disabled.
|
||||
|
||||
The equivalent CLI flags are `--track-logs --track-logs-until-ready` with `--track-mode kubedog`.
|
||||
|
||||
### trackKinds / skipKinds
|
||||
|
||||
Control which resource types to track:
|
||||
|
||||
@@ -38,6 +38,12 @@ check:
|
||||
go vet ${PKGS}
|
||||
.PHONY: check
|
||||
|
||||
# check-modernize fails (exit 1) when any `go fix` modernizer would rewrite code,
|
||||
# i.e. the codebase contains outdated Go patterns. Run `go fix ./...` to fix, then `make fmt`.
|
||||
check-modernize:
|
||||
go fix -diff ./...
|
||||
.PHONY: check-modernize
|
||||
|
||||
build-test-tools:
|
||||
go build test/diff-yamls/diff-yamls.go
|
||||
curl --progress-bar --location https://github.com/homeport/dyff/releases/download/v1.5.6/dyff_1.5.6_linux_amd64.tar.gz | tar -xzf - -C `pwd` dyff
|
||||
|
||||
+5
-1
@@ -9,13 +9,14 @@ import (
|
||||
|
||||
// NewApplyCmd returns apply subcmd
|
||||
func NewApplyCmd(globalCfg *config.GlobalImpl) *cobra.Command {
|
||||
applyOptions := &config.ApplyOptions{}
|
||||
applyOptions := config.NewApplyOptions()
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "apply",
|
||||
Short: "Apply all resources from state file only when there are changes",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
applyImpl := config.NewApplyImpl(globalCfg, applyOptions)
|
||||
applyImpl.Cmd = cmd
|
||||
|
||||
err := config.NewCLIConfigImpl(applyImpl.GlobalImpl)
|
||||
if err != nil {
|
||||
@@ -74,7 +75,10 @@ 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 (all pods)")
|
||||
f.BoolVar(&applyOptions.TrackLogsUntilReady, "track-logs-until-ready", false, "Stop pod log streaming when each pod becomes ready (requires --track-logs)")
|
||||
f.BoolVar(&applyOptions.TrackFailedLogs, "track-failed-logs", false, "Enable log streaming with kubedog tracking, but only emit logs for pods that enter a failed state. Overridden by --track-logs when both are set")
|
||||
f.DurationVar(&applyOptions.TrackLogsInterval, "track-logs-interval", applyOptions.TrackLogsInterval,
|
||||
"Interval between kubedog log output updates (minimum 1s); overrides trackLogsInterval")
|
||||
f.IntVar(&applyOptions.HelmStuckGrace, "helm-stuck-grace", 0, "When using --track-mode kubedog: if the cluster confirms all tracked resources have converged but the helm subprocess is still running, wait this many seconds before sending SIGINT to helm. Recovers from helm v4 hook waiter wedges. May leave the release secret in pending-install state requiring manual cleanup. 0 disables.")
|
||||
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"`)
|
||||
|
||||
+97
-5
@@ -4,9 +4,12 @@ import (
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.szostok.io/version/extension"
|
||||
"go.uber.org/zap"
|
||||
|
||||
@@ -15,8 +18,11 @@ import (
|
||||
"github.com/helmfile/helmfile/pkg/config"
|
||||
"github.com/helmfile/helmfile/pkg/envvar"
|
||||
"github.com/helmfile/helmfile/pkg/errors"
|
||||
"github.com/helmfile/helmfile/pkg/filesystem"
|
||||
"github.com/helmfile/helmfile/pkg/helmexec"
|
||||
"github.com/helmfile/helmfile/pkg/runtime"
|
||||
"github.com/helmfile/helmfile/pkg/state"
|
||||
"github.com/helmfile/helmfile/pkg/telemetry"
|
||||
)
|
||||
|
||||
var logger *zap.SugaredLogger
|
||||
@@ -24,8 +30,7 @@ var globalUsage = "Declaratively deploy your Kubernetes manifests, Kustomize con
|
||||
|
||||
func toCLIError(g *config.GlobalImpl, err error) error {
|
||||
if err != nil {
|
||||
var exitErr helmexec.ExitError
|
||||
if stderrors.As(err, &exitErr) {
|
||||
if exitErr, ok := stderrors.AsType[helmexec.ExitError](err); ok {
|
||||
return errors.NewExitError(exitErr.Error(), exitErr.ExitStatus())
|
||||
}
|
||||
switch e := err.(type) {
|
||||
@@ -75,6 +80,19 @@ func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) {
|
||||
}
|
||||
logger = helmexec.NewLogger(logOut, logLevel)
|
||||
globalConfig.SetLogger(logger)
|
||||
|
||||
// OpenTelemetry tracing (experimental). Setup and StartCommandSpan
|
||||
// are no-ops when tracing is disabled, so the default path is
|
||||
// unchanged. Configuration beyond the on/off switch comes from the
|
||||
// standard OTEL_* environment variables.
|
||||
cmdName := "helmfile " + c.Name()
|
||||
telemetry.Setup(c.Context(), telemetry.Options{
|
||||
Enabled: globalImpl.OtelTracing(),
|
||||
Version: version.Version(),
|
||||
Logger: logger,
|
||||
})
|
||||
telemetry.StartCommandSpan(cmdName, commandSpanAttributes(cmdName, globalImpl)...)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -83,6 +101,10 @@ func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) {
|
||||
// Set the global options for the root command.
|
||||
setGlobalOptionsForRootCmd(flags, globalConfig)
|
||||
|
||||
if err := cmd.RegisterFlagCompletionFunc("selector", selectorFlagCompletion(globalConfig)); err != nil {
|
||||
return nil, fmt.Errorf("registering selector flag completion: %w", err)
|
||||
}
|
||||
|
||||
flags.ParseErrorsAllowlist.UnknownFlags = true
|
||||
|
||||
// when set environment HELMFILE_UPGRADE_NOTICE_DISABLED any value, skip upgrade notice.
|
||||
@@ -121,16 +143,82 @@ func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) {
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
// commandSpanAttributes builds the root-span attributes from the resolved
|
||||
// global options. Values that can be overridden per release appear again on
|
||||
// child spans; the service identity (service.name/service.version) lives on
|
||||
// the OTel resource, not on spans.
|
||||
func commandSpanAttributes(cmdName string, g *config.GlobalImpl) []attribute.KeyValue {
|
||||
return []attribute.KeyValue{
|
||||
attribute.String("helmfile.command", cmdName),
|
||||
// FileOrDir may be a remote go-getter reference with credentials.
|
||||
attribute.String("helmfile.file", helmexec.RedactedRef(g.FileOrDir())),
|
||||
attribute.String("helmfile.environment", g.Env()),
|
||||
attribute.StringSlice("helmfile.selectors", g.Selectors()),
|
||||
}
|
||||
}
|
||||
|
||||
// selectorFlagCompletion provides shell completion for `-l/--selector`.
|
||||
// When the user types a value whose last comma-separated condition starts with
|
||||
// `dir=`, this enumerates directories matching the partial path after `=`,
|
||||
// relative to the root helmfile directory like the selector itself.
|
||||
// Other selector keys/values are too open-ended to enumerate, so the function
|
||||
// suppresses default file completion in that case.
|
||||
func selectorFlagCompletion(globalOptions *config.GlobalOptions) cobra.CompletionFunc {
|
||||
return func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
const dirPrefix = state.DirLabel + "="
|
||||
|
||||
groups := strings.Split(toComplete, ",")
|
||||
last := groups[len(groups)-1]
|
||||
if !strings.HasPrefix(last, dirPrefix) {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
leading := strings.Join(groups[:len(groups)-1], ",")
|
||||
if leading != "" {
|
||||
leading += ","
|
||||
}
|
||||
|
||||
partial := filepath.FromSlash(strings.TrimPrefix(last, dirPrefix))
|
||||
if partial != "" && !filepath.IsLocal(partial) {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
fileOrDir := config.NewGlobalImpl(globalOptions).FileOrDir()
|
||||
rootDir, err := app.DirSelectorRoot(filesystem.DefaultFileSystem(), fileOrDir)
|
||||
if err != nil || rootDir == "" {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
scanDir, matchPrefix := filepath.Split(partial)
|
||||
|
||||
entries, err := os.ReadDir(filepath.Join(rootDir, scanDir))
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
suggestions := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() || !strings.HasPrefix(entry.Name(), matchPrefix) {
|
||||
continue
|
||||
}
|
||||
suggestions = append(suggestions, leading+dirPrefix+filepath.ToSlash(filepath.Join(scanDir, entry.Name())))
|
||||
}
|
||||
|
||||
return suggestions, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
func setGlobalOptionsForRootCmd(fs *pflag.FlagSet, globalOptions *config.GlobalOptions) {
|
||||
fs.StringVarP(&globalOptions.HelmBinary, "helm-binary", "b", "", fmt.Sprintf(`Path to the helm binary. Overrides "HELMFILE_HELM_BINARY" OS environment variable when specified (default %q)`, app.DefaultHelmBinary))
|
||||
fs.StringVarP(&globalOptions.KustomizeBinary, "kustomize-binary", "k", "", fmt.Sprintf(`Path to the kustomize binary. Overrides "HELMFILE_KUSTOMIZE_BINARY" OS environment variable when specified (default %q)`, app.DefaultKustomizeBinary))
|
||||
fs.StringVarP(&globalOptions.File, "file", "f", "", "load config from file or directory. defaults to \"`helmfile.yaml`\" or \"helmfile.yaml.gotmpl\" or \"helmfile.d\" (means \"helmfile.d/*.yaml\" or \"helmfile.d/*.yaml.gotmpl\") in this preference. Specify - to load the config from the standard input.")
|
||||
fs.StringVarP(&globalOptions.Environment, "environment", "e", "", `specify the environment name. Overrides "HELMFILE_ENVIRONMENT" OS environment variable when specified. defaults to "default"`)
|
||||
fs.StringArrayVar(&globalOptions.StateValuesSet, "state-values-set", nil, "set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2). Used to override .Values within the helmfile template (not values template).")
|
||||
fs.StringArrayVar(&globalOptions.StateValuesSetString, "state-values-set-string", nil, "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).")
|
||||
fs.StringArrayVar(&globalOptions.StateValuesSet, "state-values-set", nil, "set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2; escape a comma in a value with \\,). Used to override .Values within the helmfile template (not values template).")
|
||||
fs.StringArrayVar(&globalOptions.StateValuesSetString, "state-values-set-string", nil, "set state STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2; escape a comma in a value with \\,). Used to override .Values within the helmfile template (not values template).")
|
||||
fs.StringArrayVar(&globalOptions.StateValuesFile, "state-values-file", nil, "specify state values in a YAML file. Used to override .Values within the helmfile template (not values template).")
|
||||
fs.BoolVar(&globalOptions.SkipDeps, "skip-deps", false, `skip running "helm repo update" and "helm dependency build"`)
|
||||
fs.BoolVar(&globalOptions.SkipRefresh, "skip-refresh", false, `skip running "helm repo update"`)
|
||||
fs.BoolVar(&globalOptions.AllowFailedReleases, "allow-failed-releases", false, `continue preparing charts for other releases when chart preparation fails for a release; failed releases are skipped and all failures are reported at the end`)
|
||||
fs.BoolVar(&globalOptions.StripArgsValuesOnExitError, "strip-args-values-on-exit-error", true, `Strip the potential secret values of the helm command args contained in a helmfile error message`)
|
||||
fs.BoolVar(&globalOptions.DisableForceUpdate, "disable-force-update", false, `do not force helm repos to update when executing "helm repo add" (Helm 3 only)`)
|
||||
fs.BoolVar(&globalOptions.EnforcePluginVerification, "enforce-plugin-verification", false, `fail plugin installation if verification is not supported (for security purposes)`)
|
||||
@@ -154,7 +242,8 @@ func setGlobalOptionsForRootCmd(fs *pflag.FlagSet, globalOptions *config.GlobalO
|
||||
fs.StringArrayVarP(&globalOptions.Selector, "selector", "l", nil, `Only run using the releases that match labels. Labels can take the form of foo=bar or foo!=bar.
|
||||
A release must match all labels in a group in order to be used. Multiple groups can be specified at once.
|
||||
"--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"`)
|
||||
The name of a release can be used as a label: "--selector name=myrelease".
|
||||
The "dir" key matches by directory prefix of the defining helmfile and skips non-matching sub-helmfiles; see https://helmfile.readthedocs.io/en/latest/#the-dir-selector`)
|
||||
fs.BoolVar(&globalOptions.AllowNoMatchingRelease, "allow-no-matching-release", false, `Do not exit with an error code if the provided selector has no matching releases.`)
|
||||
fs.BoolVar(&globalOptions.EnableLiveOutput, "enable-live-output", globalOptions.EnableLiveOutput, `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.`)
|
||||
@@ -163,6 +252,9 @@ It only applies for the Helm CLI commands, Stdout/Stderr for Hooks are still dis
|
||||
Useful when file order matters for dependencies (e.g., databases before applications).
|
||||
When processing multiple files, paths are resolved without changing the process working directory,
|
||||
so relative environment variables like KUBECONFIG work correctly.`)
|
||||
fs.BoolVar(&globalOptions.OtelTracing, "otel-tracing", globalOptions.OtelTracing, `Enable OpenTelemetry tracing and metrics (experimental).
|
||||
Configure exporters with standard OTEL_* environment variables (e.g. OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_TRACES_EXPORTER, OTEL_METRICS_EXPORTER).
|
||||
Overrides "HELMFILE_OTEL_TRACING" OS environment variable when specified. See docs/otel.md`)
|
||||
// avoid 'pflag: help requested' error (#251)
|
||||
fs.BoolP("help", "h", false, "help for helmfile")
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/config"
|
||||
"github.com/helmfile/helmfile/pkg/errors"
|
||||
@@ -70,3 +75,149 @@ func TestToCLIError(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootCmdRegistersOtelTracingFlag(t *testing.T) {
|
||||
rootCmd, err := NewRootCmd(&config.GlobalOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
flag := rootCmd.PersistentFlags().Lookup("otel-tracing")
|
||||
require.NotNil(t, flag, "--otel-tracing flag should be registered")
|
||||
assert.Equal(t, "bool", flag.Value.Type())
|
||||
assert.Equal(t, "false", flag.DefValue)
|
||||
assert.Contains(t, flag.Usage, "HELMFILE_OTEL_TRACING")
|
||||
}
|
||||
|
||||
func TestTrackLogsUntilReadyFlag(t *testing.T) {
|
||||
for _, command := range []string{"sync", "apply"} {
|
||||
t.Run(command, func(t *testing.T) {
|
||||
rootCmd, err := NewRootCmd(&config.GlobalOptions{})
|
||||
require.NoError(t, err)
|
||||
cmd, _, err := rootCmd.Find([]string{command})
|
||||
require.NoError(t, err)
|
||||
|
||||
flag := cmd.Flags().Lookup("track-logs-until-ready")
|
||||
require.NotNil(t, flag)
|
||||
assert.Equal(t, "false", flag.DefValue)
|
||||
require.NoError(t, cmd.ParseFlags([]string{"--track-logs", "--track-logs-until-ready"}))
|
||||
enabled, err := cmd.Flags().GetBool("track-logs-until-ready")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, enabled)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectorFlagCompletion_NonDirKey(t *testing.T) {
|
||||
// For any selector value that is not a dir= prefix, completion should
|
||||
// suppress file completion and return no suggestions.
|
||||
cases := []string{"", "name=", "name=foo", "tier=backend,name", "dir"}
|
||||
for _, in := range cases {
|
||||
t.Run(in, func(t *testing.T) {
|
||||
suggestions, dir := selectorFlagCompletion(&config.GlobalOptions{})(nil, nil, in)
|
||||
assert.Nil(t, suggestions)
|
||||
assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, dir)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectorFlagCompletion_DirEnumeratesDirectories(t *testing.T) {
|
||||
// Set up a temp tree with two subdirs and one file. Completion against
|
||||
// `dir=` should suggest only the subdirs, prefixed with `dir=`.
|
||||
root := t.TempDir()
|
||||
for _, sub := range []string{"apps", "infra"} {
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(root, sub), 0o755))
|
||||
}
|
||||
require.NoError(t, os.WriteFile(filepath.Join(root, "helmfile.yaml"), []byte("releases: []"), 0o644))
|
||||
|
||||
t.Chdir(root)
|
||||
|
||||
suggestions, _ := selectorFlagCompletion(&config.GlobalOptions{})(nil, nil, "dir=")
|
||||
assert.ElementsMatch(t, []string{"dir=apps", "dir=infra"}, suggestions)
|
||||
}
|
||||
|
||||
func TestSelectorFlagCompletion_DirPartialPath(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
for _, sub := range []string{"apps/opencloud", "apps/openproject", "apps/xwiki"} {
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(root, sub), 0o755))
|
||||
}
|
||||
t.Chdir(root)
|
||||
|
||||
suggestions, _ := selectorFlagCompletion(&config.GlobalOptions{})(nil, nil, "dir=apps/op")
|
||||
assert.ElementsMatch(t, []string{"dir=apps/opencloud", "dir=apps/openproject"}, suggestions)
|
||||
}
|
||||
|
||||
func TestSelectorFlagCompletion_DirCarriesOverPriorGroups(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(root, "apps"), 0o755))
|
||||
t.Chdir(root)
|
||||
|
||||
suggestions, _ := selectorFlagCompletion(&config.GlobalOptions{})(nil, nil, "name=foo,dir=")
|
||||
assert.ElementsMatch(t, []string{"name=foo,dir=apps"}, suggestions)
|
||||
}
|
||||
|
||||
func TestSelectorFlagCompletion_UnmatchableValuesReturnNoSuggestions(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(root, "apps"), 0o755))
|
||||
t.Chdir(filepath.Join(root, "apps"))
|
||||
|
||||
for _, in := range []string{"dir=does/not/exist/", "dir=" + filepath.ToSlash(root) + "/", "dir=../", "dir=../ap"} {
|
||||
t.Run(in, func(t *testing.T) {
|
||||
suggestions, dir := selectorFlagCompletion(&config.GlobalOptions{})(nil, nil, in)
|
||||
assert.Nil(t, suggestions)
|
||||
assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, dir)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectorFlagCompletion_DirIsRelativeToRootHelmfile(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
for _, sub := range []string{"deploy/apps/opencloud", "deploy/infra", "unrelated"} {
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(root, sub), 0o755))
|
||||
}
|
||||
require.NoError(t, os.WriteFile(filepath.Join(root, "deploy", "helmfile.yaml"), []byte("releases: []"), 0o644))
|
||||
t.Chdir(root)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
file string
|
||||
envFile string
|
||||
toComplete string
|
||||
want []string
|
||||
}{
|
||||
{name: "file flag", file: "deploy/helmfile.yaml", toComplete: "dir=", want: []string{"dir=apps", "dir=infra"}},
|
||||
{name: "file flag with nested partial", file: "deploy/helmfile.yaml", toComplete: "dir=apps/", want: []string{"dir=apps/opencloud"}},
|
||||
{name: "directory flag", file: "deploy", toComplete: "dir=", want: []string{"dir=apps", "dir=infra"}},
|
||||
{name: "absolute file flag", file: filepath.Join(root, "deploy", "helmfile.yaml"), toComplete: "dir=i", want: []string{"dir=infra"}},
|
||||
{name: "file from environment", envFile: "deploy/helmfile.yaml", toComplete: "dir=", want: []string{"dir=apps", "dir=infra"}},
|
||||
{name: "no file uses working directory", toComplete: "dir=", want: []string{"dir=deploy", "dir=unrelated"}},
|
||||
{name: "remote file has no local root", file: "git::https://github.com/example/repo.git@helmfile.yaml?ref=main", toComplete: "dir=", want: nil},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Setenv("HELMFILE_FILE_PATH", tt.envFile)
|
||||
|
||||
suggestions, _ := selectorFlagCompletion(&config.GlobalOptions{File: tt.file})(nil, nil, tt.toComplete)
|
||||
assert.ElementsMatch(t, tt.want, suggestions)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectorFlagCompletion_ThroughRootCmdHonorsFileFlag(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
for _, sub := range []string{"deploy/apps", "unrelated"} {
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(root, sub), 0o755))
|
||||
}
|
||||
t.Chdir(root)
|
||||
t.Setenv("HELMFILE_FILE_PATH", "")
|
||||
|
||||
rootCmd, err := NewRootCmd(&config.GlobalOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
var out bytes.Buffer
|
||||
rootCmd.SetOut(&out)
|
||||
rootCmd.SetArgs([]string{cobra.ShellCompNoDescRequestCmd, "--file", "deploy/helmfile.yaml", "sync", "--selector", "dir="})
|
||||
require.NoError(t, rootCmd.Execute())
|
||||
|
||||
assert.Contains(t, out.String(), "dir=apps\n")
|
||||
assert.NotContains(t, out.String(), "dir=deploy")
|
||||
assert.NotContains(t, out.String(), "dir=unrelated")
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ func NewSyncCmd(globalCfg *config.GlobalImpl) *cobra.Command {
|
||||
Short: "Sync releases defined in state file",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
syncImpl := config.NewSyncImpl(globalCfg, syncOptions)
|
||||
syncImpl.Cmd = cmd
|
||||
|
||||
err := config.NewCLIConfigImpl(syncImpl.GlobalImpl)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -58,7 +60,10 @@ 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 (all pods)")
|
||||
f.BoolVar(&syncOptions.TrackLogsUntilReady, "track-logs-until-ready", false, "Stop pod log streaming when each pod becomes ready (requires --track-logs)")
|
||||
f.BoolVar(&syncOptions.TrackFailedLogs, "track-failed-logs", false, "Enable log streaming with kubedog tracking, but only emit logs for pods that enter a failed state. Overridden by --track-logs when both are set")
|
||||
f.DurationVar(&syncOptions.TrackLogsInterval, "track-logs-interval", syncOptions.TrackLogsInterval,
|
||||
"Interval between kubedog log output updates (minimum 1s); overrides trackLogsInterval")
|
||||
f.IntVar(&syncOptions.HelmStuckGrace, "helm-stuck-grace", 0, "When using --track-mode kubedog: if the cluster confirms all tracked resources have converged but the helm subprocess is still running, wait this many seconds before sending SIGINT to helm. Recovers from helm v4 hook waiter wedges. May leave the release secret in pending-install state requiring manual cleanup. 0 disables.")
|
||||
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"`)
|
||||
|
||||
@@ -26,14 +26,47 @@ releases:
|
||||
Or use command-line flags:
|
||||
|
||||
```bash
|
||||
helmfile apply --track-mode kubedog --track-timeout 300 --track-logs
|
||||
helmfile apply --track-mode kubedog --track-timeout 300 --track-logs --track-logs-interval 1s
|
||||
```
|
||||
|
||||
### 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
|
||||
- **`trackLogs`**: Print logs from tracked resources during deployment
|
||||
- **`trackLogsInterval`**: Interval between log output updates, as a duration such as `1s` or `30s` (default: `10s`; minimum: `1s`)
|
||||
- **`trackLogsUntilReady`**: With `trackLogs: true`, stop routine pod logs once each pod first becomes ready (default: false)
|
||||
- **`trackFailedLogs`**: Print collected logs only for pods that fail during deployment
|
||||
|
||||
To see logs only for failed pods, set `trackFailedLogs: true` on a release or use `--track-failed-logs` with `helmfile apply` or `helmfile sync`. If both log options are enabled, logs from all tracked pods are printed.
|
||||
|
||||
With kubedog tracking and either `trackLogs` or `trackFailedLogs` enabled, `helmfile apply` and `helmfile sync` print collected logs every 10 seconds by default. Set `trackLogsInterval` on a release or use `--track-logs-interval` to change this interval (for example, `1s` above). An explicitly supplied command-line flag overrides the release setting; otherwise, the release setting takes precedence over the `10s` default. The minimum is `1s`. Deployment progress is checked for changes every 10 seconds.
|
||||
|
||||
### Startup-Only Pod Logs
|
||||
|
||||
Use `trackLogsUntilReady: true` to show startup logs without continuing to print application traffic from ready pods while other replicas are still starting:
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
- name: myapp
|
||||
chart: ./charts/myapp
|
||||
trackMode: kubedog
|
||||
trackTimeout: 600
|
||||
trackLogs: true
|
||||
trackLogsUntilReady: true
|
||||
```
|
||||
|
||||
Or use command-line flags:
|
||||
|
||||
```bash
|
||||
helmfile apply --track-mode kubedog --track-timeout 600 --track-logs --track-logs-until-ready
|
||||
```
|
||||
|
||||
Logs captured before readiness are flushed. Log output remains limited to startup if the same pod later becomes unready or fails; readiness and failure tracking continue for every pod. Job logs retain their existing behavior.
|
||||
|
||||
Kubernetes readiness timestamps have second precision, so logs from the readiness second are retained to avoid losing startup output.
|
||||
|
||||
The option defaults to `false`, so `trackLogs: true` continues printing logs throughout tracking. It only applies when `trackLogs` is enabled; `trackFailedLogs: true` alone keeps its existing failed-only behavior. When both `trackLogs` and `trackFailedLogs` are enabled, `trackLogs` takes precedence.
|
||||
|
||||
### Track Modes
|
||||
|
||||
|
||||
+30
-1
@@ -54,12 +54,16 @@ Flags:
|
||||
--log-level string Set log level. Overrides "HELMFILE_LOG_LEVEL" OS environment variable when specified (default "info")
|
||||
-n, --namespace string Set namespace. Overrides "HELMFILE_NAMESPACE" OS environment variable when specified. Uses the namespace set in the context by default, and is available in templates as {{ .Namespace }}
|
||||
--no-color Output without color. Overrides "HELMFILE_NO_COLOR" and "NO_COLOR" OS environment variables when specified
|
||||
--otel-tracing Enable OpenTelemetry tracing (experimental).
|
||||
Configure the exporter with standard OTEL_* environment variables (e.g. OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_TRACES_EXPORTER).
|
||||
Overrides "HELMFILE_OTEL_TRACING" OS environment variable when specified. See docs/otel.md
|
||||
-q, --quiet Silence output. Equivalent to log-level warn. Overrides "HELMFILE_QUIET" OS environment variable when specified
|
||||
--repo-retries int Number of times to retry "helm repo add/update" and "helm registry login" on failure, with exponential backoff (1s, 2s, 4s, ..., capped at 30s). Set to 0 to disable retries. Overrides "HELMFILE_REPO_RETRIES" OS environment variable when specified
|
||||
-l, --selector stringArray Only run using the releases that match labels. Labels can take the form of foo=bar or foo!=bar.
|
||||
A release must match all labels in a group in order to be used. Multiple groups can be specified at once.
|
||||
"--selector tier=frontend,tier!=proxy --selector tier=backend" will match all frontend, non-proxy releases AND all backend releases.
|
||||
The name of a release can be used as a label: "--selector name=myrelease"
|
||||
The name of a release can be used as a label: "--selector name=myrelease".
|
||||
The "dir" key matches by directory prefix of the defining helmfile and skips non-matching sub-helmfiles; see https://helmfile.readthedocs.io/en/latest/#the-dir-selector
|
||||
--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).
|
||||
@@ -73,6 +77,30 @@ 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`.
|
||||
|
||||
### State value overrides
|
||||
|
||||
Use `--state-values-set` to override `.Values` within the Helmfile template. Separate
|
||||
assignments with commas, and escape a comma inside a value with `\,`:
|
||||
|
||||
```bash
|
||||
helmfile --state-values-set 'message=hello\,world,replicas=2' build
|
||||
```
|
||||
|
||||
This sets `message` to the string `hello,world` and `replicas` to the number `2`.
|
||||
Single quotes preserve the backslash when passing the argument through the shell.
|
||||
Backslashes before other characters and at the end of a value are preserved.
|
||||
|
||||
`--state-values-set-string` accepts the same syntax but keeps every value as a
|
||||
string without type conversion. Its values may additionally be wrapped in
|
||||
single or double quotes, which allows commas without escaping:
|
||||
|
||||
```bash
|
||||
helmfile --state-values-set-string 'zone="zone1,zone2",imageTag=1.23.3' build
|
||||
```
|
||||
|
||||
Malformed assignments (missing `=`, empty key, or a trailing comma) are reported
|
||||
as errors instead of being partially applied.
|
||||
|
||||
### 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.
|
||||
@@ -550,6 +578,7 @@ The following global flags are also available but not shown in the main help out
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `--kubeconfig` | `""` | Use a particular kubeconfig file |
|
||||
| `--allow-failed-releases` | false | Continue preparing charts for other releases when chart preparation fails for a release; failed releases are skipped and all failures are reported at the end |
|
||||
| `--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) |
|
||||
|
||||
+31
-3
@@ -116,7 +116,8 @@ helmDefaults:
|
||||
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)
|
||||
# performs pods restart for the resource if applicable (default false).
|
||||
# Ignored with a warning on Helm 4, which removed --recreate-pods.
|
||||
recreatePods: true
|
||||
# forces resource update through delete/recreate if needed (default false)
|
||||
force: false
|
||||
@@ -129,6 +130,22 @@ helmDefaults:
|
||||
# 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
|
||||
# When set to `true` (default), resolves an OCI chart's semver constraint
|
||||
# (e.g. `~1`, `^2.0.0`, `*`, `1.x`, or any version that is not a
|
||||
# fully-qualified `X.Y.Z` semver — including partial versions like `1` or
|
||||
# `1.2`, which helm resolves as floating ranges) to a concrete registry tag
|
||||
# before deriving the on-disk cache path under `$XDG_CACHE_HOME/helmfile`.
|
||||
# This keeps the cache content-addressable so a newer matching tag is picked
|
||||
# up on the next run instead of the previously-resolved (now stale) version.
|
||||
# Set to `false` to preserve the pre-fix behavior of caching under the raw
|
||||
# constraint string. Resolution is also skipped when skipRefresh is enabled
|
||||
# (CLI `--skip-refresh`, per-release, or here): helmfile then reuses whatever
|
||||
# a previous run resolved. Releases with no `version:` at all are also
|
||||
# unaffected: helm picks the latest tag at pull time and helmfile caches it
|
||||
# under a version-less path (same as before this setting existed). Exact
|
||||
# `X.Y.Z` versions (e.g. `1.0.1`) and non-OCI releases are unaffected. See
|
||||
# issue #2766.
|
||||
resolveOCIVersions: true
|
||||
# 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
|
||||
@@ -238,7 +255,7 @@ releases:
|
||||
# waitRetries: 3
|
||||
waitForJobs: true
|
||||
timeout: 60
|
||||
recreatePods: true
|
||||
recreatePods: true # ignored with a warning on Helm 4
|
||||
force: false
|
||||
reuseValues: false
|
||||
# set `false` to uninstall this release on sync. (default true)
|
||||
@@ -442,6 +459,7 @@ The following `helmDefaults` fields are also available but not shown in the exam
|
||||
| `enableDNS` | bool | false | Enable DNS lookups when rendering templates |
|
||||
| `skipCRDs` | bool | false | Skip CRDs during installation |
|
||||
| `skipRefresh` | bool | false | Skip running `helm dependency up` |
|
||||
| `resolveOCIVersions` | bool | true | Resolve OCI semver constraints (e.g. `~1`, `^2.0.0`, `*`, `1.x`, and partial versions like `1.2` that helm treats as floating ranges) to concrete registry tags before deriving the shared cache path. Prevents stale cache hits after new matching tags are published. Set to `false` to keep the pre-fix behavior. Skipped when `skipRefresh` is enabled (CLI `--skip-refresh`, per-release, or `helmDefaults`). Only affects OCI releases with a non-exact version. See issue #2766 |
|
||||
| `atomic` | bool | false | Restore previous state on a failed install/upgrade. On Helm 4+ emits `--rollback-on-failure` (the successor to the deprecated `--atomic`); on older Helm emits `--atomic` |
|
||||
| `rollbackOnFailure` | bool | false | Restore previous state on a failed install/upgrade via the Helm 4 `--rollback-on-failure` flag. Requires Helm 4 or greater. Mutually exclusive with `atomic` |
|
||||
| `forceConflicts` | bool | false | Force server-side apply changes against conflicts (Helm 4 only) |
|
||||
@@ -456,6 +474,12 @@ The following `helmDefaults` fields are also available but not shown in the exam
|
||||
|
||||
`condition` controls whether a release is enabled. An empty condition enables the release. A direct `true` or `false` value is treated as a literal boolean and bypasses values lookup. Any other condition must be a values lookup path ending in `.enabled`, such as `vault.enabled`.
|
||||
|
||||
#### Continue On Error
|
||||
|
||||
`continueOnError` defaults to `false`. When set to `true`, Helmfile keeps processing independent releases after this release fails, but any release that depends on it is skipped with an explicit error such as `release "backend" was skipped because dependency "database" failed`.
|
||||
|
||||
This setting only changes Helmfile orchestration. It does not change Helm's per-release rollback behavior (`atomic`, `rollbackOnFailure`, `cleanupOnFail`, `wait`, `waitForJobs`, `trackMode`, hooks, or `reinstallIfForbidden`), and it does not turn a release failure into a successful command. `helmfile sync` and the sync phase of `helmfile apply` still exit with a non-zero status when any release fails. For `apply`, diff and chart-preparation failures remain fatal and are not converted into tolerated release errors.
|
||||
|
||||
`conditionTemplate` is evaluated before `condition` is checked. It must render to a boolean value; when both `condition` and `conditionTemplate` are set, the rendered `conditionTemplate` value replaces `condition`. Like other `*Template` fields, `conditionTemplate` is not evaluated by the `list` command.
|
||||
|
||||
The following per-release fields are also available:
|
||||
@@ -475,6 +499,7 @@ The following per-release fields are also available:
|
||||
| `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` |
|
||||
| `resolveOCIVersions` | bool | inherited | Per-release override of the `helmDefaults.resolveOCIVersions` setting |
|
||||
| `disableAutoDetectedKubeVersionForDiff` | bool | false | Disable auto-detected kubeVersion for helm diff on this release |
|
||||
| `takeOwnership` | bool | false | Take ownership of existing resources for this release |
|
||||
| `serverSide` | string | | Controls the helm 4 `--server-side` flag for this release. Must be `"true"`, `"false"`, or `"auto"` (Helm 4 only) |
|
||||
@@ -490,7 +515,10 @@ See [Advanced Features](advanced-features.md#resource-tracking-with-kubedog) for
|
||||
|-------|------|---------|-------------|
|
||||
| `trackMode` | string | `""` | Track mode: `helm`, `helm-legacy`, or `kubedog` |
|
||||
| `trackTimeout` | int | 300 | Tracking timeout in seconds |
|
||||
| `trackLogs` | bool | false | Enable real-time log streaming |
|
||||
| `trackLogs` | bool | false | Print logs from tracked resources during deployment |
|
||||
| `trackLogsInterval` | duration | `10s` | Interval between log output updates for `trackLogs` or `trackFailedLogs` (e.g. `1s`; minimum `1s`); overrides the default, but an explicitly supplied `--track-logs-interval` takes precedence |
|
||||
| `trackLogsUntilReady` | bool | false | With `trackLogs: true`, stop routine pod logs once each pod first becomes ready; readiness and failure tracking continue. Job logs are unaffected |
|
||||
| `trackFailedLogs` | bool | false | Print collected logs only for pods that fail during deployment; overridden by `trackLogs` |
|
||||
| `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`) |
|
||||
|
||||
@@ -222,6 +222,14 @@ environments:
|
||||
- http://$HOSTNAME/artifactory/example-repo-local/test.tgz@environments/production.secret.yaml
|
||||
```
|
||||
|
||||
Wildcards are supported in the file selector, the same way as for [remote values files](#loading-remote-environment-values-files):
|
||||
```yaml
|
||||
environments:
|
||||
staging:
|
||||
secrets:
|
||||
- git::https://{{ env "GITHUB_PAT" }}@github.com/org/repo.git@environments/staging/secrets/*.yaml?ref=main
|
||||
```
|
||||
|
||||
### Loading remote Environment values files
|
||||
|
||||
Since Helmfile v0.118.8, you can use `go-getter`-style URLs to refer to remote values files:
|
||||
@@ -261,6 +269,26 @@ For more information about the supported protocols see: [go-getter Protocol-Spec
|
||||
|
||||
This is particularly useful when you co-locate helmfiles within your project repo but want to reuse the definitions in a global repo.
|
||||
|
||||
##### Wildcards in remote values files
|
||||
|
||||
The file selector (the part after `@`) can be a glob pattern, so a whole directory of values files can be referenced at once, without listing every file individually:
|
||||
|
||||
```yaml
|
||||
environments:
|
||||
production:
|
||||
values:
|
||||
- git::https://github.com/org/repo.git@config/production/*.yaml?ref=main
|
||||
```
|
||||
|
||||
All matches are fetched from a single clone of the repository, then merged in the same sorted order as a local `values:` glob (see [Precedence](#environment-values-precedence)).
|
||||
|
||||
Notes and limitations:
|
||||
- The pattern is matched with the same syntax as local values file globs (`filepath.Match`: `*`, `?`, `[abc]`), against a single path segment — there is no recursive `**`.
|
||||
- Wildcards require an explicit `@` selector to mark the repository root, as in the example above.
|
||||
- Wildcards are only supported with getters that download a whole directory, such as `git::`. Plain `https://`/`s3://` references and a non-archive `s3::` reference each fetch a single file and cannot be expanded; use `git::` (or an `s3::` archive URL) instead.
|
||||
- A raw `?` in the file selector can never work as a wildcard: it starts the URL's query string (e.g. `?ref=main`), so anything after it is parsed as part of the query, not the file selector. A `?` wildcard must be percent-encoded as `%3F` to survive as part of the path, e.g. `@dir/v%3F.yaml?ref=main`.
|
||||
- This applies to environment `values:`/`secrets:` only. In release-level `values:`/`secrets:`, a wildcard that matches exactly one file also works, but a wildcard matching more than one file still fails with "glob patterns in release values and secrets is not supported yet" — the same restriction that already applies to local glob patterns there.
|
||||
|
||||
### 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)
|
||||
|
||||
@@ -28,3 +28,17 @@ 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.
|
||||
|
||||
## otel-tracing
|
||||
|
||||
OpenTelemetry tracing support: export a trace of a helmfile run to any OTLP-compatible backend. The current increments emit the command-level root span, state-loading spans (discover/load/render/parse), one span per external process (helm invocations, hooks, plugin execs), per-hook spans, per-release spans for the sync/diff/delete/status/test/prepare paths, and metrics (`helmfile.helm.exec.duration`, `helmfile.release.duration`, `helmfile.release.count`; per-release dimensions opt-in via `HELMFILE_OTEL_METRICS_PER_RELEASE`) (the remaining loops are being added incrementally) — see [the design proposal](https://github.com/helmfile/helmfile/blob/main/docs/proposals/otel-tracing.md).
|
||||
|
||||
Unlike the features above, `otel-tracing` is **not** gated by `HELMFILE_EXPERIMENTAL`. It is off by default and enabled explicitly per run:
|
||||
|
||||
```bash
|
||||
helmfile --otel-tracing -e production apply
|
||||
# or
|
||||
export HELMFILE_OTEL_TRACING=true
|
||||
```
|
||||
|
||||
All exporter, sampler, and propagator settings come from the standard `OTEL_*` environment variables (e.g. `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_TRACES_EXPORTER`). See [OpenTelemetry Tracing](otel.md) for details.
|
||||
|
||||
@@ -51,6 +51,8 @@ To avoid upgrades for each iteration of `helm`, the `helmfile` executable delega
|
||||
|
||||
**Patch**: JSON/Strategic-Merge Patch Kubernetes resources before `helm-install`ing, without forking upstream charts (See [#673](https://github.com/roboll/helmfile/pull/673))
|
||||
|
||||
**Observability**: Opt-in [OpenTelemetry traces and metrics](otel.md) for CI/CD runs — see where time goes, per release and per helm invocation (experimental)
|
||||
|
||||
## Installation
|
||||
|
||||
* download one of [releases](https://github.com/helmfile/helmfile/releases)
|
||||
@@ -213,6 +215,25 @@ The `selector` parameter can be specified multiple times. Each parameter is reso
|
||||
|
||||
In addition to user supplied labels, the name, the namespace, and the chart are available to be used as selectors. The chart will just be the chart name excluding the repository (Example `stable/filebeat` would be selected using `--selector chart=filebeat`).
|
||||
|
||||
### The `dir` selector
|
||||
|
||||
The `dir=` selector key matches releases by the directory of their defining helmfile, relative to the root directory: the directory of the file passed via `-f` (or that directory itself when `-f` points to one), or the current directory when no `-f` is given. Unlike other selectors, `dir=` uses directory-prefix matching: `--selector dir=apps/opencloud` matches both releases defined directly in `apps/opencloud/` and those in any subdirectory below it. Values are normalized (trailing slashes and `./` prefixes are stripped). Absolute paths, `..` segments that escape the root, and the bare `.` are rejected at parse time; omit the selector entirely to match every release.
|
||||
|
||||
The `dir` selector has a second, more important effect: **it short-circuits sub-helmfile traversal**. When `dir=` is set, helmfile inspects each `helmfiles:` entry's path *before* loading or templating it, and skips any branch whose location rules out a matching release. For projects that aggregate many services via nested helmfiles (e.g. opendesk's `helmfile_generic.yaml.gotmpl` lists ~13 services), running `helmfile -l dir=apps/<single-service> sync` becomes roughly as fast as targeting that single service directly, without the cost of parsing and templating the other branches.
|
||||
|
||||
The label key `dir` is reserved. If `commonLabels`, a release's `labels` or a release template declares a key named `dir`, helmfile logs a warning and the user-defined value is ignored for selector matching (the auto-populated value shadows it, also for releases that have no auto-populated value). **Deprecation:** this will become a hard error in a future release, so rename such labels now.
|
||||
|
||||
#### Interactions worth knowing
|
||||
|
||||
* Releases loaded from remote (`git::…`) helmfiles do not receive a `dir` label and the optimization does not engage for them; they are always descended into. The same applies to sub-helmfiles located outside the root directory (e.g. `helmfiles: [../shared/helmfile.yaml]`).
|
||||
* The skip decision only looks at the location of the `helmfiles:` entry itself, so it assumes that nested helmfiles stay at or below their parent's directory. A branch that reaches the target via `..` (e.g. `stacks/prod/helmfile.yaml` listing `../../apps/foo/helmfile.yaml`) is skipped for `-l dir=apps/foo`, and its releases are not found.
|
||||
* Without `-f`, an auto-discovered `helmfile.d/` is anchored at the current directory, so its releases match `dir=helmfile.d`. With `-f helmfile.d` the anchor is `helmfile.d` itself.
|
||||
* **A nested `helmfiles:` entry that declares its own `selectors:` block replaces the CLI selectors for its sub-tree** per the existing inheritance model, so a CLI `-l dir=…` neither filters releases in that branch nor skips traversal below it.
|
||||
* Releases declared in a file loaded via `bases:` are merged into the *including* helmfile, so their `dir` value reflects the including helmfile's location rather than the base file's.
|
||||
For example, with `apps/web/helmfile.yaml` containing `bases: [../../common/releases.yaml]`, every release declared in `common/releases.yaml` gets `dir=apps/web`, not `dir=common`; `-l dir=common` matches nothing.
|
||||
* Globs in `helmfiles:` (e.g. `apps/**/helmfile.yaml`) are expanded before the dir-skip check, so a missing-file error in an entry that the dir filter would have skipped still fails the parent load. Use `missingFileHandler: Warn` if this is a problem.
|
||||
* Cross-branch transitive `needs:` are not auto-resurrected across pruned branches. If a release in `apps/x` depends on one in `apps/y`, run with a `dir=` covering both subtrees.
|
||||
|
||||
`commonLabels` can be used when you want to apply the same label to all releases and use [templating](templating.md) based on that.
|
||||
For instance, you install a number of charts on every customer but need to provide different values file per customer.
|
||||
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
# OpenTelemetry Tracing
|
||||
|
||||
Helmfile can export [OpenTelemetry](https://opentelemetry.io/) traces of a run, which is especially useful in CI/CD: see where time is spent, which helm invocations dominate, how label selectors fan out, and what to target for improvement.
|
||||
|
||||
Tracing is **experimental** and **off by default**. It currently emits the command-level root span, state-loading spans (discover/load/render/parse), one span per external process (helm invocations, hooks, plugin execs), per-hook spans, and per-release spans for the sync/diff/delete/status/test/prepare paths.
|
||||
|
||||
## Enabling
|
||||
|
||||
```bash
|
||||
helmfile --otel-tracing -e production -l tier=backend apply
|
||||
```
|
||||
|
||||
or via environment variable (handy for CI):
|
||||
|
||||
```bash
|
||||
export HELMFILE_OTEL_TRACING=true
|
||||
helmfile sync
|
||||
```
|
||||
|
||||
`--otel-tracing` overrides `HELMFILE_OTEL_TRACING` when specified.
|
||||
|
||||
## Configuration
|
||||
|
||||
Everything beyond the on/off switch uses the standard [OpenTelemetry environment variables](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/); helmfile defines no telemetry-specific variables of its own.
|
||||
|
||||
| Variable | Default | Notes |
|
||||
|---|---|---|
|
||||
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4318` | OTLP endpoint; use `http://host:4317` for gRPC |
|
||||
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/protobuf` | or `grpc` |
|
||||
| `OTEL_EXPORTER_OTLP_HEADERS` | — | e.g. `authorization=Bearer <token>` |
|
||||
| `OTEL_TRACES_EXPORTER` | `otlp` | `otlp` \| `console` \| `none` |
|
||||
| `OTEL_METRICS_EXPORTER` | `otlp` | `otlp` \| `console` \| `prometheus` \| `none` — see [Metrics](#metrics) |
|
||||
| `OTEL_TRACES_SAMPLER` (+ `OTEL_TRACES_SAMPLER_ARG`) | `parentbased_always_on` | |
|
||||
| `OTEL_SERVICE_NAME` | `helmfile` | |
|
||||
| `OTEL_RESOURCE_ATTRIBUTES` | — | e.g. `cicd.pipeline=deploy,cicd.run_id=4821` |
|
||||
| `OTEL_PROPAGATORS` | `tracecontext,baggage` | |
|
||||
| `OTEL_SDK_DISABLED` | `false` | standard kill switch |
|
||||
|
||||
HTTP proxies (`HTTPS_PROXY`/`HTTP_PROXY`) are honored automatically by the Go runtime.
|
||||
|
||||
## Quick start without a collector
|
||||
|
||||
To inspect what would be exported, print spans as JSON on stdout:
|
||||
|
||||
```bash
|
||||
OTEL_TRACES_EXPORTER=console helmfile --otel-tracing -l name=myrelease template
|
||||
```
|
||||
|
||||
## Quick start with Jaeger
|
||||
|
||||
```bash
|
||||
docker run --rm -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one:latest
|
||||
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \
|
||||
helmfile --otel-tracing -e production apply
|
||||
# Open http://localhost:16686 and pick service "helmfile"
|
||||
```
|
||||
|
||||
## What gets traced
|
||||
|
||||
The root span is named after the command (`helmfile apply`, `helmfile sync`, ...) and carries `helmfile.command`, `helmfile.file`, `helmfile.environment`, and `helmfile.selectors` attributes, plus `helmfile.exit_code` and the error (if any) at the end.
|
||||
|
||||
Every external process helmfile starts — each helm invocation, hook command, and plugin exec — gets its own span nested under the command span: `helm.exec` (with `helm.subcommand`) for helm binaries, `os.exec` otherwise, both carrying `exec.command`, redacted `exec.args`, and `exec.exit_code` on failure.
|
||||
|
||||
State loading is traced too: `helmfile.discover_states`, one `helmfile.load` per state file (including nested helmfiles), with `helmfile.render` and `helmfile.parse` children — rendering is frequently the hidden time sink. Each hook execution produces a `helmfile.hook` span (with `hook.event` and `hook.name`) that its subprocess span nests under — and release-scoped hooks (presync/postsync & co. within sync/diff/delete) nest under their release's span, keeping full release attribution.
|
||||
|
||||
Per-release spans (`helmfile.release.sync` / `.diff` / `.delete` / `.status` / `.test` / `.prepare`) carry the release name, namespace, chart, and labels, and nest under the state-file load span — with the release's helm subprocesses (upgrade, diff, delete, status, test) nested under the release span. The remaining loops (flag preparation, template/lint/unittest) are being added incrementally; see [the design proposal](proposals/otel-tracing.md) for the full taxonomy.
|
||||
|
||||
Secret-bearing command arguments (`--set`, `--set-file`, `--username`, `--password`, ...) are never recorded in span attributes — they are masked before export.
|
||||
|
||||
## Metrics
|
||||
|
||||
Tracing and metrics share the same switch and resource; metrics are exported through the standard metrics environment variables:
|
||||
|
||||
| Variable | Default | Notes |
|
||||
|---|---|---|
|
||||
| `OTEL_METRICS_EXPORTER` | `otlp` | `otlp` \| `console` \| `prometheus` \| `none` |
|
||||
| `OTEL_METRIC_EXPORT_INTERVAL` | `60000` (ms) | periodic export interval; the final flush happens at exit |
|
||||
|
||||
Three instruments are emitted:
|
||||
|
||||
| Metric | Type | Attributes |
|
||||
|---|---|---|
|
||||
| `helmfile.helm.exec.duration` | histogram (seconds, buckets tuned for 5ms–600s helm invocations) | `subcommand`, `success` |
|
||||
| `helmfile.release.duration` | histogram (seconds, same buckets) | `verb`, `result` — plus `helmfile.release` and `helmfile.namespace` when per-release metrics are enabled (below) |
|
||||
| `helmfile.release.count` | counter (unit `{release}`) | `verb` (sync/diff/delete/status/test/prepare), `result` (success/error) |
|
||||
|
||||
### Per-release metrics (opt-in, high cardinality)
|
||||
|
||||
By default metric dimensions are bounded enumerations — release names never appear, so time-series count stays constant regardless of fleet size. If you need per-release durations on dashboards, opt in:
|
||||
|
||||
```bash
|
||||
HELMFILE_OTEL_METRICS_PER_RELEASE=true helmfile --otel-tracing sync
|
||||
```
|
||||
|
||||
This adds `helmfile.release` and `helmfile.namespace` to `helmfile.release.duration`. Time-series count then scales with your release fleet: well-suited to bounded CI runs, but long-lived centralized collection needs a backend capacity/TTL story. (Per-release timing is always available in traces, without this flag.)
|
||||
|
||||
Both dimensions are bounded enumerations — never release names — so metric cardinality stays constant regardless of fleet size. The instrumentation scope (`helmfile`) carries the helmfile version.
|
||||
|
||||
Inspect them without a collector with `OTEL_METRICS_EXPORTER=console`.
|
||||
|
||||
## CI trace correlation
|
||||
|
||||
If your CI system injects a W3C `TRACEPARENT` environment variable into jobs, helmfile joins that trace automatically, so helmfile spans appear inside your pipeline's trace.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **No spans arrive**: check `OTEL_EXPORTER_OTLP_ENDPOINT`/protocol match your collector (4318 for `http/protobuf`, 4317 for `grpc`); verify with `OTEL_TRACES_EXPORTER=console`.
|
||||
- **Tracing breaks nothing**: exporter/sampler misconfiguration and export failures never fail a helmfile run; at worst you get no spans.
|
||||
- **Still stuck?** Set `OTEL_SDK_DISABLED=false` explicitly and rerun with `--log-level debug`.
|
||||
@@ -0,0 +1,560 @@
|
||||
# Proposal: OpenTelemetry Tracing Support
|
||||
|
||||
- **Issue**: [#2767 — feat-request: OpenTelemetry (tracing) support](https://github.com/helmfile/helmfile/issues/2767)
|
||||
- **Status**: Draft
|
||||
- **Origin discussion**: [#2758](https://github.com/helmfile/helmfile/discussions/2758)
|
||||
|
||||
## 1. Summary
|
||||
|
||||
Add opt-in OpenTelemetry (OTel) distributed tracing to helmfile. When enabled, a helmfile
|
||||
run produces a trace whose spans cover the full execution timeline — state-file discovery,
|
||||
template rendering, per-release operations, hooks, and every helm subprocess helmfile itself
|
||||
starts — exported via OTLP to any OTel-compatible backend (Jaeger, Tempo, Zipkin,
|
||||
Honeycomb, Datadog, Grafana Cloud, cloud-vendor collectors, ...).
|
||||
|
||||
This directly serves the motivating use case from CI/CD: *see where time is spent, what runs
|
||||
most often (label selectors), and what to target for improvement* — the same capability
|
||||
Terragrunt ships today ([Terragrunt OTel docs](https://docs.terragrunt.com/troubleshooting/open-telemetry/)).
|
||||
|
||||
When disabled (the default), behavior and performance are identical to today: a no-op tracer
|
||||
provider, no goroutines, no network, no exported spans (explicit guarantees in §7).
|
||||
|
||||
### Example resulting trace
|
||||
|
||||
```
|
||||
helmfile sync file=helmfile.yaml env=production (root, ~2m)
|
||||
├─ helmfile.discover_states (~5ms)
|
||||
├─ helmfile.load file=helmfile.yaml (~800ms)
|
||||
│ ├─ helmfile.render pass=values (~400ms)
|
||||
│ └─ helmfile.parse (~50ms)
|
||||
├─ helmfile.repos.update (~4s)
|
||||
│ └─ helm.exec subcommand="repo update" (~4s)
|
||||
├─ helmfile.release.prepare release=gateway chart=gateway (~9s)
|
||||
│ └─ helm.exec subcommand="dependency build" (~8s)
|
||||
├─ helmfile.release.sync release=envoy ns=ingress (~21s)
|
||||
│ ├─ helmfile.hook event=presync (~2s)
|
||||
│ │ └─ os.exec command="./migrate.sh" (~2s)
|
||||
│ ├─ helm.exec subcommand="upgrade --install envoy ..." (~18s)
|
||||
│ └─ helmfile.hook event=postsync (~1s)
|
||||
├─ helmfile.release.sync release=api ns=apps (~35s)
|
||||
└─ helmfile.wait release=api (kubedog) (~30s)
|
||||
```
|
||||
|
||||
## 2. Goals and non-goals
|
||||
|
||||
### Goals
|
||||
|
||||
1. **Opt-in, zero-cost-when-off**: tracing disabled by default; no measurable overhead or
|
||||
behavior change when not requested.
|
||||
2. **Standard OTLP export** configured through the [OTel environment-variable
|
||||
specification](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/)
|
||||
— no helmfile-specific duplicate knobs for endpoints/protocols/headers/sampling.
|
||||
3. **Meaningful hierarchy**: spans nest (command → state file → release → hook/subprocess)
|
||||
so the critical path of a sync/apply is visible at a glance — *including* the kubedog and
|
||||
hook paths, which today run on detached contexts (§4.3).
|
||||
4. **Safe by default**: span attributes never contain secret-bearing values
|
||||
(`--set` values, registry credentials, secret refs).
|
||||
5. **CI-friendly**: W3C `tracecontext` propagation so a CI system that injects
|
||||
`TRACEPARENT` gets spans correlated into its own trace.
|
||||
|
||||
### Non-goals (for the initial implementation)
|
||||
|
||||
- OTel **metrics** and **log export** (the SDK setup leaves room; see §12 for follow-ups).
|
||||
- Tracing *inside* the helm binary or cluster-side (helm/chart hooks are separate processes;
|
||||
context injection into them is an open question, §14).
|
||||
- Fixing the pre-existing cancellation gaps discovered during design (kubedog path rooted at
|
||||
`context.Background()`, hooks at `context.TODO()` — §4.3). This proposal bridges them for
|
||||
*trace context only*, with byte-identical cancellation semantics; semantic fixes are
|
||||
reported separately.
|
||||
- Any helmfile-config-file surface (`helmfile.yaml`) for telemetry — CLI flags + env vars only,
|
||||
matching how observability tooling is usually injected by the platform, not the state author.
|
||||
|
||||
## 3. User-facing design
|
||||
|
||||
### 3.1 Activation
|
||||
|
||||
| Mechanism | Default | Description |
|
||||
|---|---|---|
|
||||
| `--otel-tracing` (persistent flag) | `false` | Enable tracing for this run |
|
||||
| `HELMFILE_OTEL_TRACING=true` (env) | `false` | Same, via environment (CI-friendly) |
|
||||
|
||||
The feature ships as **experimental** initially and is listed in
|
||||
`docs/experimental-features.md` (see §11 Rollout). We do *not* require
|
||||
`HELMFILE_EXPERIMENTAL=otel-tracing`: tracing is purely additive, invisible when off, and
|
||||
gating it twice would complicate CI adoption. The experimental label sets stability
|
||||
expectations only. (Note: this differs from the current entries in that document, which are
|
||||
gated by `HELMFILE_EXPERIMENTAL`; the entry will say so explicitly.)
|
||||
|
||||
### 3.2 Exporter & SDK configuration — standard OTel env vars only
|
||||
|
||||
When `--otel-tracing` is set, helmfile initializes the OTel SDK honoring the standard
|
||||
environment variables. Exporter selection is delegated to
|
||||
`go.opentelemetry.io/contrib/exporters/autoexport` (already in the module graph, §8) rather
|
||||
than hand-rolled, so helmfile maintains no exporter-construction code of its own:
|
||||
|
||||
| Variable | Default in helmfile | Read by | Notes |
|
||||
|---|---|---|---|
|
||||
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4318` | otlp client (SDK) | default follows the protocol: 4318 with `http/protobuf` (below), 4317 with `grpc` |
|
||||
| `OTEL_EXPORTER_OTLP_PROTOCOL` / `..._TRACES_PROTOCOL` | `http/protobuf` | autoexport | `grpc`, `http/protobuf` (traces-specific var wins; `http/json` is not supported by autoexport traces in v0.67.0) |
|
||||
| `OTEL_EXPORTER_OTLP_HEADERS` / `..._TRACES_HEADERS` | — | otlp client (SDK) | e.g. collector auth tokens |
|
||||
| `OTEL_EXPORTER_OTLP_TIMEOUT` / `..._TRACES_TIMEOUT` | `10s` | otlp client (SDK) | per-export timeout |
|
||||
| `OTEL_EXPORTER_OTLP_INSECURE` | per endpoint scheme | otlp client (SDK) | plaintext export for local collectors |
|
||||
| `OTEL_TRACES_EXPORTER` | `otlp` | autoexport | `otlp` \| `console` \| `none` (verified against autoexport v0.67.0; more values possible via `RegisterSpanExporter`) |
|
||||
| `OTEL_TRACES_SAMPLER` (+`..._ARG`) | `parentbased_always_on` | our wrapper | standard samplers (the Go SDK core does not parse this env itself) |
|
||||
| `OTEL_SERVICE_NAME` | `helmfile` | SDK resource | service identity (`resource` `WithFromEnv`; verified in sdk v1.44.0 `resource/env.go`) |
|
||||
| `OTEL_RESOURCE_ATTRIBUTES` | — | SDK resource | e.g. `deployment.environment=ci,cicd.pipeline=release` (same verified reader) |
|
||||
| `OTEL_PROPAGATORS` | `tracecontext,baggage` | our wrapper | extract parent from CI-injected `TRACEPARENT` (SDK core does not parse this env either) |
|
||||
| `OTEL_METRICS_EXPORTER` | `otlp` | autoexport (metrics) | `otlp` \| `console` \| `prometheus` \| `none` — delivered in PR 4 |
|
||||
| `OTEL_METRIC_EXPORT_INTERVAL` | `60000` (ms) | SDK metric reader | periodic export interval; final flush happens at exit |
|
||||
| `OTEL_SDK_DISABLED` | `false` | our wrapper | standard kill switch (the Go SDK does not read this one itself) |
|
||||
|
||||
`console` (JSON spans on **stdout**, via `stdouttrace`) exists for local debugging without a
|
||||
collector — note stdout, so it does not interleave with helmfile's stderr logs;
|
||||
`none` produces no export (used by tests and for pure-propagation setups).
|
||||
|
||||
HTTP proxies (`HTTPS_PROXY`/`HTTP_PROXY`) are honored automatically by the Go HTTP/gRPC
|
||||
stacks — no helmfile-specific proxy configuration.
|
||||
|
||||
Minimal CI example:
|
||||
|
||||
```bash
|
||||
export OTEL_EXPORTER_OTLP_ENDPOINT="https://otel.example.com"
|
||||
export OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer ${OTEL_TOKEN}"
|
||||
export OTEL_SERVICE_NAME="helmfile-ci"
|
||||
export OTEL_RESOURCE_ATTRIBUTES="cicd.pipeline=deploy,cicd.run_id=4821"
|
||||
helmfile --otel-tracing -e production -l tier=backend apply
|
||||
```
|
||||
|
||||
## 4. Architecture
|
||||
|
||||
### 4.1 New package `pkg/telemetry`
|
||||
|
||||
Single, dependency-light façade over the OTel SDK. No other package imports OTel SDK
|
||||
exporters directly.
|
||||
|
||||
```
|
||||
pkg/telemetry/
|
||||
├── telemetry.go // Setup/Shutdown lifecycle, CommandContext(), Tracer(name)
|
||||
├── exporter.go // thin autoexport wiring + propagators (no exporter construction)
|
||||
└── telemetry_test.go
|
||||
```
|
||||
|
||||
Public surface sketch:
|
||||
|
||||
```go
|
||||
package telemetry
|
||||
|
||||
type Options struct {
|
||||
Enabled bool
|
||||
Version string // helmfile version, recorded as service.version
|
||||
Logger *zap.SugaredLogger // for one-line diagnostics, never span data
|
||||
}
|
||||
|
||||
// Setup initializes the global provider. Idempotent; a no-op when
|
||||
// opts.Enabled is false. Exporter/sampler misconfiguration and
|
||||
// OTEL_SDK_DISABLED=true degrade to disabled with a warning — telemetry
|
||||
// problems never fail a helmfile run.
|
||||
func Setup(ctx context.Context, opts Options)
|
||||
|
||||
// StartCommandSpan starts the root span for one command invocation, joins a
|
||||
// remote parent from TRACEPARENT/TRACESTATE/BAGGAGE env vars, and makes its
|
||||
// context the one returned by CommandContext. No-op when disabled.
|
||||
func StartCommandSpan(command string, attrs ...attribute.KeyValue)
|
||||
|
||||
// CommandContext returns the root command span's context, or
|
||||
// context.Background() when telemetry is disabled. Single source of truth
|
||||
// for deriving App.ctx.
|
||||
func CommandContext() context.Context
|
||||
|
||||
// Tracer returns a tracer for the given instrumentation scope. Never nil:
|
||||
// returns the OTel no-op tracer when telemetry is disabled, so callers need
|
||||
// no `if enabled` branches. Sanctioned scopes: ScopeHelmfile, ScopeHelm.
|
||||
func Tracer(name string) trace.Tracer
|
||||
|
||||
// Shutdown ends the command span (recording runErr and exitCode), flushes
|
||||
// buffered spans bounded by ctx, and reverts to disabled. Idempotent and
|
||||
// nil-safe (safe before Setup, twice, or on a signal that raced Setup).
|
||||
// ShutdownTimeout is the recommended flush bound.
|
||||
func Shutdown(ctx context.Context, runErr error, exitCode int) error
|
||||
```
|
||||
|
||||
Design constraints for maintainability:
|
||||
|
||||
- **No domain knowledge in this package.** Span-attribute redaction rules live with the
|
||||
existing redaction code in `pkg/helmexec` (§6); telemetry only consumes the result.
|
||||
A telemetry package that knows about `--set` semantics would be a layering violation.
|
||||
- Sanctioned instrumentation scopes are only `"helmfile"` (app/state layer) and `"helm"`
|
||||
(helmexec layer), documented in `telemetry.go`.
|
||||
|
||||
### 4.2 Lifecycle
|
||||
|
||||
```
|
||||
main.go cmd/root.go
|
||||
────── ───────────
|
||||
rootCmd.Execute() ─────────────► PersistentPreRunE:
|
||||
logger setup (existing)
|
||||
telemetry.Setup(ctx, opts) ← provider + exporters
|
||||
start root span "helmfile <sub>"
|
||||
RunE / subcommand execution … (child spans attach via context)
|
||||
errChan <- Execute()
|
||||
┌────────────────────────────────┐
|
||||
shutdown: │ end root span (status=error on │
|
||||
rootSpan.End() │ failure, exit code attr) │
|
||||
provider.Shutdown(ctx 5s) ◄─────┘ │
|
||||
```
|
||||
|
||||
- `PersistentPreRunE` (`cmd/root.go`) is the natural init point: it already centralizes
|
||||
logger construction from `GlobalOptions`, and receives the `*cobra.Command`, so
|
||||
`c.Name()` gives the root span name (`helmfile sync`, ...).
|
||||
- **`App.ctx` derivation requires no signature changes.** `app.New(conf)` is called from
|
||||
every subcommand (`cmd/*.go`, 22 call sites) and currently roots its context at
|
||||
`context.Background()` (`pkg/app/app.go`). Instead of threading a parameter through all
|
||||
callers, `app.New` replaces that single `context.Background()` with
|
||||
`telemetry.CommandContext()` — Background-identical when tracing is off, span-rooted
|
||||
when on. Cancellation is unaffected (`context.WithCancel` on either parent behaves the
|
||||
same for SIGINT handling in `main.go`).
|
||||
- **Shutdown must run even on failure and on signals.** The end of
|
||||
`rootCmd.Execute()` in `main.go` (both the `errChan` and the `SIGINT`/`SIGTERM` paths;
|
||||
on the error path *before* `errors.HandleExitCoder`, which terminates via `OsExiter`,
|
||||
and on the signal path after `app.CleanWaitGroup.Wait()` and *before*
|
||||
`os.Exit(130/143)`) calls the returned shutdown func with a 5s-timeout context so buffered
|
||||
spans are flushed. The stored shutdown must be nil-safe — a signal arriving before
|
||||
`PersistentPreRunE` completed (i.e. before `Setup`) must be a no-op, never a panic. This
|
||||
is the one behavior that is easy to get wrong and is explicitly tested (§10).
|
||||
- Setup failures (e.g. unusable exporter configuration) are logged as a warning and
|
||||
**do not fail the run** — telemetry must never break deployments. Export errors after
|
||||
startup surface only through OTel's own error handler, wired to the zap logger.
|
||||
|
||||
### 4.3 Verified context-reality map (as of this writing)
|
||||
|
||||
All claims below were checked against the code; line numbers are anchors for reviewers:
|
||||
|
||||
| Path | Today | Consequence for tracing |
|
||||
|---|---|---|
|
||||
| cmd → app | `app.New` roots at `context.Background()`; `ctx, Cancel = WithCancel(ctx)` (`pkg/app/app.go`, `New`) | span must be injected here (§4.2) |
|
||||
| app → all helm execs | `getHelm()` constructs the `ShellRunner` with `Ctx: a.ctx` (`pkg/app/app.go:1008`); the resulting `execer` is **cached per (helm binary, kube-context)** in `a.helms` and shared by all releases and workers (`pkg/app/app.go:982–1021`) | once `App.ctx` is span-rooted, every non-kubedog helm call nests automatically, with **zero changes** to `getHelm`. The shared-instance cache is also why per-release contexts must ride per-call parameters, never mutation of the shared execer |
|
||||
| kubedog path (sync with tracking) | originally `startBackgroundKubedogTracking(gocontext.Background(), …)` (`pkg/state/state.go`), with `bufferHelmOutput` deriving `releaseCtx := context.WithCancel(ctx)` and swapping it in via `execer.WithContext(releaseCtx)` (`pkg/state/helmx.go`). **Since #2791** the three call sites pass `st.releaseCancelContext()` — the app cancel context injected via `HelmState.SetCancelContext` — so SIGINT/SIGTERM reaches these helm subprocesses too (#2770) | trace context on this path is the same command span as everywhere else: the §4.4 bridge kept spans attached while cancellation was detached, and #2791 subsequently re-attached cancellation |
|
||||
| hooks | both `event.Bus` constructions (`triggerGlobalReleaseEvent`, `triggerReleaseEvent`, `pkg/state/state.go:3666, 3703`) duplicate the same literal and pass **no** `Runner`, so the default kicks in: `ShellRunner{Dir: bus.BasePath, Logger: bus.Logger, Ctx: goContext.TODO()}` with an inline comment acknowledging it should be `app.Ctx` (`pkg/event/bus.go:61–71`) | hook execs are detached; spans would be orphans. Bridged in §4.4 |
|
||||
| non-kubedog release workers | release loops (`SyncReleases` etc., `pkg/state/state.go:1212 ff.`) call the shared `helmexec.Interface` with a `HelmContext` (`pkg/helmexec/context.go`) that carries **no go-context** | per-release spans need the §4.4 mechanism |
|
||||
| subprocess funnel | exactly three `ShellRunner` construction sites exist (verified exhaustive): `pkg/app/app.go:129` (`Init`, `Ctx: a.ctx`), `pkg/app/app.go:1006` (`getHelm`, `Ctx: a.ctx`), and the hooks default (`pkg/event/bus.go:62`, `Ctx: TODO` — §4.4 bridge). Every external process helmfile itself starts goes through `Execute`/`ExecuteStdIn` (`pkg/helmexec/runner.go`); helm commands additionally funnel through `execer.exec()` (`pkg/helmexec/exec.go:1207`). Exception: kustomize executes inside the chartify library, outside this funnel (§12) | one instrumentation point covers everything except chartify-internal execs; spans nest wherever the runner's `Ctx` carries a span |
|
||||
|
||||
Two pre-existing gaps surfaced by this analysis — kubedog tracking not being cancellable via
|
||||
`App.ctx`, and hooks likewise — are **out of scope** for this proposal beyond trace-context
|
||||
bridging (§4.4), because fixing their *cancellation* semantics would be a behavior change.
|
||||
They should be reported as separate issues. (Update: the kubedog gap was fixed by
|
||||
#2791 via `HelmState.SetCancelContext`; the hooks gap remains open as #2771.)
|
||||
|
||||
### 4.4 Context plan
|
||||
|
||||
**Phase 1 (no state-package changes):**
|
||||
|
||||
1. Root span ctx via `telemetry.CommandContext()` → `app.New` (§4.2). Every `a.ctx`-rooted
|
||||
exec nests. Discover/load/render spans in `pkg/app` also need no signature changes:
|
||||
the `helmfile.load` span starts inside `loadDesiredStateFromYamlWithBaseDir`
|
||||
(`pkg/app/app.go:932`) with `a.ctx` as parent — its two call sites (app.go:1045 and
|
||||
the nested-helmfile path at app.go:1280) are thereby both covered — and `helmfile.render`
|
||||
children parent through an unexported `ctx` field on the `desiredStateLoader` struct
|
||||
(`pkg/app/desired_state_file_loader.go:29`), set once at its single construction site
|
||||
(`pkg/app/app.go:938`). Zero method-signature changes, zero exported API.
|
||||
2. Runner-level spans in `ShellRunner.Execute`/`ExecuteStdIn` nest for all non-kubedog,
|
||||
non-hook execs automatically.
|
||||
3. **Orphan-bridge for kubedog and hooks, with identical cancellation semantics:**
|
||||
- kubedog: originally pass `context.WithoutCancel(telemetry.CommandContext())`
|
||||
instead of `gocontext.Background()`. `WithoutCancel` preserved values (the span)
|
||||
while dropping cancellation — and `Background` never carried cancellation anyway, so
|
||||
SIGINT/timeout behavior stayed **bit-for-bit unchanged**; only trace context was
|
||||
added. (Phase 1 used the root span from `telemetry.CommandContext()`, which required
|
||||
no new plumbing in `pkg/state`; phase 2 re-parented under the per-release
|
||||
`st.traceCtx`.) Superseded by #2791: the three kubedog call sites now pass
|
||||
`st.releaseCancelContext()` — same command-span trace context, plus app
|
||||
cancellation, fixing #2770.
|
||||
- `pkg/event/bus.go`: add an optional `Ctx context.Context` field to `Bus`; the default
|
||||
runner construction uses `bus.Ctx` when set, `TODO` when nil (so behavior is unchanged
|
||||
for any nil-Ctx caller). The two construction sites in `pkg/state/state.go:3666, 3703`
|
||||
set it from the same span-rooted, cancel-stripped context. `Dir`/`Logger` wiring of
|
||||
the default runner is untouched.
|
||||
|
||||
**Phase 2 (per-release spans, still zero changes to `helmexec.Interface` signatures):**
|
||||
|
||||
- Start `helmfile.release.<verb>` spans in the seven release-worker loops — six
|
||||
`scatterGather` sites in `pkg/state/state.go` (`prepareSyncReleases`:894,
|
||||
`DeleteReleasesForSync`:1135, `SyncReleases`:1241, `PrepareCharts`:2328,
|
||||
`prepareDiffReleases`:3068, `DiffReleases`:3266) plus `iterateOnReleases`
|
||||
(`pkg/state/state_run.go:59`, the shared loop behind test/lint/unittest-style
|
||||
iteration) — all following the same `scatterGather` shape. (An eighth `scatterGather`
|
||||
sites, `scatterGatherEnvSecretFiles` at `pkg/state/create.go:486`, decrypts environment
|
||||
secrets rather than processing releases; an optional `helmfile.env_secrets` span there
|
||||
is a follow-up in the spirit of §4.5 item 6.)
|
||||
- Release spans are rooted via a new unexported `traceCtx` field on `HelmState`, exposed
|
||||
through a purely additive exported setter called by `pkg/app` right after state
|
||||
creation. (Why a setter and not a constructor parameter: `st.logger` is injected through
|
||||
`state.NewCreator` — a 9-positional-parameter exported function,
|
||||
`pkg/state/create.go:80` — so threading a context through it would churn a public
|
||||
signature used by the app loader; an additive setter touches no existing signature and
|
||||
defaults to nil, i.e. current behavior.)
|
||||
- Carry the span context per call by adding an optional `Ctx context.Context` field to
|
||||
`HelmContext` (`pkg/helmexec/context.go`), stamped **inside** `createHelmContext`
|
||||
(`pkg/state/state.go:3168`) so all eight call sites (state.go:1007, 1026, 1147, 1261,
|
||||
3034, 3184, 3358, 3374) inherit it without per-call-site edits.
|
||||
- Funnel it inside `helmexec`: the `execer` methods that take a `HelmContext` pass its
|
||||
`Ctx` to `execWithContext`, which **injects the release span into the runner's own
|
||||
context** (`trace.ContextWithSpan`) rather than replacing it — cancellation authority
|
||||
stays with the runner (including the kubedog safety valve installed via
|
||||
`WithContext`), which a naive context replacement would have overridden. A nil `Ctx`
|
||||
behaves exactly like the plain funnel. Helm-vs-other classification comes from an
|
||||
explicit marker stamped in the execer funnels (`withRunnerCtx`/`markHelmRunner`,
|
||||
value and pointer runner forms), not from the executable basename — so wrapper
|
||||
`--helm-binary` names classify correctly. The `exectest.Helm` fake is unaffected: app
|
||||
tests pre-seed `App.helms` with it (`pkg/app/app_template_test.go:115–116`), so it
|
||||
replaces the whole `helmexec.Interface` and bypasses `execer` internals entirely.
|
||||
*Why per-call threading rather than the existing `WithContext` clone: `WithContext`
|
||||
(`pkg/helmexec/exec.go:251`) is suited to whole-execution substitution (kubedog path),
|
||||
but release workers share one cached execer across concurrent workers (§4.3), so a
|
||||
per-release context must travel with the per-release `HelmContext` parameter.*
|
||||
|
||||
### 4.5 Instrumentation points (in priority order)
|
||||
|
||||
1. **`helmexec.ShellRunner.Execute` / `ExecuteStdIn`** (`pkg/helmexec/runner.go`) — the single
|
||||
choke point for *every* external process started by helmfile itself: helm invocations,
|
||||
hooks, and helmfile plugin execs. One span per subprocess: name `helm.exec` when `cmd`
|
||||
is the helm binary, else `os.exec`. The one exception is kustomize, which runs inside
|
||||
the `github.com/helmfile/chartify` library (see §5/§12). This instrumentation alone
|
||||
delivers most of the requested value (where does time go).
|
||||
2. **Root command span** — `cmd/root.go` (see §4.2).
|
||||
3. **State loading** — span `helmfile.load` started inside
|
||||
`loadDesiredStateFromYamlWithBaseDir` (`pkg/app/app.go:932`, covering both callers
|
||||
incl. nested helmfiles), with `helmfile.render`/`helmfile.parse` children in
|
||||
`two_pass_renderer.go` parented via the loader-struct `ctx` field (§4.4 step 1) —
|
||||
rendering is frequently the hidden time sink.
|
||||
4. **Per-release operations** — release loops in `pkg/state/state.go` (phase 2, §4.4).
|
||||
5. **Hooks** — `pkg/event/bus.go` `Trigger` (`pkg/event/bus.go:56`): span `helmfile.hook`
|
||||
with `hook.event`, `hook.name` attributes; naturally parents the `os.exec` span of the
|
||||
hook command once the §4.4 bridge is in place.
|
||||
6. **(Phase 2)** kubedog wait spans, vals/remote secret resolution, `helm repo` retry loops.
|
||||
|
||||
## 5. Span taxonomy
|
||||
|
||||
| Span name | Attributes (beyond standard `otel.*`) | Notes |
|
||||
|---|---|---|
|
||||
| `helmfile <subcommand>` (root) | `helmfile.command`, `helmfile.file`, `helmfile.environment`, `helmfile.selectors`, `helmfile.exit_code` | `error` status + recorded error on failure. Service identity (`service.name`, `service.version`) lives on the OTel resource, not on spans |
|
||||
| `helmfile.discover_states` | `helmfile.path` | `findDesiredStateFiles` (`pkg/app/app.go:1642`) |
|
||||
| `helmfile.load` | `helmfile.state_file` | one per file in `helmfile.d`, nested helmfiles |
|
||||
| `helmfile.render` | `helmfile.state_file`, `helmfile.pass`=`values`\|`main` | two-pass rendering (`pkg/app/two_pass_renderer.go`) |
|
||||
| `helmfile.repos.update` | — | wraps `helm repo update` |
|
||||
| `helmfile.release.prepare` | `helmfile.release`, `helmfile.namespace`, `helmfile.chart`, `helmfile.chart_version` | chart pull/build/registry login |
|
||||
| `helmfile.release.sync` / `.diff` / `.template` / `.delete` / `.test` / `.lint` / `.unittest` | same as above + `helmfile.labels` | one per selected release (phase 2) |
|
||||
| `helmfile.hook` | `hook.event` (presync/…), `hook.name` | |
|
||||
| `helm.exec` / `os.exec` | `exec.command`, `exec.args` (strict-redacted, plus URL userinfo/query masking via `RedactedRef`), `exec.redacted`, `exec.exit_code` (on failure), `helm.subcommand` (helm only) | one per external process; helm classification is marker-based (wrapper binaries included); release identity comes from the parent release span. Hooks, kustomize (invisible, §12), plugin execs land here too |
|
||||
Attribute values are strings/ints only; no structured payloads, no output capture in spans
|
||||
(output already flows through logs).
|
||||
|
||||
## 6. Security and redaction
|
||||
|
||||
Traces leave the machine they run on. Ground rules, checked against what exists today:
|
||||
|
||||
1. **The existing redaction is not sufficient on its own.** The exit-error path
|
||||
(`pkg/helmexec/exit_error.go:8–20`) redacts only the argument *following* a flag whose
|
||||
name starts with `--set` (two-argument form). It does not cover the single-argument
|
||||
`--set=key=value` form, nor credential-bearing flags such as `--username` or
|
||||
`--password`. (Registry passwords themselves already travel via stdin —
|
||||
`--password-stdin`, `pkg/helmexec/exec.go` `RegistryLogin` — but usernames appear in
|
||||
args.) Therefore:
|
||||
2. **One shared redaction implementation, two profiles — spans get the strict superset
|
||||
without touching existing error output.** Extract the exit-error redaction into an
|
||||
exported helper in `pkg/helmexec` (keeping it in the domain that owns flag semantics)
|
||||
with two profiles:
|
||||
- `legacy`: byte-identical to today's exit-error behavior — the current goldens in
|
||||
`pkg/helmexec/exit_error_test.go` (which assert the exact `--set` / `*** STRIP ***`
|
||||
shape) keep passing unchanged. The exit-error path switches to this profile, so
|
||||
error messages are unchanged.
|
||||
- `strict`: the superset required for spans — all `--set*` forms including
|
||||
`--set=k=v`, plus `--username`, `--password`, `--key-file`,
|
||||
`--kube-token`, ...; positional arguments are additionally passed through
|
||||
`helmexec.RedactedRef`, which masks go-getter forced forms (`git::`,
|
||||
`s3::`), whole URL userinfo, and credential-bearing query parameters,
|
||||
failing closed for malformed references. The previous token is always
|
||||
read from the original input so adjacent secret flags cannot leak.
|
||||
Both profiles are the same code path, so the span view is guaranteed at least as
|
||||
redacted as the error view. *Unifying* the two profiles (i.e. tightening exit-error
|
||||
messages too) would change observable output and is deliberately deferred to a
|
||||
separate follow-up PR with its own test updates — this proposal changes no existing
|
||||
message content.
|
||||
3. **Never record**: `vals://`-resolved values, environment variables, exporter headers.
|
||||
`OTEL_EXPORTER_OTLP_HEADERS` is the only place collector credentials live; helmfile
|
||||
never logs it.
|
||||
4. Chart/repo URLs go through the existing `redactedURL` (`pkg/helmexec/exec.go:184`) —
|
||||
credentials embedded in URLs are masked.
|
||||
5. Release *names*, namespaces, chart names, and label selectors are assumed non-secret
|
||||
(consistent with existing helmfile log output).
|
||||
6. Note for reviewers: `execer.exec` today logs the *full unredacted* command line at Debug
|
||||
level (`pkg/helmexec/exec.go:1218`). Span attributes deliberately do **not** mirror that
|
||||
log line; §10 pins this with a redaction test.
|
||||
|
||||
## 7. Performance and zero-impact guarantees
|
||||
|
||||
**When disabled (the default):**
|
||||
- `telemetry.Tracer` returns the OTel no-op tracer. No-op span start/end is a few ns and
|
||||
allocation-free; provider setup, exporters, and the batch worker goroutine never start.
|
||||
- `telemetry.CommandContext()` returns `context.Background()`; `app.New` behaves exactly as
|
||||
today. No flag checks appear in hot loops.
|
||||
|
||||
**When enabled:**
|
||||
- Standard SDK BatchSpanProcessor (5s interval / 512-span batches); metrics
|
||||
use a periodic reader (default 60s, `OTEL_METRIC_EXPORT_INTERVAL`) with a
|
||||
final flush at exit. Three instruments exist — `helmfile.helm.exec.duration`,
|
||||
`helmfile.release.duration`, and `helmfile.release.count` — so metric
|
||||
cardinality stays tiny by default (bounded by subcommands and verbs, never
|
||||
by release names); `HELMFILE_OTEL_METRICS_PER_RELEASE` opts into
|
||||
name/namespace dimensions for bounded CI runs. A
|
||||
`sync --concurrency=16` run produces at most one span per helm invocation plus one per
|
||||
release — hundreds, not tens of thousands. Export happens off the critical path; the
|
||||
only synchronous cost is the ≤5s shutdown flush, paid only when tracing is on.
|
||||
- SDK spans and exporters are goroutine-safe; helmfile's parallel release workers need no
|
||||
extra locking.
|
||||
|
||||
**Functional-impact checklist (the review criteria for every PR in §11):**
|
||||
1. No `helmexec.Interface` signature changes in any phase; `getHelm()` unchanged.
|
||||
2. `app.New` changes one line (`Background()` → `telemetry.CommandContext()`), no call-site
|
||||
churn; cancellation semantics identical.
|
||||
3. Kubedog and hook bridging uses `context.WithoutCancel`, which drops cancellation and
|
||||
keeps values — the swapped-out parents (`Background`/`TODO`) never propagated
|
||||
cancellation either, so SIGINT/timeout behavior is unchanged. (Superseded for kubedog
|
||||
by #2791, which re-roots tracking under the app cancel context; hooks stay bridged.)
|
||||
4. Telemetry setup or export failures never fail or slow the run (warning log only, export
|
||||
off the critical path).
|
||||
5. All pre-existing behavior, including the known cancellation gaps of §4.3 and the exact
|
||||
content of exit-error messages (legacy redaction profile, §6.2), is preserved
|
||||
bit-for-bit; gap fixes and redaction unification are out of scope and filed separately.
|
||||
|
||||
## 8. Dependency impact
|
||||
|
||||
The OTel libraries are **already in the module graph as indirect dependencies**, required
|
||||
transitively by existing direct dependencies (`helm.sh/helm/v4` v4.2.4 requires
|
||||
`go.opentelemetry.io/otel` v1.44.0; helm v3 and `helmfile/vals` also carry otel modules).
|
||||
Every module this proposal would import — `otel`, `otel/trace`, `otel/sdk`,
|
||||
`otel/exporters/otlp/otlptrace/otlptracegrpc`, `.../otlptracehttp`,
|
||||
`otel/exporters/stdout/stdouttrace`, and `contrib/exporters/autoexport` v0.67.0 — is
|
||||
already pinned in `go.sum` (verified), so promoting them to direct requires brings
|
||||
**zero new modules to download**. No conflict with existing OTel usage in the process:
|
||||
none of helmfile's direct dependencies registers OTel globals in library code — verified
|
||||
no `SetTracerProvider` call sites under helm v3/v4 `pkg/`, vals, or chartify (helm's own
|
||||
OTel wiring, where present, lives in its CLI layer, not the libraries helmfile imports).
|
||||
Choosing `autoexport` (§3.2) also means
|
||||
helmfile maintains no exporter-construction code; its `RegisterSpanExporter` hook covers
|
||||
any future backend (e.g. zipkin) without helmfile changes. Note the resulting defaults:
|
||||
`http/protobuf` on `localhost:4318` unless the user overrides the protocol/endpoint.
|
||||
|
||||
## 9. Code layout of the change
|
||||
|
||||
```
|
||||
cmd/root.go // --otel-tracing flag, Setup call, root span, shutdown handoff
|
||||
main.go // shutdown on both exit and signal paths
|
||||
pkg/config/global.go // OtelTracing option (+ accessor on GlobalImpl)
|
||||
pkg/envvar/const.go // OtelTracing = "HELMFILE_OTEL_TRACING"
|
||||
pkg/telemetry/… // new package (§4.1)
|
||||
pkg/app/app.go // app.New: Background() → telemetry.CommandContext(); helmfile.load span; loader ctx wiring
|
||||
pkg/app/desired_state_file_loader.go // ctx field on the unexported desiredStateLoader struct (render-span parent)
|
||||
pkg/app/two_pass_renderer.go // helmfile.render/helmfile.parse spans
|
||||
pkg/helmexec/runner.go // helm.exec / os.exec spans (single choke point)
|
||||
pkg/helmexec/redact.go // shared args redaction, legacy+strict profiles (extracted from exit_error.go)
|
||||
pkg/helmexec/exit_error.go // calls shared helper with legacy profile (output byte-identical)
|
||||
pkg/helmexec/context.go // HelmContext.Ctx field (phase 2)
|
||||
pkg/helmexec/exec.go // execCtx funnel beside exec/execStdIn (phase 2)
|
||||
pkg/state/state.go // WithoutCancel bridges (both event.Bus constructions; the kubedog call sites moved to st.releaseCancelContext() in #2791); release spans (phase 2)
|
||||
pkg/state/helmx.go // (no change — bridge happens at its caller)
|
||||
pkg/event/bus.go // optional Ctx field consumed by the default runner
|
||||
docs/experimental-features.md // feature entry → promoted out when stable
|
||||
docs/otel.md // user guide (config, backends, CI recipes, sample trace)
|
||||
```
|
||||
|
||||
## 10. Testing strategy
|
||||
|
||||
1. **Unit (`pkg/telemetry`)**: table-driven tests for enabled/disabled no-op guarantees,
|
||||
`CommandContext()` identity, default resource attributes (`service.name=helmfile`,
|
||||
`service.version` from `pkg/app/version`), and propagator extraction of `TRACEPARENT`.
|
||||
Global state reset via `export_test.go` so tests stay isolated.
|
||||
2. **Redaction (`pkg/helmexec`)**: table-driven tests for both profiles — `legacy` pinned
|
||||
byte-identical by the existing `exit_error_test.go` goldens; `strict` covering `--set v`
|
||||
and `--set=k=v`, `--set-string`/`--set-file`/`--set-json`, `--username`/`--password`,
|
||||
benign flags untouched. Pins §6.6.
|
||||
3. **Span-hierarchy golden tests**: drive `App` with the existing fake helm
|
||||
(`pkg/exectest/helm.go`, pre-seeded into `App.helms` the way current app tests do) and
|
||||
an in-memory exporter; assert the span tree (names, parent
|
||||
links, order) for `template`/`sync` over a small fixture — catches context-plumbing
|
||||
regressions. Includes an **orphan-span regression test** for the kubedog and hook paths
|
||||
(§4.3): every exported span must have the root span as an ancestor.
|
||||
4. **OTLP end-to-end**: an `httptest` server speaking OTLP/HTTP+protobuf, pointed at by
|
||||
`OTEL_EXPORTER_OTLP_ENDPOINT`; decode exported payloads (`go.opentelemetry.io/proto/otlp`,
|
||||
already in the graph) and assert count/attributes. Runs in unit-test context — no
|
||||
external collector needed in CI.
|
||||
5. **Lifecycle tests**: shutdown flushes on command failure and on SIGINT, so spans never
|
||||
vanish on failing deploys — the case users care about most. (Requires extracting
|
||||
`main.go`'s signal/select/exit logic into a small pure function; the extraction itself
|
||||
is behavior-preserving and covered by the same tests.) Also covers the nil-shutdown
|
||||
race noted in §4.2.
|
||||
6. **Regression suite**: existing `make test` must pass unchanged with tracing compiled in
|
||||
but disabled. Note the existing `pkg/app` tests construct `&App{...}` literals directly
|
||||
and never call `app.New`, so they do *not* guard the §4.2 one-liner — PR 1 adds a
|
||||
targeted test asserting `app.New` roots cancellation (and the span) exactly as before.
|
||||
|
||||
## 11. Documentation & rollout
|
||||
|
||||
1. `docs/otel.md` — user guide: enabling, env vars, collector recipes (Jaeger all-in-one,
|
||||
Grafana Tempo, vendor SaaS), CI correlation via `TRACEPARENT`, sample trace reading.
|
||||
Linked from `docs/index.md`.
|
||||
2. Feature listed under **experimental** in `docs/experimental-features.md` for one or two
|
||||
minor releases (feedback on span taxonomy is the main thing that may change), then
|
||||
promoted to stable with a CHANGELOG entry.
|
||||
3. PR sequence (each independently shippable and revertible, each measured against the
|
||||
§7 functional-impact checklist):
|
||||
- **PR 1**: `pkg/telemetry` + flag/env + root span + lifecycle + `app.New` one-liner +
|
||||
docs + tests (§4.1–4.2).
|
||||
- **PR 2**: `ShellRunner` instrumentation + shared redaction extraction/extension (§4.4
|
||||
phase-1 steps 2–3, §6) — the core value.
|
||||
- **PR 3**: load/render spans + hook bridging + per-release spans via
|
||||
`HelmContext.Ctx`/`execCtx` (§4.4 phase 2).
|
||||
- **PR 4 (post-stabilization)**: metrics (e.g. `helmfile.helm.exec.duration` histogram,
|
||||
`helmfile.release.count` by result), kubedog/vals spans.
|
||||
|
||||
## 12. Future work (explicitly out of scope for v1)
|
||||
|
||||
- ~~Metrics pipeline on the same provider (`otel/sdk/metric` with the same env-var config)~~ — delivered: `helmfile.helm.exec.duration`, `helmfile.release.count`, and `helmfile.release.duration` via `autoexport.NewMetricReader` (`OTEL_METRICS_EXPORTER`); per-release name/namespace attributes on the duration histogram are opt-in via `HELMFILE_OTEL_METRICS_PER_RELEASE` (bounded dimensions by default).
|
||||
- `TRACEPARENT` injection into helm subprocess env so chart-test hooks / plugins can extend
|
||||
the helmfile trace.
|
||||
- Subprocesses started inside `github.com/helmfile/chartify` (kustomize, and any helm calls
|
||||
chartify makes) are invisible to `ShellRunner` instrumentation; options are a wrapper
|
||||
span around each chartify call in `pkg/state`, or upstream OTel support in chartify.
|
||||
- Log-to-trace correlation (zap OTel appender).
|
||||
- Remaining per-release instrumentation: the flag-preparation loops
|
||||
(`prepareSyncReleases`/`prepareDiffReleases`) and the exec nesting for
|
||||
`helmexec.Interface` methods that take no `HelmContext`
|
||||
(TemplateRelease/Lint/Unittest/Fetch) — their exec spans currently sit flat
|
||||
under the load span while the release span covers the surrounding work.
|
||||
- Fixing the kubedog/hook cancellation gaps (§4.3) — separate issues filed from this design.
|
||||
|
||||
## 13. Alternatives considered
|
||||
|
||||
| Alternative | Why rejected |
|
||||
|---|---|
|
||||
| Structured logs + collector-side parsing | No hierarchy/timing guarantees; every backend needs custom parsing; poor UX. |
|
||||
| Prometheus metrics only | Shows counts/durations but not critical paths or nesting; the request is explicitly about tracing where time goes. |
|
||||
| Hand-rolled exporter selection in helmfile | `autoexport` already exists in the dependency graph and implements the spec env vars (verified: v0.67.0 supports `otlp`/`console`/`none`, protocol dispatch, `none` detection); hand-rolled code is pure maintenance burden and would drift from the spec. |
|
||||
| Helmfile-specific env vars for endpoint/headers etc. | Duplicates the OTel spec; standard vars are already what platform teams configure. |
|
||||
| Full `context.Context` refactor of `pkg/state` first | Large, risky churn unrelated to the feature; the `HelmContext.Ctx`/`execCtx` design achieves nesting without it. |
|
||||
| Using the existing `WithContext` clone for per-release spans | The cached, shared execer (`a.helms`) serves concurrent workers; per-release contexts must travel with the per-call `HelmContext` parameter, not via instance substitution. |
|
||||
| Config-file (`helmfile.yaml`) telemetry settings | Telemetry is an operational/platform concern, not state authoring; flag+env matches how it's injected in CI. |
|
||||
|
||||
## 14. Open questions
|
||||
|
||||
1. **Propagation into helm subprocesses**: should helmfile inject `TRACEPARENT` into the
|
||||
child process env (opt-in) so hooks/plugins can continue the trace? (§12)
|
||||
2. **Span-name taxonomy stability**: do we commit to the §5 names as stable API for
|
||||
dashboard authors during the experimental window, or reserve the right to rename?
|
||||
Proposal: rename freely while experimental, freeze on promotion.
|
||||
3. **`helmfile.d` parallel mode**: state-file spans are siblings under the root span — is a
|
||||
synthetic `helmfile.parallel` grouping span wanted, or does flat suffice?
|
||||
4. **Trace output when `--log-level=debug`**: duplicate a compact span tree to stderr at
|
||||
shutdown for quick local triage without a collector?
|
||||
5. **Root span noise for trivial commands**: `helmfile version`/`help` also run
|
||||
`PersistentPreRunE` — export a root span for them, or skip? (Proposal: skip via a
|
||||
short denylist; cosmetic.)
|
||||
@@ -46,6 +46,20 @@ On `helmfile [delete|destroy]`, deletions happen in the reverse order.
|
||||
|
||||
That is, `myapp1` and `myapp2` are deleted first, then `servicemesh`, and finally `logging`.
|
||||
|
||||
### Failure handling and `continueOnError`
|
||||
|
||||
By default, `helmfile [sync|apply]` stops on the first release failure (fail-fast). Set `continueOnError: true` on a release to keep processing independent releases after it fails:
|
||||
|
||||
```yaml
|
||||
- name: myapp1
|
||||
chart: charts/myapp
|
||||
continueOnError: true
|
||||
needs:
|
||||
- servicemesh
|
||||
```
|
||||
|
||||
When a release with `continueOnError` fails, releases in other branches of the DAG are still processed, while releases that (transitively) depend on the failed release are skipped with an error like `release "myapp2" was skipped because dependency "myapp1" failed`. The command still exits non-zero, and Helm's own rollback behavior (`atomic`, `rollbackOnFailure`, ...) is unaffected. See [Continue On Error](configuration.md#continue-on-error) for details.
|
||||
|
||||
### Selectors and `needs`
|
||||
|
||||
When using selectors/labels, `needs` are ignored by default. This behaviour can be overruled with a few parameters:
|
||||
|
||||
+63
-1
@@ -54,4 +54,66 @@ service:
|
||||
login: svc-login # fetched from vault
|
||||
password: pass
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
|
||||
## Disabling vals
|
||||
|
||||
You can disable the built-in vals processing using environment variables:
|
||||
|
||||
### Pass-through mode
|
||||
|
||||
Set `HELMFILE_DISABLE_VALS=true` to disable internal vals processing. Any `ref+` values will pass through unchanged, allowing you to validate them with a policy tool such as [conftest](https://www.conftest.dev/) before they are resolved:
|
||||
|
||||
```bash
|
||||
HELMFILE_DISABLE_VALS=true helmfile template | conftest test -
|
||||
```
|
||||
|
||||
### Strict mode
|
||||
|
||||
Set `HELMFILE_DISABLE_VALS_STRICT=true` to disable vals and error if any `ref+` values are detected. This is useful when you want to prevent users from using vals references:
|
||||
|
||||
```bash
|
||||
HELMFILE_DISABLE_VALS_STRICT=true helmfile sync
|
||||
# Error: vals is disabled via HELMFILE_DISABLE_VALS_STRICT environment variable
|
||||
```
|
||||
|
||||
Note: If both are set, strict mode takes precedence.
|
||||
|
||||
Strict mode detects any `ref+<provider>://` or `secretref+<provider>://` expression, including nested ones in maps and arrays. Plain strings that merely contain the text `ref+` (without a provider scheme) are not vals references and do not trigger the error.
|
||||
|
||||
### Validating ref+ expressions with conftest
|
||||
|
||||
You can use `HELMFILE_DISABLE_VALS=true` with [conftest](https://www.conftest.dev/) to validate that all `ref+` expressions conform to your security policy before processing them.
|
||||
|
||||
Example rego policy (`policy/vals_refs.rego`):
|
||||
|
||||
```rego
|
||||
package main
|
||||
|
||||
allowed_refs := {
|
||||
"ref+tfstates3://my-terraform-state/networking/eu-west-2/vpc/vpc_id",
|
||||
"ref+tfstates3://my-terraform-state/networking/eu-west-2/vpc/private_subnet_ids",
|
||||
"ref+tfstates3://my-terraform-state/platform/eu-west-2/eks/cluster_endpoint",
|
||||
}
|
||||
|
||||
deny[msg] {
|
||||
value := input[_]
|
||||
startswith(value, "ref+tfstates3://")
|
||||
not allowed_refs[value]
|
||||
msg := sprintf("ref+ expression references an unapproved tfstates3 URI: %s", [value])
|
||||
}
|
||||
|
||||
deny[msg] {
|
||||
value := input[_]
|
||||
startswith(value, "ref+")
|
||||
not startswith(value, "ref+tfstates3://")
|
||||
msg := sprintf("only tfstates3 ref+ expressions are permitted, got: %s", [value])
|
||||
}
|
||||
```
|
||||
|
||||
Run against your rendered values:
|
||||
|
||||
```bash
|
||||
HELMFILE_DISABLE_VALS=true helmfile template | conftest test -
|
||||
```
|
||||
|
||||
@@ -2,7 +2,7 @@ Babel==2.17.0
|
||||
click==8.1.2
|
||||
ghp-import==2.0.2
|
||||
gitdb==4.0.9
|
||||
GitPython==3.1.58
|
||||
GitPython==3.1.62
|
||||
importlib-metadata==4.11.3
|
||||
Jinja2==3.1.6
|
||||
Markdown==3.8.1
|
||||
|
||||
@@ -62,6 +62,8 @@ 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_DISABLE_VALS` - disable internal vals processing, `ref+` values pass through unchanged for use with external vals, expecting `true` lower case
|
||||
* `HELMFILE_DISABLE_VALS_STRICT` - disable vals and error if any `ref+` values are detected, expecting `true` lower case
|
||||
* `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
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
module github.com/helmfile/helmfile
|
||||
|
||||
go 1.26.2
|
||||
go 1.27.1
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2
|
||||
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.38
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.107.3
|
||||
github.com/aws/aws-sdk-go-v2/config v1.33.9
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
|
||||
github.com/go-test/deep v1.1.1
|
||||
github.com/gofrs/flock v0.13.0
|
||||
github.com/fatih/color v1.19.0
|
||||
github.com/go-test/deep v1.1.2
|
||||
github.com/gofrs/flock v0.13.1
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/gookit/color v1.6.1
|
||||
github.com/gookit/color v1.6.3
|
||||
github.com/gosuri/uitable v0.0.4
|
||||
github.com/hashicorp/go-cty-funcs v0.1.0
|
||||
github.com/hashicorp/go-getter/v2 v2.2.3
|
||||
github.com/hashicorp/hcl/v2 v2.24.0
|
||||
github.com/helmfile/chartify v0.28.2
|
||||
github.com/helmfile/vals v0.46.0
|
||||
github.com/sashabaranov/go-openai v1.42.0
|
||||
github.com/hashicorp/go-getter/v2 v2.2.5
|
||||
github.com/hashicorp/hcl/v2 v2.25.0
|
||||
github.com/helmfile/chartify v0.29.0
|
||||
github.com/helmfile/vals v0.47.0
|
||||
github.com/mattn/go-runewidth v0.0.31
|
||||
github.com/otiai10/copy v1.14.1
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/sashabaranov/go-openai v1.43.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/stretchr/testify v1.12.1
|
||||
@@ -30,113 +34,60 @@ require (
|
||||
github.com/werf/kubedog v0.13.1-0.20260217150136-ed58edf34eac
|
||||
github.com/zclconf/go-cty v1.19.0
|
||||
github.com/zclconf/go-cty-yaml v1.2.0
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.72.0
|
||||
go.opentelemetry.io/otel v1.47.0
|
||||
go.opentelemetry.io/otel/metric v1.47.0
|
||||
go.opentelemetry.io/otel/sdk v1.47.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.47.0
|
||||
go.opentelemetry.io/otel/trace v1.47.0
|
||||
go.opentelemetry.io/proto/otlp v1.11.1
|
||||
go.szostok.io/version v1.2.0
|
||||
go.uber.org/zap v1.28.0
|
||||
go.yaml.in/yaml/v2 v2.4.4
|
||||
go.yaml.in/yaml/v3 v3.0.5
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/term v0.45.0
|
||||
golang.org/x/sync v0.24.0
|
||||
golang.org/x/term v0.47.0
|
||||
google.golang.org/protobuf v1.36.12
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
helm.sh/helm/v3 v3.21.4
|
||||
helm.sh/helm/v4 v4.2.4
|
||||
k8s.io/apimachinery v0.36.3
|
||||
k8s.io/client-go v0.36.3
|
||||
helm.sh/helm/v3 v3.22.1
|
||||
helm.sh/helm/v4 v4.3.1
|
||||
k8s.io/apimachinery v0.37.1
|
||||
k8s.io/client-go v0.37.1
|
||||
)
|
||||
|
||||
replace (
|
||||
// kubedog's flagger dependency still imports k8s.io/api/autoscaling/v2beta2,
|
||||
// while Helm 4.2.2 requires k8s.io/apimachinery/content.IsPathSegmentName from v0.36+.
|
||||
// which was removed in k8s.io/api v0.36+, while Helm 4 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.11.0 // indirect
|
||||
cloud.google.com/go/storage v1.64.0 // 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.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
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/a8m/envsubst v1.4.3 // indirect
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
|
||||
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.12.1 // 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.2.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.23.0 // indirect
|
||||
github.com/goware/prefixer v0.0.0-20160118172347-395022866408 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/go-slug v0.16.8 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
||||
github.com/hashicorp/go-tfe v1.103.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.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.15 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.28
|
||||
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
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/otiai10/copy v1.14.1
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
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.57.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/api v0.291.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260622175928-b703f567277d // indirect
|
||||
google.golang.org/grpc v1.82.1 // indirect
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||
gopkg.in/ini.v1 v1.67.3 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
// controller-runtime v0.25.x (required by Helm 4.3.1) needs k8s.io/client-go >= v0.36
|
||||
// for toolscache.DoneChecker, which conflicts with the client-go pin above.
|
||||
// Helm only uses pkg/client/apiutil, whose API is unchanged since v0.24.x.
|
||||
sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.24.1
|
||||
)
|
||||
|
||||
require (
|
||||
al.essio.dev/pkg/shellescape v1.6.0 // indirect
|
||||
cel.dev/expr v0.25.2 // indirect
|
||||
cloud.google.com/go/auth v0.22.0 // indirect
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
cloud.google.com/go/auth v0.23.2 // 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/iam v1.12.0 // indirect
|
||||
cloud.google.com/go/kms v1.33.0 // indirect
|
||||
cloud.google.com/go/longrunning v1.2.0 // indirect
|
||||
cloud.google.com/go/monitoring v1.29.0 // indirect
|
||||
cloud.google.com/go/monitoring v1.30.0 // indirect
|
||||
cloud.google.com/go/secretmanager v1.21.0 // indirect
|
||||
cloud.google.com/go/storage v1.67.1 // indirect
|
||||
filippo.io/age v1.3.2 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // 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/1password/onepassword-sdk-go v0.4.1 // indirect
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 // 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
|
||||
@@ -145,16 +96,25 @@ require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // 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.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
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect
|
||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||
github.com/DelineaXPM/tss-sdk-go/v3 v3.0.2 // indirect
|
||||
github.com/DelineaXPM/delinea-common v1.0.0 // indirect
|
||||
github.com/DelineaXPM/tss-sdk-go/v3 v3.1.0 // indirect
|
||||
github.com/DopplerHQ/cli v0.5.11-0.20230908185655-7aef4713e1a4 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.34.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 // indirect
|
||||
github.com/MakeNowJust/heredoc v1.0.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.4.1 // indirect
|
||||
github.com/a8m/envsubst v1.4.3 // 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
|
||||
@@ -166,27 +126,30 @@ 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.43.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.37 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.38 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.47.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.20.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.28 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.38 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.38 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.39 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.31 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.38 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.39 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.54.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.44.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.5.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.73.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.33.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.45.7 // indirect
|
||||
github.com/aws/smithy-go v1.27.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.60.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.49.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.10.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.76.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.38.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.51.4 // indirect
|
||||
github.com/aws/smithy-go v1.28.5 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
|
||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
@@ -198,12 +161,13 @@ require (
|
||||
github.com/cloudflare/circl v1.6.4 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/cyberark/conjur-api-go v0.15.5 // indirect
|
||||
github.com/cyberark/conjur-api-go v0.15.7 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.7.0 // indirect
|
||||
github.com/danieljoos/wincred v1.2.2 // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/dominikbraun/graph v0.23.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect
|
||||
github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
|
||||
@@ -212,9 +176,10 @@ 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.1.0 // indirect
|
||||
github.com/fluxcd/cli-utils v1.2.1 // indirect
|
||||
github.com/fluxcd/cli-utils v1.2.2 // indirect
|
||||
github.com/fluxcd/flagger v1.36.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/fujiwara/tfstate-lookup v1.12.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
|
||||
github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e // indirect
|
||||
github.com/getsops/sops/v3 v3.13.2 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
@@ -223,74 +188,102 @@ require (
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/go-logr/logr v1.4.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/analysis v0.25.5 // indirect
|
||||
github.com/go-openapi/errors v0.22.8 // indirect
|
||||
github.com/go-openapi/jsonpointer v1.0.0 // indirect
|
||||
github.com/go-openapi/jsonreference v1.0.0 // indirect
|
||||
github.com/go-openapi/loads v0.25.0 // indirect
|
||||
github.com/go-openapi/runtime v0.33.0 // indirect
|
||||
github.com/go-openapi/runtime/server-middleware v0.30.0 // indirect
|
||||
github.com/go-openapi/spec v0.22.9 // indirect
|
||||
github.com/go-openapi/strfmt v0.27.0 // 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.27.3 // indirect
|
||||
github.com/go-openapi/swag/fileutils v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.26.1 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/loading v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/mangling v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/netutils v0.24.0 // indirect
|
||||
github.com/go-openapi/swag/pools v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.27.3 // indirect
|
||||
github.com/go-openapi/validate v0.26.1 // indirect
|
||||
github.com/go-openapi/analysis v1.0.0 // indirect
|
||||
github.com/go-openapi/errors v0.22.9 // indirect
|
||||
github.com/go-openapi/jsonpointer v1.0.1 // indirect
|
||||
github.com/go-openapi/jsonreference v1.0.1 // indirect
|
||||
github.com/go-openapi/loads v0.25.2 // indirect
|
||||
github.com/go-openapi/runtime v0.33.2 // indirect
|
||||
github.com/go-openapi/runtime/server-middleware v0.33.2 // indirect
|
||||
github.com/go-openapi/spec v1.0.0 // indirect
|
||||
github.com/go-openapi/strfmt v0.27.2 // indirect
|
||||
github.com/go-openapi/swag v0.29.2 // indirect
|
||||
github.com/go-openapi/swag/cmdutils v0.29.2 // indirect
|
||||
github.com/go-openapi/swag/conv v0.29.2 // indirect
|
||||
github.com/go-openapi/swag/fileutils v0.29.2 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.29.2 // indirect
|
||||
github.com/go-openapi/swag/loading v0.29.2 // indirect
|
||||
github.com/go-openapi/swag/mangling v0.29.2 // indirect
|
||||
github.com/go-openapi/swag/netutils v0.29.2 // indirect
|
||||
github.com/go-openapi/swag/pools v0.29.2 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.29.2 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.29.2 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.29.2 // indirect
|
||||
github.com/go-openapi/validate v1.0.0 // indirect
|
||||
github.com/go-resty/resty/v2 v2.13.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/gnostic-models v0.7.1 // indirect
|
||||
github.com/google/go-jsonnet v0.22.0 // indirect
|
||||
github.com/google/go-querystring v1.2.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.19 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.20 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.24.0 // indirect
|
||||
github.com/goware/prefixer v0.0.0-20160118172347-395022866408 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/go-safetemp v1.0.0 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
||||
github.com/hashicorp/go-slug v0.16.8 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
|
||||
github.com/hashicorp/go-tfe v1.103.0 // indirect
|
||||
github.com/hashicorp/go-version v1.8.0 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hashicorp/hcp-sdk-go v0.174.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
|
||||
github.com/hashicorp/hcp-sdk-go v0.175.0 // indirect
|
||||
github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e // indirect
|
||||
github.com/hashicorp/vault/api v1.23.0 // indirect
|
||||
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.202 // indirect
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20251118225945-96ee0021ea0f // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/infisical/go-sdk v0.8.0 // indirect
|
||||
github.com/itchyny/gojq v0.12.19 // 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
|
||||
github.com/klauspost/compress v1.19.1 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lib/pq v1.12.3 // indirect
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
||||
github.com/muesli/termenv v0.15.1 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.1 // indirect
|
||||
github.com/openbao/openbao/api/v2 v2.6.0 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.2 // indirect
|
||||
github.com/openbao/openbao/api/v2 v2.7.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
@@ -299,14 +292,24 @@ 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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.24.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.3 // indirect
|
||||
github.com/prometheus/common v0.72.0 // indirect
|
||||
github.com/prometheus/otlptranslator v1.0.0 // indirect
|
||||
github.com/prometheus/procfs v0.22.0 // 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
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/samber/lo v1.39.0 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.37 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.10.2 // indirect
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
|
||||
github.com/sony/gobreaker v0.5.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.8.1 // indirect
|
||||
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect
|
||||
github.com/tetratelabs/wazero v1.12.0 // indirect
|
||||
@@ -316,6 +319,7 @@ require (
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
|
||||
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/urfave/cli v1.22.17 // indirect
|
||||
github.com/werf/logboek v0.6.1 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
@@ -324,45 +328,65 @@ require (
|
||||
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.95.0 // indirect
|
||||
github.com/yandex-cloud/go-sdk v0.32.0 // indirect
|
||||
github.com/yandex-cloud/go-genproto v0.121.0 // indirect
|
||||
github.com/yandex-cloud/go-sdk v0.35.0 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
github.com/zalando/go-keyring v0.2.6 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.9 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.72.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
|
||||
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.23.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.23.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.69.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.23.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.47.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.47.0 // indirect
|
||||
go.opentelemetry.io/otel/log v1.47.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/log v1.47.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.54.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
|
||||
golang.org/x/mod v0.37.0 // indirect
|
||||
golang.org/x/tools v0.47.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df // indirect
|
||||
golang.org/x/crypto v0.58.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect
|
||||
golang.org/x/mod v0.41.0 // indirect
|
||||
golang.org/x/net v0.60.0 // indirect
|
||||
golang.org/x/oauth2 v0.37.0 // indirect
|
||||
golang.org/x/sys v0.49.0 // indirect
|
||||
golang.org/x/text v0.43.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/tools v0.51.0 // indirect
|
||||
google.golang.org/api v0.296.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect
|
||||
google.golang.org/grpc v1.84.0 // 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/ini.v1 v1.67.3 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
k8s.io/api v0.36.3 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.36.2 // indirect
|
||||
k8s.io/apiserver v0.36.2 // indirect
|
||||
k8s.io/cli-runtime v0.36.2 // indirect
|
||||
k8s.io/component-base v0.36.2 // indirect
|
||||
k8s.io/api v0.37.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.37.1 // indirect
|
||||
k8s.io/apiserver v0.37.1 // indirect
|
||||
k8s.io/cli-runtime v0.37.1 // indirect
|
||||
k8s.io/component-base v0.37.1 // indirect
|
||||
k8s.io/klog/v2 v2.140.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
|
||||
k8s.io/kubectl v0.36.2 // indirect
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect
|
||||
k8s.io/kubectl v0.37.1 // indirect
|
||||
k8s.io/utils v0.0.0-20260626114624-be93311217bd // indirect
|
||||
oras.land/oras-go/v2 v2.6.2 // indirect
|
||||
sigs.k8s.io/controller-runtime v0.24.1 // indirect
|
||||
sigs.k8s.io/controller-runtime v0.25.2 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.21.1 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.21.1 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.21.2 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.3 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA=
|
||||
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
|
||||
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M=
|
||||
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
|
||||
c2sp.org/CCTV/age v0.0.0-20260829155415-4448f2097b2d h1:Blprhc2SbChNZtWcU+BLTM4YdoqYAS9V7cJgOwJKyAs=
|
||||
c2sp.org/CCTV/age v0.0.0-20260829155415-4448f2097b2d/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
|
||||
cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
|
||||
cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||
cloud.google.com/go/auth v0.22.0 h1:Xp9wAKkLoeaYb5pYZZoQGz4E9sdPxIbzS3gywZE3ciQ=
|
||||
cloud.google.com/go/auth v0.22.0/go.mod h1:M9o2Oz+YI2jAfxewJgb1vyI3vceHF+eohmxyzmrl+9s=
|
||||
cloud.google.com/go/auth v0.23.2 h1:pxSCpfiji41hpzpPdMCftEUCezpgpqmmDdYiAjCKXxo=
|
||||
cloud.google.com/go/auth v0.23.2/go.mod h1:4DhBRcqvtljQN3dJ57qtqbib5ZGCYE5f2crfiiC2EM0=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cloud.google.com/go/iam v1.11.0 h1:KieQ9Pb+LLPak1O3Rv3GgCxhnmkYf7Xyh0P5HfF1jFM=
|
||||
cloud.google.com/go/iam v1.11.0/go.mod h1:KP+nKGugNJW4LcLx1uEZcq1ok5sQHFaQehQNl4QDgV4=
|
||||
cloud.google.com/go/iam v1.12.0 h1:Aki3bX9aHUDKPHfnRJfDcTdVedvy6quGBQcTqx3DRXk=
|
||||
cloud.google.com/go/iam v1.12.0/go.mod h1:FEZ4lXpADAC2AIpQY7LANNjjwyQ2jK439CI2VaD+sLY=
|
||||
cloud.google.com/go/kms v1.33.0 h1:pG0X78m212b2pv9N4fdMoUO69LuZGQ9kSvn8sHBOFAo=
|
||||
cloud.google.com/go/kms v1.33.0/go.mod h1:CSGvW6GnMQbY+1nOHcIzhMtHSbExXlOmCKjWtYVjcpA=
|
||||
cloud.google.com/go/logging v1.18.0 h1:KhzZq+1cSkPH9YUaKLLhLtQxIHitVayBmk0sGfoM9+k=
|
||||
cloud.google.com/go/logging v1.18.0/go.mod h1:ZGKnpBaURITh+g/uom2VhbiFoFWvejcrHPDhxFtU/gI=
|
||||
cloud.google.com/go/logging v1.19.0 h1:NCqhdVUg3wQ8Cobdf16FDSuTGi3+6+hdSBHrY5TsR6Q=
|
||||
cloud.google.com/go/logging v1.19.0/go.mod h1:i40NZCHC9Gqvod4yE+yQfDWwlgwW/SrshkkGibCHxcA=
|
||||
cloud.google.com/go/longrunning v1.2.0 h1:WjYH3YHBGCxGJP9M4dWGHBfXr/cFIjMkNgWcJj7/iMM=
|
||||
cloud.google.com/go/longrunning v1.2.0/go.mod h1:5KMQALFGOCtFoi2xSOA1u3H7WKlhmckgiyFw7+LGQp0=
|
||||
cloud.google.com/go/monitoring v1.29.0 h1:AHhDsFaSax1/4k+qlIDX/SDGe6hggnfXJ9dkgD9qBPY=
|
||||
cloud.google.com/go/monitoring v1.29.0/go.mod h1:72NOVjJXHY/HBfoLT0+qlCZBT059+9VXLeAnL2PeeVM=
|
||||
cloud.google.com/go/monitoring v1.30.0 h1:r/d+JUbyKmJ8b07iznuKfzVzrIXTWxHQ3lBRm3x2LlY=
|
||||
cloud.google.com/go/monitoring v1.30.0/go.mod h1:htlUR0QWVMrjFzZmN4LGnMAve9xB/eduwjmINxVZ8RM=
|
||||
cloud.google.com/go/secretmanager v1.21.0 h1:e56QQaKWRyzBdUz40AeZaio/ZHAl268cFx3QFAAw9CY=
|
||||
cloud.google.com/go/secretmanager v1.21.0/go.mod h1:+nlV+GYqTD8DM+x7Kk3UF7ZPYgdYMowrkZxAmMXORQ8=
|
||||
cloud.google.com/go/storage v1.64.0 h1:KLpxI/oX9LxeRsNqn877d2WyeT3ryiEwnGt8pwcSPZg=
|
||||
cloud.google.com/go/storage v1.64.0/go.mod h1:lWyAtwvDZHdL3k68WVKbESP6bmWaV23ZJJ/JEVw/ZaQ=
|
||||
cloud.google.com/go/storage v1.67.1 h1:cDeyjY1ik/2IBs4/nLytL6Orrtbi5M0G6beyGRl/9EQ=
|
||||
cloud.google.com/go/storage v1.67.1/go.mod h1:UsS9OgFg/XHOSYakQ8ZtLWWeyGkk1WnmD/GsGfN0BHM=
|
||||
cloud.google.com/go/trace v1.16.0 h1:GmQovzFc5F0CNfl0VLgL64aoTtu7xsM0YajW2GlG9+E=
|
||||
cloud.google.com/go/trace v1.16.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
filippo.io/age v1.3.1 h1:hbzdQOJkuaMEpRCLSN1/C5DX74RPcNCk6oqhKMXmZi0=
|
||||
filippo.io/age v1.3.1/go.mod h1:EZorDTYUxt836i3zdori5IJX/v2Lj6kWFU0cfh6C0D4=
|
||||
filippo.io/age v1.3.2 h1:r6RSZLFSMm6rzKepZ7ZAYkKCu14f3/Me8c7uKYh7C8c=
|
||||
filippo.io/age v1.3.2/go.mod h1:TH/Yr2sSRhCKbaH4XPxpUV0Us8Gv6txYUpiZQWz8Evk=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
@@ -40,14 +40,14 @@ filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
|
||||
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
|
||||
github.com/1Password/connect-sdk-go v1.5.3 h1:KyjJ+kCKj6BwB2Y8tPM1Ixg5uIS6HsB0uWA8U38p/Uk=
|
||||
github.com/1Password/connect-sdk-go v1.5.3/go.mod h1:5rSymY4oIYtS4G3t0oMkGAXBeoYiukV3vkqlnEjIDJs=
|
||||
github.com/1password/onepassword-sdk-go v0.3.1 h1:dz0LrYuIh/HrZ7rxr8NMymikNLBIXhyj4NBmo5Tdamc=
|
||||
github.com/1password/onepassword-sdk-go v0.3.1/go.mod h1:kssODrGGqHtniqPR91ZPoCMEo79mKulKat7RaD1bunk=
|
||||
github.com/1password/onepassword-sdk-go v0.4.1 h1:My/Q2QXemep0I0qHgGrOs7EEzpPh2QZ1/II+S3YqOG0=
|
||||
github.com/1password/onepassword-sdk-go v0.4.1/go.mod h1:j/CbzhucTywjlYrd6SE6k0LcQaFZ2l8OLBsAsOYtvD0=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
||||
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.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1 h1:zvXfGJCWvywnCA814d8ZiVyt+fm9nnTE8xSb99zRyfo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1/go.mod h1:iptorS+VYKFL2N6PnebpS91dubG35eAOEERnT4PJbQU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 h1:CU4+EJeJi3TKYWEcYuSdWsjzw0nVsK/H0MSQOiPcymU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0/go.mod h1:q0+UTSRvShwUCrR/s5HtyInYphN7Wvxb7snFM3u+SLA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0 h1:xFaZZ+IubdftrDHnGGwZ6QvQ3KHTtWl2MCK+GMt2vxs=
|
||||
@@ -96,12 +96,14 @@ 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.2 h1:8wRzxlo6fujNoDbnp6PnawY3moxqQelxpJGzTHG7Qoo=
|
||||
github.com/DelineaXPM/tss-sdk-go/v3 v3.0.2/go.mod h1:VmyoHQ25FhSVHTI3/ptQNOviNEMfCy2ALAf/3E4Eqxg=
|
||||
github.com/DelineaXPM/delinea-common v1.0.0 h1:JcD1b9uGxVpWORls6fQ4KkDGNZgGlfPPZfH5UAd4YiI=
|
||||
github.com/DelineaXPM/delinea-common v1.0.0/go.mod h1:P/a9zx/q5x1GxsICEZ+xS+DcZgMN5cE2zk/mT8WuhPE=
|
||||
github.com/DelineaXPM/tss-sdk-go/v3 v3.1.0 h1:Z8Npn0lGxAjYMgR5Wu/Di8OCfD2e1A1pnVUVL8TbYDU=
|
||||
github.com/DelineaXPM/tss-sdk-go/v3 v3.1.0/go.mod h1:RUucah2faoja0H6kqzuAdgm6KgwYtC2spZfWZlVCgqY=
|
||||
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.33.0 h1:l7+6kwRMJNwdCvYdDl7Eax+wzEYHSnNY7zrrfbhDdTA=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.34.0 h1:yzIYdwuro811Z27D3T80Wkd3rqZzb0K43nner7Eh1yE=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.34.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 h1:jLdiS1vO+XJFyDSWRHBx56r4s/NNtcl5J6KyCcWUX/w=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0/go.mod h1:8lmpHY+1VRoteiOwyrQMDt1YGXOrFKCz+1wJW7n3ODY=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0 h1:cSjUzZ7KU8hicTgzaSv9NmSyM9fTVK3y5lsBUl3wOis=
|
||||
@@ -155,50 +157,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.43.7 h1:msCzvkeYJA9ehbV8mRRmkZLo/zJg/+yDVLNtflg83hQ=
|
||||
github.com/aws/aws-sdk-go-v2 v1.43.7/go.mod h1:tXpPM+v0D1lndmga+HqqLDIzUFJlEeR21aspVklHF00=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.18 h1:LAfOuhAH331fmOjTQpAaOlH+Ftn7RzSDJ2VFwjdMMy4=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.18/go.mod h1:4e5xhuXHx1e4U9EthvbPP1r/DIMp5c2823OL8karzcM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.38 h1:n4yPHBjtQ3BrIIUyk0/LAqf/BL2iv0Tw6XZcMRzM0ps=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.38/go.mod h1:dencYsOS1R7rBy8zehCvwBYzdxxL4Q/nRK7In03wjN8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.37 h1:FJ8Iz4/xISMB/rwLlgfWujfGDFWr0oneQgtA6KPcYLY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.37/go.mod h1:Q6pWOgVUp49x4g5QVi29wHofUoICnZ+Zq4jHbRN/7ec=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.38 h1:Nqo2jU1wz5rnBM9XQyXfVD1RP8txkbP3EDx8hR/hbCE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.38/go.mod h1:PzJFHhjR2vWFKHe8HmY5Lxhvwyxnr5MERtk0nDxWNbk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.47.3 h1:HRoT/MkNi3bjO5xF4dCzLzvVmkvRGHj3/cLINDnJejk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.47.3/go.mod h1:xSl+OSI5ZUw6bSMupWc218zE5Pl+65MCCu3ySWFY99A=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 h1:oiEmPz/YMMeh/JvAE8xSxNCIsvWE6lpFe4ZPlyFykQI=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22/go.mod h1:QWUDuRi9PFjBHIU2iH1Pkldbe7zqLfXJC4wWLbF8nuQ=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.33.9 h1:Hv6bcQUJVnmuWaKjqD6Ot8sE75NrWsWV20oGT5aWle0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.33.9/go.mod h1:ivowmrZNfBQLcg4QkaItgi5wae4Bzot9RzIdbY1hfTg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.20.9 h1:SNO55PnWhaDafsOcv16lCA0kdoFMl/YlegWoVsU02s0=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.20.9/go.mod h1:+2reOTRtdD3VSBqpRDNwrZIYy8yMCYa7oK3WKTJ/I1I=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3 h1:gr3b87OLB29jymWPzp/CAyXwQRJVE+sC4Wm0qBIKHsk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3/go.mod h1:BRjf4L/Yx2hY54o+ZibPbLJyMMeSwqYQXwcJfqW66a8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.28 h1:ez4y5o7sa0uaRI8BquYOXtZpioUPhbQEh7Igm88oV9U=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.28/go.mod h1:TpmZOrQA12XKEpVypgBGZSQBsm1WUTndCiSnbDsbvug=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.38 h1:MBMg0zJ6i4TkAJ0dVFLKKn2cOkY6FkicmUDM67BRr6g=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.38/go.mod h1:9MWuJbyiUyj6eA7W1/zm1zuePDPSB3g+xcgRQeMWsXc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.38 h1:lHm4jPf3k1Lz5ZWc+Vcn3MKVwym+26kWCba9FkJ4f0Y=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.38/go.mod h1:Rn+P2XR+FbyZzjmWKjg/KUZNxmGfr5oZwh5jQiE+CzI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.39 h1:vo4xvMRs/F6h1E52qsgLqCQgWIQXgIJUauG6rlZEh4U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.39/go.mod h1:jB03R1ij/A+OE2e1dz6vgj076gd7vlYcfstAzj3HcnU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17 h1:OvYZOB3qA6zvfdRFiRFRzVSiElMYrz3GdntkXZxlp1o=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.17/go.mod h1:JgR/2Ew50ACfIWau1oeMRX59tMtC0kM+PYQGEaT04cY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.31 h1:uZOinZb+h7lZw8IYzP1z1IuEnueB76/EFkcf/fEW4Ag=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.31/go.mod h1:NRtwAM/p5VRt03TlEUs0pH3TeWamWdf4YyJpSrzPYLc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.38 h1:H/5TI1jqaHsNoDQ60UwvPvJBg4GURkinXI3Qga29t2w=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.38/go.mod h1:PTVFf+XH++7NJOky+RLBYQx0QA5NcaeEYFQ2fsi0nwo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.39 h1:HLPAVrlLDaN2boN0xJx7MgaQDNEO3Q+c9L6kl/8m47Q=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.39/go.mod h1:Pg/dVfsNkm1hsIDK/gMvCKtmyNfNTV12mrgHqVE/6Oo=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.54.1 h1:aeJAJyvWS3gQ679pJbz8ZdOh3MViD1zvEdoZMVEawbg=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.54.1/go.mod h1:0RXNc6Yf3AvSMldGD6Lcch96Ojlw2TtGnHsqfD/L4u8=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.107.3 h1:IKoCZqfWfZzSBi16QFQ+QcbQ3LRQ7QgB1S5tDAyPBQQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.107.3/go.mod h1:RBpRcXiM4s2pOInVs32GsBonnje+fiAj4mcrStRmlCA=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.44.1 h1:TpALnO2Chp6wQOihK/v+qoPN80YOyu3hG/MQ9dG+DGo=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.44.1/go.mod h1:nfho2mqRsESddA8OHQiiqhNKaZtGW1Pe1sf6RJFYnfE=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.5.7 h1:YcczQ6zNH/ojIzD/ikDrO+RfW06wmdMp18d4NH5hXY4=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.5.7/go.mod h1:nl9RVnb9ulgAYzOkjLq1NyFxmWcnH2maCUEuOdESy98=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.73.0 h1:8AE9z5vMHNC7tQuaje8fSsNZyvj+0ttiQ2Ed/8rLBsc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.73.0/go.mod h1:004bP6yJs8vdEpZwBT3H25GzleBVJYgeT2pPXkU4t4g=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.33.7 h1:P+bMNiA93gyuYT3Oh+4dWtvrnGcu2bd9Uy5hRJM8BNo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.33.7/go.mod h1:zy+397isDFLvleg9H18Zq2MGzMso7uKyJyzR7DWSgFk=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.7 h1:WWkehGZ4nWtOKLMy0yi8+RqzzVqAGe60hGaxwF06JAw=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.7/go.mod h1:T8AI4SbQYm9ybcVmki2T3n7Qg1g3kfWoeQlNwNYOyO8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.45.7 h1:yU/9y2r7s9kSUPbHXbpQTa4LA8kt+CMgpu1OBrhx8p4=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.45.7/go.mod h1:0lQTDEBArMevQXpxu443LVGjKxxEeSsSnrw9n8YiTMg=
|
||||
github.com/aws/smithy-go v1.27.8 h1:FR0dxZfIlV7Z8eh2iHfIofdunw382XsDV3Mxt9nUvRY=
|
||||
github.com/aws/smithy-go v1.27.8/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 h1:CqL8NN3p4I0JTwwV5MEoatIl4Y2amZ2X9dRD+0hbRL0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6/go.mod h1:exdJ32hMqXl0Wyo6NwqEC5yIhtua3+PDaKE8P29m4qE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 h1:FpBq24TG6WVSObTNhQOv7Q0088rounUUlwn3zlNItkM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6/go.mod h1:ONzdPVYC95oBOu0VWNEsKEntcoemP3xVKjT+qWml1s4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 h1:Qmn60RWuk9+bd2ddvJJqwf3Cpac3VvGMOdbGemtUhX4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6/go.mod h1:h/YJhe4AVyQxEUMluph10iqV7mjajH7A9L+BHF5ppis=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 h1:e6YW5uME30TkrElR+4/xcY7NB6CCj2dcv7vtmGXhfhQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21/go.mod h1:obpHAMP693+f3/A8PkLhIqfK1C+NWWn8zXzqD3OH520=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 h1:CwvFlKw0uUScfgOMflXSRZa+ayXgOf6wk7lhyMemQYU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7/go.mod h1:4SbD8BB+p+QEnG0aX6DoWwrtk9SufyYXRsUv0nve8LE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 h1:LHHdobjo5le1sjJdolitV7uVvYg3wkaeM1Ju2fDgeZQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6/go.mod h1:CBGB0evAiaMD3Lk4wPO7LQCcCZLuhzL5Qf/wfEUHQ5w=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 h1:mYViDr2azcgxOpRrObZOl631bvavqILoE/L+Kxgbwys=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6/go.mod h1:OXVdAo9jNhggB+hJRGOyy7WolGTKB6Rg7IIulDeSzPM=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.60.0 h1:O/xKUsrz7J69Q0yca9XeYxwgi+NbBVbxczcZWudJdU0=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.60.0/go.mod h1:/eZ9xj8IZyb7fj+/t3HO7x89X+7Vn+gCDDbQ2NHCVtM=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3 h1:NyeuyXkZL476uoAowNActm4XJO1QPKf7aassgXHpiqA=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3/go.mod h1:GfoBeKMDJecgRUsGf/0RGdx0NJAh81ObTd922h8pQys=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.49.0 h1:XkHFx2EDC1VxiXaPVBkq3NWD4O+geADhelhl/nvr2Q0=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.49.0/go.mod h1:HMOw7but3OQg86ARfV8Hvoc8h/kNiB3OQm1q6AwO27I=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.10.4 h1:tvpDjuUvgKURf3Yk4hOaGbZ4QdY+070OWcA5d6LUv2A=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.10.4/go.mod h1:3ibVKg5Ng82Zby8++jELTD2nHxiPk2DJRLEeW/LKp8M=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.76.0 h1:hdrI7EQp8HQzmxSwsXsWhjo7AwcVsw8rC/5AOCiDwQ4=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.76.0/go.mod h1:l+OkBEbZ5r0XEHQ+R7c27Zw3LO89fau+FBxsDoAW1TU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.38.4 h1:ChCtagixSFOxltv0o6E3NT8F7ksvZtZGPwUBynO5Nmk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.38.4/go.mod h1:+FEO9jrVMpc9oN3CdAxJbtlQEu3z8UddDPSedpeM3PY=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4 h1:j6c+HA4AoS0EBEjArPjavFEBrucJEryfrQU0TffmACk=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4/go.mod h1:RhghaXUk9q66VtOQUX1FdPddQuHnqQZzbB0fEYXmPRA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.51.4 h1:OKEfbf2tcfsF2pBk/S9QqNpo6alaAtiBs4hysCsBjPw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.51.4/go.mod h1:mCd6qGo9b8hTtfjgsmNkFlzwjyce4owd7HDVFnCLXhM=
|
||||
github.com/aws/smithy-go v1.28.5 h1:OqbqKtzY9NUnr2yAe9+hiTdFb20tNaK/lI0PMOGXX9k=
|
||||
github.com/aws/smithy-go v1.28.5/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -209,8 +211,6 @@ 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.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=
|
||||
@@ -247,8 +247,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
|
||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
|
||||
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/cyberark/conjur-api-go v0.15.5 h1:dGf5QDtSmNX8LdIE3x3hGnOg5FdnwM/QTsBb5GtGi5I=
|
||||
github.com/cyberark/conjur-api-go v0.15.5/go.mod h1:IxsTkDhEewa3iU/W7DMaJ6/snX208F4PYVTjzofRjzc=
|
||||
github.com/cyberark/conjur-api-go v0.15.7 h1:8bOdz6KpujabuQGXMIc9/ejZ6wCHQtMtQnrHw9KSYag=
|
||||
github.com/cyberark/conjur-api-go v0.15.7/go.mod h1:IxsTkDhEewa3iU/W7DMaJ6/snX208F4PYVTjzofRjzc=
|
||||
github.com/cyphar/filepath-securejoin v0.7.0 h1:s0Y3ITPy6sQn5xt54DuYvTF8hu134ooYLUb58DX/HjE=
|
||||
github.com/cyphar/filepath-securejoin v0.7.0/go.mod h1:ymLGms/u3BYaviIiuKFnUx8EkQEZeK6cInNoAPJA3o4=
|
||||
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
|
||||
@@ -261,8 +261,8 @@ 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.1.1 h1:KUbk7C8CfaLXy8kbf/hGq9cad/wCoLB6dbWH6DMbmX0=
|
||||
github.com/distribution/distribution/v3 v3.1.1/go.mod h1:d7lXwZpph0bVcOj4Aqn0nMrWHIwRQGdiV5TLeI+/w6Y=
|
||||
github.com/distribution/distribution/v3 v3.1.2 h1:/Bv2YIqqSR00HiO49FT93hovy6r9IlrzwUDsJjCN5qg=
|
||||
github.com/distribution/distribution/v3 v3.1.2/go.mod h1:Oz0Drs2Tx3iTXzK3DhUD4otfTfqlCxlWVC7QJeXWBP0=
|
||||
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=
|
||||
@@ -275,16 +275,16 @@ github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf
|
||||
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
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-metrics v0.1.0 h1:r76KPNpstz+IvQKSWpYegSkkyzex0V3A1ZGVx6bhGlY=
|
||||
github.com/docker/go-metrics v0.1.0/go.mod h1:PciI3sONtB051kXALN1JoIlpcu54E1FuPh+4DuqEzyw=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
|
||||
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
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/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1 h1:idfl8M8rPW93NehFw5H1qqH8yG158t5POr+LX9avbJY=
|
||||
github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q=
|
||||
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=
|
||||
@@ -311,8 +311,8 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
|
||||
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
|
||||
github.com/fluxcd/cli-utils v1.2.1 h1:ug9CicKW7H9QXnvNDapTSKuryZvWcu4Nw7pRvQa6jDY=
|
||||
github.com/fluxcd/cli-utils v1.2.1/go.mod h1:cky6M6eHvTQkoPtsuFYLIgAMYdpTCSLoor4IA6vueSw=
|
||||
github.com/fluxcd/cli-utils v1.2.2 h1:adDOmwE+LSwTzmYUaoEFPblruOuaQEKAg1ZNTmPJObE=
|
||||
github.com/fluxcd/cli-utils v1.2.2/go.mod h1:FsghNGY+3Sr70c0FOB7I5So0kzoYVdvQ8GTid3XXVWM=
|
||||
github.com/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=
|
||||
@@ -321,8 +321,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fujiwara/tfstate-lookup v1.12.1 h1:WQ0/+wYDm5yeiQtfejriKHonIXH6DSGkCJXEX8cV3hw=
|
||||
github.com/fujiwara/tfstate-lookup v1.12.1/go.mod h1:c7Nd3ZdQ2UljabuWzhgDaseKZhdXfgs6YLFarGbXyDs=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e h1:y/1nzrdF+RPds4lfoEpNhjfmzlgZtPqyO3jMzrqDQws=
|
||||
github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e/go.mod h1:awFzISqLJoZLm+i9QQ4SgMNHDqljH6jWV0B36V5MrUM=
|
||||
github.com/getsops/sops/v3 v3.13.2 h1:jjeipXFlUoBIjBL6Dm1HSsCOOjJZKTq7mx0nVfkcLxY=
|
||||
@@ -342,58 +342,56 @@ 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.25.5 h1:xPYEvTb90o1y0epuiOPAoG4QqahjP3cdp5xNlHeKJRI=
|
||||
github.com/go-openapi/analysis v0.25.5/go.mod h1:d3UGtQC5uq5Kqqqis2VH09Km/v3vwsWrYkbp4gdm+Rc=
|
||||
github.com/go-openapi/errors v0.22.8 h1:oP7sW7TWc3wFFjrzzj0nI83H2qMBkNjNfSd+XRejk/I=
|
||||
github.com/go-openapi/errors v0.22.8/go.mod h1:BuUoHcYrU6E7V9gfj1I5wLQqgtIHnup/alXZ8KdgQ0w=
|
||||
github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s=
|
||||
github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y=
|
||||
github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY=
|
||||
github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0=
|
||||
github.com/go-openapi/loads v0.25.0 h1:74Bc2snfaVlsHzwdQj/3gsA9XJz3daXTJVs+4ZaK7jI=
|
||||
github.com/go-openapi/loads v0.25.0/go.mod h1:JFBw4SIB9+PTIFHDfcXuSSy5h6aWzjtUCrPYyx3qWU8=
|
||||
github.com/go-openapi/runtime v0.33.0 h1:Dd3Oj2ig+WH8ckK95l0Wn2V8a4bH/UqWPRZVT0vc8yU=
|
||||
github.com/go-openapi/runtime v0.33.0/go.mod h1:+rsupH3+TFKqmFysqkmgBOTxpVJV8eV+j9myvvea2Xw=
|
||||
github.com/go-openapi/runtime/server-middleware v0.30.0 h1:8rPoJ/xv7JL8BsovaqboKETlpWBArVh8n+0L/GyePog=
|
||||
github.com/go-openapi/runtime/server-middleware v0.30.0/go.mod h1:OYNT/TxNvB/VK5oe4htM2jDTwlEXuejVJmu0DVZfAMs=
|
||||
github.com/go-openapi/spec v0.22.9 h1:/vKIFDcGKp0ktZWGbym/tJEWbk6/XOEmAVU0kqKMH+w=
|
||||
github.com/go-openapi/spec v0.22.9/go.mod h1:b/mNUYIOQOyIiUzUzXEE8xzyZqf93KvM9hQGP91yfl0=
|
||||
github.com/go-openapi/strfmt v0.27.0 h1:kbcTeaD9TXuXD0hhMXzuYa1sdTo6+dWGvwjW93E80IM=
|
||||
github.com/go-openapi/strfmt v0.27.0/go.mod h1:s/qhDqfY72irigXUGJmtgid2Rm+3tnz3k8hZaRmvWYc=
|
||||
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.27.3 h1:iqJFmGEjmX3AY0lSszABFqRVqOSt99XS0LzNIMJYuhU=
|
||||
github.com/go-openapi/swag/conv v0.27.3/go.mod h1:nPRmN6jgNme99hpf+nM0auDZGALWIqlwhisKPK/bQhQ=
|
||||
github.com/go-openapi/swag/fileutils v0.27.3 h1:3UVoZ2RLaIs1lt+2jcKzL8RM3Yk0rmsDE9FLA/HGxFE=
|
||||
github.com/go-openapi/swag/fileutils v0.27.3/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8=
|
||||
github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE=
|
||||
github.com/go-openapi/swag/jsonname v0.26.1/go.mod h1:OvdW6BoWoj33pTfi7x9vFrgmT+fk7aw0BRwvCE0YOuc=
|
||||
github.com/go-openapi/swag/jsonutils v0.27.3 h1:1DEz+O82frtSMBcos/7XIn1GnpNTbsD4Bru4Dc/uhRc=
|
||||
github.com/go-openapi/swag/jsonutils v0.27.3/go.mod h1:qiDCoQvzkMxrV3G8FLEdIU5L+EFYc0zcDOHWT3Yofvo=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3 h1:h/eT9kmGCDdFLJF29lOhzLtF0FmP1AX2MhLJWVebsb8=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY=
|
||||
github.com/go-openapi/swag/loading v0.27.3 h1:L9nQkEgzU7QgFQL+pLEMfGUKxeM4pWwGwbET9Z3weW0=
|
||||
github.com/go-openapi/swag/loading v0.27.3/go.mod h1:rJ0NeaKsF4CVPnMGjPQl7JlSHzvD0bc2DKXLss1hiuE=
|
||||
github.com/go-openapi/swag/mangling v0.27.3 h1:gRzzD1PAUoLTtGMgI3KpBmCSOlTuLTFWnviLxLcTnyg=
|
||||
github.com/go-openapi/swag/mangling v0.27.3/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w=
|
||||
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/pools v0.27.3 h1:gXjImP3F6/56wRRcFgEPld084Y6u2gs21ikPBt8NKBk=
|
||||
github.com/go-openapi/swag/pools v0.27.3/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE=
|
||||
github.com/go-openapi/swag/stringutils v0.27.3 h1:Ru28hnbAvN5wycALQYy8IobHvASq+FUFMlp1QzLM0JI=
|
||||
github.com/go-openapi/swag/stringutils v0.27.3/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM=
|
||||
github.com/go-openapi/swag/typeutils v0.27.3 h1:l6SSrx5eR5/WVwrGNzN6bQ9WqL04mrxNBl9YgQ3rcJ4=
|
||||
github.com/go-openapi/swag/typeutils v0.27.3/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
|
||||
github.com/go-openapi/swag/yamlutils v0.27.3 h1:cRFCAoYtslYn9L9T0xWryHy1t7c1MACC+DMj3CLvwvs=
|
||||
github.com/go-openapi/swag/yamlutils v0.27.3/go.mod h1:6JYBGj8sw/NawMllyZY+cTA8Mzk2etS3ZBASdcyPsiU=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo=
|
||||
github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug=
|
||||
github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
|
||||
github.com/go-openapi/validate v0.26.1 h1:pZSbvtRO8G2R2FpWTYRn3w8LrsNwbtaVhP2dWiBa0Us=
|
||||
github.com/go-openapi/validate v0.26.1/go.mod h1:B8UMgXiQiwwQWIbmuROlwJZDPGlikPuh7iHV1vPX9Oo=
|
||||
github.com/go-openapi/analysis v1.0.0 h1:sNvbAGCJqUTqIAodr9IVqJMmuZas3YS9ms1dGK9yiJ4=
|
||||
github.com/go-openapi/analysis v1.0.0/go.mod h1:NhYjJ57fnE+bcE7UwrJyMkhWA3Dfz7TdiBfTVAnos4Y=
|
||||
github.com/go-openapi/errors v0.22.9 h1:HI9+SyVYiRzyeBQGv0CbWbQa+u2S0nny7G7AhY+UMiM=
|
||||
github.com/go-openapi/errors v0.22.9/go.mod h1:R73q66sXP2smzAGgFOI4nqtusRK2xN5pYCjsij+KX58=
|
||||
github.com/go-openapi/jsonpointer v1.0.1 h1:2KxywRmNwJkT/FMBa3iRNHEaAxSJvjqoufQZy3au1Mg=
|
||||
github.com/go-openapi/jsonpointer v1.0.1/go.mod h1:wI7ZYsFmbIi9nBXOZqgDaS/bqOchRGZjqxFli7FBYxY=
|
||||
github.com/go-openapi/jsonreference v1.0.1 h1:4zJ7AmYDKNmD3aSpfPnFNCFA5E80/xMHUNKgydaLh38=
|
||||
github.com/go-openapi/jsonreference v1.0.1/go.mod h1:dYplQXa6p5lXprLcJ8LE2iU7vNpXsAHDQ5ZAgL+Qx3A=
|
||||
github.com/go-openapi/loads v0.25.2 h1:+uNsDlRQfYtZTrh+3pdwampcAqZVPuBJW0IA82aZHII=
|
||||
github.com/go-openapi/loads v0.25.2/go.mod h1:RXsfJEQGGNv4uw8u/KdmF8Vh8OR4fTfs9ZO4QrzNohA=
|
||||
github.com/go-openapi/runtime v0.33.2 h1:HSxskMs0WmpCdQvBWVxHt2t2mXMwn8DDav3VtVidwig=
|
||||
github.com/go-openapi/runtime v0.33.2/go.mod h1:NQpSLiIsEAIpZDEs6xUrhjm6ZiQh85X8bHKlqLPgNP8=
|
||||
github.com/go-openapi/runtime/server-middleware v0.33.2 h1:BVFjAaW4Jh/kZ4QSsgDrGzLuSuaKzSiyvappUFKxgUk=
|
||||
github.com/go-openapi/runtime/server-middleware v0.33.2/go.mod h1:E3mWY61/UgBJ5EUmKh+h6A1oQQdD1U4raowVMmPmQug=
|
||||
github.com/go-openapi/spec v1.0.0 h1:JtB/GHOj+eetjse6YvxqLze88oEekl/4uPBethvzRrA=
|
||||
github.com/go-openapi/spec v1.0.0/go.mod h1:boj1PRhqS0x5jylgcNp9BRWxenphrh7vVNYZ90IRCoo=
|
||||
github.com/go-openapi/strfmt v0.27.2 h1:SG32SlbwNy92s0KJiVxt2joJeFdqIYHvwrA0OU6HqzQ=
|
||||
github.com/go-openapi/strfmt v0.27.2/go.mod h1:M4CKsMO0Fb8qR10+1Ra75wCKNNquy+Vj+4LWZrhTo2E=
|
||||
github.com/go-openapi/swag v0.29.2 h1:9n8frkcsuQRA0INU31VbxNxt1zBaHxrHYGEf5BUjFi4=
|
||||
github.com/go-openapi/swag v0.29.2/go.mod h1:RkCiX1gCVXWgLOavDXE3ALJrQ/VO2ziiIzOUs3z10xo=
|
||||
github.com/go-openapi/swag/cmdutils v0.29.2 h1:cXEzX/nWCODon251f+1HgL75/R4XXML5IZxgLCWMoro=
|
||||
github.com/go-openapi/swag/cmdutils v0.29.2/go.mod h1:XziaSVzYqkDvpWXzASfWajw2YvJrLscq610O2XiyqJw=
|
||||
github.com/go-openapi/swag/conv v0.29.2 h1:8c9shoB8l0QRSR6ymq1llHdBWDqpIgJfjTGHx3Vuhm0=
|
||||
github.com/go-openapi/swag/conv v0.29.2/go.mod h1:AZS0YigTNf8qNtD6WqJz/N4RUNmpr76SyjFGkq4+p6Q=
|
||||
github.com/go-openapi/swag/fileutils v0.29.2 h1:mdUL+Vw5ah1fO1AvFCoHeIyv7YZsCz2MN3KNqd/lLVI=
|
||||
github.com/go-openapi/swag/fileutils v0.29.2/go.mod h1:7QKmmodjAebaq61lGVJa8ldLdGeMF4DSgYv8tXOGGo0=
|
||||
github.com/go-openapi/swag/jsonutils v0.29.2 h1:uZNSD2/rJDYAfvsLYkykTzXuyxM3QpDKV9jhRHrMu6k=
|
||||
github.com/go-openapi/swag/jsonutils v0.29.2/go.mod h1:ONTdNvj3Y+IXRzfa17E+Rgt2ns/qiSOYjIo2K7v2yDs=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.2 h1:w+Fd6EBOMGlC74GYGHqCLGyb3Bpams8TtNrgM/SG4jo=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.2/go.mod h1:HC2egPORFzbj/gf05Zrfgz8Mgplx3oW7hgGi7pjZVhM=
|
||||
github.com/go-openapi/swag/loading v0.29.2 h1:QU1ry24e6r6Shoxe380Nx0X5iKkpRO+hJffsyJ2dVlY=
|
||||
github.com/go-openapi/swag/loading v0.29.2/go.mod h1:DqilDjuiJKETecsS4TRopWG1acUDsfqN39ZmQhPu6uI=
|
||||
github.com/go-openapi/swag/mangling v0.29.2 h1:ltdo3K0tTP4P6zWhVVWDU9D37/YYBuMJRdZxiS2Dtog=
|
||||
github.com/go-openapi/swag/mangling v0.29.2/go.mod h1:RjDi4TnItAenS3cgCXSpIl1kKbjG7il6pnmQVl/8bp8=
|
||||
github.com/go-openapi/swag/netutils v0.29.2 h1:dBli6jyUa93sDS/XYWShJzf4+nnVfFKfvY9SLUaUzB8=
|
||||
github.com/go-openapi/swag/netutils v0.29.2/go.mod h1:1pM6Xg/Um8E1eps3umWl8Hme3ByWXOtHsvFC0CULHUE=
|
||||
github.com/go-openapi/swag/pools v0.29.2 h1:PlGoDRF8WtSyXkedZZkpW3f9wDhFf3u8KtaBcmgmh8c=
|
||||
github.com/go-openapi/swag/pools v0.29.2/go.mod h1:V3lhxVT4qDYRqc2MRSSbLsTYIYV/2HlHafhyEkmzLIc=
|
||||
github.com/go-openapi/swag/stringutils v0.29.2 h1:lcnBxwAaysT3bMUVBfn9V/PkfVqkurpqufKU6rTUMGk=
|
||||
github.com/go-openapi/swag/stringutils v0.29.2/go.mod h1:i9cdh7sGaa0nm3CqIvH5IM5h2QClk1wns2ktKeILvRI=
|
||||
github.com/go-openapi/swag/typeutils v0.29.2 h1:O7aVvkTs3pXwikgtrPLenigEMdQFgONKRooQA9pgf2I=
|
||||
github.com/go-openapi/swag/typeutils v0.29.2/go.mod h1:7+GDG+uz9Ke+eVt4rClZg7wklSDp8hT/mKNh/pC26TE=
|
||||
github.com/go-openapi/swag/yamlutils v0.29.2 h1:IFKFFeDnuIwzfsuWRQU+rI8iL3g4XyAYjV/RlnlxPLM=
|
||||
github.com/go-openapi/swag/yamlutils v0.29.2/go.mod h1:7MGqtcrK73sxQ4ceiyIf8qiXS/s09JLMdR5esK5euYQ=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.7.0 h1:wPW6YRgx3+SID1yUy/Xwa17L8kFEaEKod2VRbJDZNUs=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.7.0/go.mod h1:mI1M88etYbc3PhgHsWQK2kwvNwW5aGFqMPbmib+SGIs=
|
||||
github.com/go-openapi/testify/v2 v2.8.0 h1:19QDx5b57p8KjO2E4sgs/woL4Akp+FGLRlVm2xn0Rho=
|
||||
github.com/go-openapi/testify/v2 v2.8.0/go.mod h1:4gUN8jC+RqOE2qjW/vDmfyTvl5BCht4qL7oca0odrak=
|
||||
github.com/go-openapi/validate v1.0.0 h1:dFsYCLVUQUL6Vi2lQSexgwmCXDuHe7eWRDhQxkE+xYA=
|
||||
github.com/go-openapi/validate v1.0.0/go.mod h1:wwXGRqMQzOZ7PCqBcgNk+DD9+Cacnxv7we5T0M/eA3Y=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
@@ -404,8 +402,8 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/go-test/deep v1.1.2 h1:JUuyv17rKrqYfLFnP9A4Yh6hw0635ugB93bMg2bwuDQ=
|
||||
github.com/go-test/deep v1.1.2/go.mod h1:lwVFiFFWPjydTsV3xGJ/YTQsnWwi8zxBFM+gOVcwINE=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
@@ -417,8 +415,8 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
github.com/gofrs/flock v0.13.1 h1:jjREztyBeSKBZYAC+mgc1laB+xsgy4kYMf3FbKF2UBo=
|
||||
github.com/gofrs/flock v0.13.1/go.mod h1:sf4BFiHwnvgxa25DlQoDqXQnwRMEOwqxRq37P6MzzmE=
|
||||
github.com/golang-jwt/jwt/v4 v4.0.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=
|
||||
@@ -445,8 +443,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
|
||||
github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
@@ -462,22 +460,22 @@ github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJ
|
||||
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=
|
||||
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
|
||||
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg=
|
||||
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.19 h1:mMOE7DN2+p76/EdIrmAy9B9bH+yC4563vmnJ34QR8i4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.19/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k=
|
||||
github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE=
|
||||
github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.20 h1:t/xL64VUoN69MuMRQuJETqYGOw4Z9mSRJK9epIEtwFk=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.20/go.mod h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w=
|
||||
github.com/googleapis/gax-go/v2 v2.24.0 h1:myMaPYyF9MecEmvQqMqomIwn9t/4KCZN9qnwsS76wlg=
|
||||
github.com/googleapis/gax-go/v2 v2.24.0/go.mod h1:IaTHBDd7NHxSCiu0vEs8pQZu4dGZrWwuSoxCnk16OFM=
|
||||
github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=
|
||||
github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=
|
||||
github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU=
|
||||
github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
|
||||
github.com/gookit/color v1.6.3 h1:m6E49x+KftGaspcLtPLMsyci2ARhWIUW0PUV1owMlhs=
|
||||
github.com/gookit/color v1.6.3/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
|
||||
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
|
||||
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
@@ -488,8 +486,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.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0 h1:Bd7KaOxzULLxtZ/K5s1aLbWhR0+5RToO65TXHsf3bqQ=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0/go.mod h1:nN7ts3dFXKtCZWc//yfkpcQNKJABg16/uDVAZpLDalo=
|
||||
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=
|
||||
@@ -497,8 +495,8 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-cty-funcs v0.1.0 h1:TRO/6x1unvTPpotTgrTU7qlcbd99JBLt+vmF6dMF6lY=
|
||||
github.com/hashicorp/go-cty-funcs v0.1.0/go.mod h1:crc3afXAsjGOJ+12LNX8PImH+ejyxOjnjvsUteKcFIw=
|
||||
github.com/hashicorp/go-getter/v2 v2.2.3 h1:6CVzhT0KJQHqd9b0pK3xSP0CM/Cv+bVhk+jcaRJ2pGk=
|
||||
github.com/hashicorp/go-getter/v2 v2.2.3/go.mod h1:hp5Yy0GMQvwWVUmwLs3ygivz1JSLI323hdIE9J9m7TY=
|
||||
github.com/hashicorp/go-getter/v2 v2.2.5 h1:kfj7TeF3Mpq6UZcZxXfz7sgig85sTmPYwfGHd8UfTII=
|
||||
github.com/hashicorp/go-getter/v2 v2.2.5/go.mod h1:aZFhc+xmYUqWNxvBhlVi5ZJinWvSjelAFIeCDE5uIkM=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
@@ -531,18 +529,18 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=
|
||||
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.174.0 h1:BVUBgq4ZX5U5LeSb3OZilLUOFOlDqm1lfdFQDui8iPI=
|
||||
github.com/hashicorp/hcp-sdk-go v0.174.0/go.mod h1:v2vbpNIrmgUTelW4Z+ur+aQuSPxeaVK3xytFdpEXvSg=
|
||||
github.com/hashicorp/hcl/v2 v2.25.0 h1:HmmQVYRny4MaBo4b20TjmL46wyuUxpnMWkPZ4+NTbWk=
|
||||
github.com/hashicorp/hcl/v2 v2.25.0/go.mod h1:vR+FKETxoZAmRlHgFfKmuqivj+C4Izm/c66XkmZ3r7M=
|
||||
github.com/hashicorp/hcp-sdk-go v0.175.0 h1:Ff9CVHDzk74qNDS+grGcwmS8HP9xSAvwFK76L3fuQVE=
|
||||
github.com/hashicorp/hcp-sdk-go v0.175.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.28.2 h1:Xi+KOAoPlAP0WraYPcMfJE9HYx9Pf01TRWL5kQFSDeM=
|
||||
github.com/helmfile/chartify v0.28.2/go.mod h1:ATb9N7qzJMiJG6ZJMSAhDv6X0JEi/2SaBJG8pMksSCg=
|
||||
github.com/helmfile/vals v0.46.0 h1:cxXqsKAtk1shtHX3tmmXxjlvAw8lN0l9zaHbbU7JBcw=
|
||||
github.com/helmfile/vals v0.46.0/go.mod h1:ZRDi8e0oF1snqzaAOQ9AA8n8+pEJmJtc9jSV7LfzDxI=
|
||||
github.com/helmfile/chartify v0.29.0 h1:IfE5XqDYwW0FVnVBIt3YDwlSkYBEAuJEcbFG99wwNAg=
|
||||
github.com/helmfile/chartify v0.29.0/go.mod h1:z5icqRatx1gRVuMbv0qT3e9jMoDx3BKXiw6RXIaT2vs=
|
||||
github.com/helmfile/vals v0.47.0 h1:K6ef8t9NEvQdSDsgLb9r++1NK2MezONMWyBINQzwQXM=
|
||||
github.com/helmfile/vals v0.47.0/go.mod h1:VDhFfy8XplCvEeiR9hN6JCjpumtuldUs+XnGkfF9nQs=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
||||
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8=
|
||||
@@ -551,8 +549,8 @@ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.202 h1:GLG7UGUNWcZ65fFYFwi/tbC7IdTG4JAKwc9H+8H5VzE=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.202/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20251118225945-96ee0021ea0f h1:Fnl4pzx8SR7k7JuzyW8lEtSFH6EQ8xgcypgIn8pcGIE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20251118225945-96ee0021ea0f/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.8.0 h1:Ipw62V1ptg77OY/5DYH/39D2Ej61Hnrbgn05PCZd32c=
|
||||
@@ -572,8 +570,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.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
|
||||
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
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=
|
||||
@@ -598,11 +596,11 @@ github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy
|
||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU=
|
||||
github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
|
||||
github.com/mattn/go-runewidth v0.0.31 h1:1oCV4ReTlosDBc6rYRw5cL8S/4y8iv5oRl4SJ5IBgwo=
|
||||
github.com/mattn/go-runewidth v0.0.31/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
|
||||
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=
|
||||
@@ -647,14 +645,14 @@ github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs
|
||||
github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/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.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
|
||||
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
|
||||
github.com/openbao/openbao/api/v2 v2.6.0 h1:KvfspAaL9bab9hI8jFYkV2cgtSrwWtaG+k9AUTHWU4M=
|
||||
github.com/openbao/openbao/api/v2 v2.6.0/go.mod h1:H4IWiH+2rgF/TbrsUbsfrMyGoqojkLqxPCRLENSMnSo=
|
||||
github.com/oklog/ulid/v2 v2.1.2 h1:IEclFb9JNvzYA6MW2SCxbLzcHTVsfqm3PrqGQJH5zec=
|
||||
github.com/oklog/ulid/v2 v2.1.2/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/onsi/ginkgo/v2 v2.32.0 h1:Hw7s2pVrQo/8Yz5N77qdnpHaoc+c6cC9WIV1Jce+J6E=
|
||||
github.com/onsi/ginkgo/v2 v2.32.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
|
||||
github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I=
|
||||
github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg=
|
||||
github.com/openbao/openbao/api/v2 v2.7.0 h1:3CD1l3tr39nQraCgFGAWA5vYvPFzZoZrt3NL7DMQKAc=
|
||||
github.com/openbao/openbao/api/v2 v2.7.0/go.mod h1:uXbMoyH2pjSvNyTepinUvLde8pOJB82EuhUCfOKnKbo=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
@@ -674,8 +672,6 @@ github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAq
|
||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
|
||||
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -687,17 +683,17 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
|
||||
github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
|
||||
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
|
||||
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
|
||||
github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4=
|
||||
github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk=
|
||||
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.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
|
||||
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
|
||||
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho=
|
||||
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U=
|
||||
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc=
|
||||
@@ -705,8 +701,8 @@ 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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
|
||||
github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
|
||||
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
|
||||
@@ -718,19 +714,19 @@ github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkB
|
||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
|
||||
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
github.com/sashabaranov/go-openai v1.42.0 h1:fgeZx7/D8dRT//PwXAGe9ylOMtj6vrs999uWF71K+f8=
|
||||
github.com/sashabaranov/go-openai v1.42.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
github.com/sashabaranov/go-openai v1.43.0 h1:HNRpO8TAQ01ssO7aPXO/68QRlcCCYQQ5GfHbFceRZcY=
|
||||
github.com/sashabaranov/go-openai v1.43.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.37 h1:1Q6K8D0BagYYEnCTkT9fn3YHUFb06bS1OvIHWcc3JQM=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.37/go.mod h1:Rtb4r3WZ5x4AqmL3t/wiF/DmQi+7GlU/nCRdqFbClV4=
|
||||
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
|
||||
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
|
||||
@@ -809,10 +805,10 @@ 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.95.0 h1:ByHofu3SFftRGDGNFCLOP8a+nAPJ0vF9qVtTKWiUVIQ=
|
||||
github.com/yandex-cloud/go-genproto v0.95.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
|
||||
github.com/yandex-cloud/go-sdk v0.32.0 h1:+krs7xf7rPvuPcRITAnqovrKraj5S62VconSPjmAOo4=
|
||||
github.com/yandex-cloud/go-sdk v0.32.0/go.mod h1:YIEb0cQHowyOaIy7BAPUu2oejnoiu1q8z5Yk0KH+pxo=
|
||||
github.com/yandex-cloud/go-genproto v0.121.0 h1:OigYLrjvm/a8V+mQC35rn264BLhw4nV71WZ/4mITH84=
|
||||
github.com/yandex-cloud/go-genproto v0.121.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
|
||||
github.com/yandex-cloud/go-sdk v0.35.0 h1:vuvt/b0Wicu5UK84/e5dzDEEFBri1yh7fW8ROkiXwog=
|
||||
github.com/yandex-cloud/go-sdk v0.35.0/go.mod h1:rSkifg+iUBWUf86kg1zKy6iF3QbNx3mOVjJ3SbfPR/s=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
@@ -832,56 +828,58 @@ go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5
|
||||
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 h1:dkBzNEAIKADEaFnuESzcXvpd09vxvDZsOjx11gjUqLk=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.67.0/go.mod h1:Z5RIwRkZgauOIfnG5IpidvLpERjhTninpP1dTG2jTl4=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.72.0 h1:OUIUu7ovHCEvG4IE5LQd2PNBH2eVYh50w/1ROwHEM7M=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.72.0/go.mod h1:pCrnkC07AZ5+h+0A0Z/1GYyqB+xu3A8LaEEhxvv9s6o=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 h1:NmLfL734pJhM0JKaYd2Y28+nY9dPRWYAAbxhRCrKXPw=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.44.0/go.mod h1:tNAsgd8avTGke1+MndXlU5Cru4PQ9Ai/cCNWQv/ZJ/s=
|
||||
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.69.0 h1:2yEATaop1/a1I4psnSLgWVPLWwCzkqWakgJy7xTDVy0=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0/go.mod h1:D7J12YRapIekYyPWgGPlA/23pRmpSEZC5xJC/TTLI9U=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
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.44.0 h1:hqxVTu/GtBF+vJ8d1fzW7fRxZFvgoDjWcxwwCaFDYpU=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0/go.mod h1:z5fVEF4X5v0ESvlJqBrrFlBVoj5EQuefZpzsu7R+x5Q=
|
||||
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.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
|
||||
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||
go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko=
|
||||
go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.72.0 h1:TPXrl7vEkmh69tp7dHTpz+Y6AUenFg1Du3Nx3a6dlxY=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.72.0/go.mod h1:0jE6K8nsOgeiF53rcEhy71cgakek+XEWi4OPROUqLoI=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0 h1:B2h3uqicet1CT2N5TOFhS+Gq++9i0/CLmaxvhmhtP5s=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0/go.mod h1:dylvB+ZiiwMvsDij9O84Uy7SijLgHMX4mbkncds+4Sw=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU=
|
||||
go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM=
|
||||
go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.23.0 h1:5Ek4qQ5XX0PSQfY7lMW3CvcR36yy8YFDfGnYnaku8zE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.23.0/go.mod h1:NUKIMOKSexQdTmS1pSxOQBDiVes3DHRier2ePsGW/gQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.23.0 h1:5WVp40k2fJHuc5BtNTgKk+EugKFT8Eg3lywFfCPI/nE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.23.0/go.mod h1:59nH0RYCGHRlwt15WyioeSYINBbuObeBpTowPhEKWHY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0 h1:LaLVGJtIzHcvHjVs2OftmKcc+M9mdOqS8vZ1fBD1Yes=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0/go.mod h1:vF6H5sNDuQBJWgSX00iZgbIqk2MZZoVMH0ar5t5bOpw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0 h1:7kszRN4Ze5GeAkUnBjBSUIunVBMA+mZIG04auueqguM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.47.0/go.mod h1:CLzakYy8jOdkBfcveCWYkeKJxujcF4xPhh2Z7IfE98U=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0 h1:UFpxOpYPmMNUtOWhdb+nC1WELIgVf8z9higURrbzYU4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0/go.mod h1:YBGjxe3lt0jtXoEXxGrGhv/jM7oUBUDVK7zhAvNyEjc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 h1:aXZXsZ012wOgVkqaiQv+Z/d8V0xKjmg70F6m4CE94lc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0/go.mod h1:ziu1gUIJhAaxM3EV1e3Ralk5AAyL1UOYz3lJJ4VSI38=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.69.0 h1:LQVBTGnHvzrfGJpDoCSVTY7eaLZArvaTSq5l84Skxew=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.69.0/go.mod h1:moif0nlgj/M16NRT8lDZVt8MC52+EOAJfR8EKRJ8LyU=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.23.0 h1:PW/E9Tw6ZykC88pey0MZY8hKOUeEw+hX6PU+oXA4A9o=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.23.0/go.mod h1:hUw4VNfiOnQ/yzGgy6ZdWVnudXw7iRfImhd6FjXOnvI=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.47.0 h1:ww5DYBpMKcqLdxjWrhGUIh6O+ZfvZUaMk4niVyNQPEQ=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.47.0/go.mod h1:Av5t+Ezx5S/Ks2dsEfWhny+GK+wrZ1K7NZ6aHSWnX3U=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.47.0 h1:N3YQCxjxQ/bMjyc3heladfRm9t9RTksGQH8z4w6yU/0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.47.0/go.mod h1:Mp8HOFqcaUyypCuGv9IhDdTHnJ56lSudSHMd+pVSCEA=
|
||||
go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI=
|
||||
go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw=
|
||||
go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY=
|
||||
go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU=
|
||||
go.opentelemetry.io/otel/metric/x v0.69.0 h1:DjRLr15H83v+hCW7JA9NoJvOkYTtmq5YoDRbe9deYpM=
|
||||
go.opentelemetry.io/otel/metric/x v0.69.0/go.mod h1:uVvsMPMFFyj/HUQfrUnH3JjnOQ1dwFDorgFLRBasM0k=
|
||||
go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I=
|
||||
go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0=
|
||||
go.opentelemetry.io/otel/sdk/log v1.47.0 h1:W4N/ZfgpkRRnmARnqYoQ6BrWA0mrj8IzVDNLlpmUJZY=
|
||||
go.opentelemetry.io/otel/sdk/log v1.47.0/go.mod h1:Wu74Z2EmOKXi2FleSNAb0Hyau2G+r5kubSEMJw5aAMs=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.23.0 h1:j3pm42QCrKYYLLkI6OrV1QHFkUfDA6/4EP+Y1PzggLg=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.23.0/go.mod h1:zt0gEnuTZgDvqvczHwYswz93eqMaIr6upTTsdV/HUlU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw=
|
||||
go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM=
|
||||
go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0=
|
||||
go.opentelemetry.io/proto/otlp v1.11.1 h1:SCrPqH9NE5CmBrOqpjNCiT0X5mGwMyGERXdogfR7Yjw=
|
||||
go.opentelemetry.io/proto/otlp v1.11.1/go.mod h1:wq8W4aSf6bOy8RZyESOeQFr9Pu7wydWX/TY4sgvrTPg=
|
||||
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=
|
||||
@@ -910,19 +908,19 @@ golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq
|
||||
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.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
|
||||
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
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/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU=
|
||||
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E=
|
||||
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.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||
golang.org/x/net v0.0.0-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=
|
||||
@@ -939,19 +937,19 @@ 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.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
|
||||
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
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=
|
||||
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
|
||||
golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
|
||||
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -977,8 +975,8 @@ 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.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
||||
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -988,8 +986,8 @@ 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.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
|
||||
golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
|
||||
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=
|
||||
@@ -1000,8 +998,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.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
|
||||
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
||||
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=
|
||||
@@ -1015,40 +1013,40 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
|
||||
golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA=
|
||||
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.291.0 h1:wfPbbY+mr9c7wZLqqzrHJLft/q8iFKREd6IgTBUene0=
|
||||
google.golang.org/api v0.291.0/go.mod h1:at7kwWbuonglBFEBoeMDAV1bguHqL3qf0BHFsv3coa0=
|
||||
google.golang.org/api v0.296.0 h1:Nn5EHeKdGx70MFClaV/II0gsWUm6xhEjb0xYLylVvaA=
|
||||
google.golang.org/api v0.296.0/go.mod h1:02qB8+Ox1ZFzcaKFMguy1nQLJmSIyvV6Ff4txJEXtl4=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20260622175928-b703f567277d h1:CP5omUq8AJTiWMrPKM1WRLJ7zZeXd9OPcQD3TbBNAyY=
|
||||
google.golang.org/genproto v0.0.0-20260622175928-b703f567277d/go.mod h1:DrwuGJgFSEVNpv3S5Q5VxhRTvdnjauw9GtvwVOEARfA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df h1:O3ig1i5WDDzsVzRp+cCdgelT9vXnlnOFdlEeFtL4HCc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d h1:C9v1o0/4quuhOAfmRXA2j+we0PqZIp8traLdeogF3Ms=
|
||||
google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d/go.mod h1:Wz2wFJntZFmLGo7pLDXZ3wYk5hyc0Mb+SkHhDDXT+lU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0=
|
||||
google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
@@ -1068,34 +1066,34 @@ 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.21.4 h1:T/GcIEXU/gNjJnkITlIZ3e9xqkZjhFTmISuStTZ6+Qg=
|
||||
helm.sh/helm/v3 v3.21.4/go.mod h1:cS2FBb+xfLuaSqvEmbqIeKUVFgHdHVHtVeXb2epof3M=
|
||||
helm.sh/helm/v4 v4.2.4 h1:qIysMI0JpTC4WXf3AQ99V6rZGT0+gO0Ww8IOnnUnaZk=
|
||||
helm.sh/helm/v4 v4.2.4/go.mod h1:ZP8nFdYe7jG1PTQelKzQXQ7m09/ruhMTrpDAf+OL5ms=
|
||||
helm.sh/helm/v3 v3.22.1 h1:uo2SouzAxMkKZTNzZ1lLP/c/Bu7xfUJKYDaYMxMHvk8=
|
||||
helm.sh/helm/v3 v3.22.1/go.mod h1:FNQoawxnIIReGrTytJc55tKakylfpaC3pGf64BGo1rI=
|
||||
helm.sh/helm/v4 v4.3.1 h1:UndCoLe9UPmNoL7IvWzaUXveL7T5t/ijoorEmgJdI9g=
|
||||
helm.sh/helm/v4 v4.3.1/go.mod h1:Z663D/wNQG1UsG3X7svSEBPvosFVel4SIw4YIMZwmuY=
|
||||
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.36.2 h1:3O5gqOj/dt2XWWbpMe+TXWpE9yU6pjM/tXxtHHJT/K4=
|
||||
k8s.io/apiextensions-apiserver v0.36.2/go.mod h1:cL1tBWe8XSaP1H30iWKGo7hf6iAUUUJPEU70dskmAnA=
|
||||
k8s.io/apiextensions-apiserver v0.37.1 h1:7fIQG8eThDSTVYBWg/DOpI8v5wYfKCo0N8/TDMjj+zY=
|
||||
k8s.io/apiextensions-apiserver v0.37.1/go.mod h1:1Q3ujWwHgnhBoGsdmGORflwK1sDFmuaqH1OsMDWk2Gs=
|
||||
k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ=
|
||||
k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc=
|
||||
k8s.io/apiserver v0.36.2 h1:6vMnkmHZPeBloNkHUhmZYq7Ylv8WIB8xjyEl+eSt26E=
|
||||
k8s.io/apiserver v0.36.2/go.mod h1:9PoQ2ikCytrZyZg11mGhLEF5m8Rgsb5FJmYJ4Wvnl1k=
|
||||
k8s.io/cli-runtime v0.36.2 h1:CconTvEeV4DJs4ZX3HQKCFbFRGsm6OtuBM9yjmMP2VM=
|
||||
k8s.io/cli-runtime v0.36.2/go.mod h1:LddcjiMf4YlnHO7c1Y7rEtDqL84FyiYVLco7V679GUU=
|
||||
k8s.io/apiserver v0.37.1 h1:dUsNwjsvItyVKNpC1N0AhQ0OBSiUJKfWz004vPifOe0=
|
||||
k8s.io/apiserver v0.37.1/go.mod h1:EWXW6PntGfOC2uD9GGVobnOExgFoX1z0hZo7f7Rs7Ng=
|
||||
k8s.io/cli-runtime v0.37.1 h1:3mir5bM4XjMJHW3SMRk++3Ylf4YQne0XFFsW+IGT85U=
|
||||
k8s.io/cli-runtime v0.37.1/go.mod h1:g3VQOm71f//aNbp79g0az9jBRTrvPjAHz7WxVysbZ4M=
|
||||
k8s.io/client-go v0.35.4 h1:DN6fyaGuzK64UvnKO5fOA6ymSjvfGAnCAHAR0C66kD8=
|
||||
k8s.io/client-go v0.35.4/go.mod h1:2Pg9WpsS4NeOpoYTfHHfMxBG8zFMSAUi4O/qoiJC3nY=
|
||||
k8s.io/component-base v0.36.2 h1:Z0VH80O7Ng0HDZnZj3WRR3urEGa0kTwmO8CwEwjVK1w=
|
||||
k8s.io/component-base v0.36.2/go.mod h1:mGfFOA7Gwpdm1VW2cwSQYbiDIlz8GD2WGwH88QSeCyA=
|
||||
k8s.io/component-base v0.37.1 h1:93DMmlENnK7gNLkL4pMqLO5M/HVf3p3sM4q/GasLveY=
|
||||
k8s.io/component-base v0.37.1/go.mod h1:bBrdziT4dreQG5lzTaPVxBwsZe1oxphG+ZVdD56dQWQ=
|
||||
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
|
||||
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
|
||||
k8s.io/kubectl v0.36.2 h1:rpUGGpeL09XVOLep2yle5jrtk//JA1L6ZHfkQQtVEwk=
|
||||
k8s.io/kubectl v0.36.2/go.mod h1:gVbQ3B/yb4bSR2ggQ7rd0W6icUSWs7sduH4e16Vii+0=
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A=
|
||||
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I=
|
||||
k8s.io/kubectl v0.37.1 h1:n1VCIntOJiX943uByXCThSZQmE+4v9jGS8S9u94/aHU=
|
||||
k8s.io/kubectl v0.37.1/go.mod h1:66cc4Bz8PxBTxlrHzBuzXEzGIytAnNasKQPeEcpuaV8=
|
||||
k8s.io/utils v0.0.0-20260626114624-be93311217bd h1:Ea7fgQ5we8Y9T0OX5o0dAHzQOBRI07D/dEYRaB9ZZEs=
|
||||
k8s.io/utils v0.0.0-20260626114624-be93311217bd/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||
oras.land/oras-go/v2 v2.6.2 h1:N04RXngAp1LJKTG6ifz3xHPipasEkWr+hFmInja5YKo=
|
||||
oras.land/oras-go/v2 v2.6.2/go.mod h1:PlTtg4JTDJkDe8yVHpM2wz7/YDc00GVas+i4jAW2TZ4=
|
||||
sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4=
|
||||
@@ -1104,11 +1102,11 @@ sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5E
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs=
|
||||
sigs.k8s.io/kustomize/api v0.21.1/go.mod h1:f3wkKByTrgpgltLgySCntrYoq5d3q7aaxveSagwTlwI=
|
||||
sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI=
|
||||
sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ=
|
||||
sigs.k8s.io/kustomize/kyaml v0.21.2 h1:1javwStFk7cgOeLU7yJtPmXcgMEhQgC2X0WjFT6U0p0=
|
||||
sigs.k8s.io/kustomize/kyaml v0.21.2/go.mod h1:zX3qwtuouXd2K1fMiCV0VSFReX06a+CY1rhyf5Dy7hQ=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.3 h1:u08YRbVUi59ri4YD6cg0UqNM4Dimn0sIl+wldcx5PYw=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.3/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3CIki3aW8c4Q=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
gocontext "context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"github.com/helmfile/helmfile/pkg/app"
|
||||
"github.com/helmfile/helmfile/pkg/config"
|
||||
"github.com/helmfile/helmfile/pkg/errors"
|
||||
"github.com/helmfile/helmfile/pkg/telemetry"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -35,6 +37,8 @@ func main() {
|
||||
app.Cancel()
|
||||
app.CleanWaitGroup.Wait()
|
||||
|
||||
shutdownTelemetry(nil, signalExitCode(sig))
|
||||
|
||||
// See http://tldp.org/LDP/abs/html/exitcodes.html
|
||||
switch sig {
|
||||
case syscall.SIGINT:
|
||||
@@ -44,6 +48,41 @@ func main() {
|
||||
}
|
||||
}
|
||||
case err := <-errChan:
|
||||
shutdownTelemetry(err, exitCodeOf(err))
|
||||
errors.HandleExitCoder(err)
|
||||
}
|
||||
}
|
||||
|
||||
// shutdownTelemetry flushes buffered spans before the process exits. It is a
|
||||
// no-op when tracing was never enabled. Flush failures are deliberately
|
||||
// ignored: telemetry must not mask the command's own result.
|
||||
func shutdownTelemetry(runErr error, exitCode int) {
|
||||
ctx, cancel := gocontext.WithTimeout(gocontext.Background(), telemetry.ShutdownTimeout)
|
||||
defer cancel()
|
||||
_ = telemetry.Shutdown(ctx, runErr, exitCode)
|
||||
}
|
||||
|
||||
// signalExitCode maps termination signals to conventional exit codes, matching
|
||||
// the os.Exit calls in main.
|
||||
func signalExitCode(sig os.Signal) int {
|
||||
switch sig {
|
||||
case syscall.SIGINT:
|
||||
return 130
|
||||
case syscall.SIGTERM:
|
||||
return 143
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// exitCodeOf extracts the exit code from the command result for the root-span
|
||||
// attribute; success is 0.
|
||||
func exitCodeOf(err error) int {
|
||||
if err == nil {
|
||||
return 0
|
||||
}
|
||||
if exitErr, ok := err.(errors.ExitCoder); ok {
|
||||
return exitErr.ExitCode()
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ nav:
|
||||
- Advanced Features: advanced-features.md
|
||||
- Hooks: hooks.md
|
||||
- Secrets: remote-secrets.md
|
||||
- OpenTelemetry Tracing: otel.md
|
||||
- Shared Configuration: shared-configuration-across-teams.md
|
||||
- Integrations: integrations.md
|
||||
- Experimental Features: experimental-features.md
|
||||
|
||||
@@ -224,7 +224,7 @@ func TestReportJSON_FieldOrderIsHumanReadable(t *testing.T) {
|
||||
// Cheap way: scan line-by-line, the first 7 non-space chars of each line
|
||||
// are `"key":`.
|
||||
keys := []string{}
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
for line := range strings.SplitSeq(out, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(line, `"`) {
|
||||
continue
|
||||
|
||||
@@ -50,7 +50,10 @@ func (o *openaiClient) Analyze(ctx goContext.Context, diff string, extras Analyz
|
||||
req := openai.ChatCompletionRequest{
|
||||
Model: o.cfg.Model,
|
||||
Temperature: o.cfg.Temperature,
|
||||
MaxTokens: o.cfg.MaxTokens,
|
||||
// SA1019: keep the deprecated MaxTokens field deliberately: `max_tokens` is the
|
||||
// only form universally accepted by OpenAI-compatible backends (One-API,
|
||||
// LiteLLM, Ollama shim); MaxCompletionTokens is not supported by all of them.
|
||||
MaxTokens: o.cfg.MaxTokens, //nolint:staticcheck // SA1019
|
||||
ResponseFormat: &openai.ChatCompletionResponseFormat{
|
||||
Type: openai.ChatCompletionResponseFormatTypeJSONObject,
|
||||
},
|
||||
@@ -166,8 +169,8 @@ func stripJSONCodeFence(s string) string {
|
||||
// Case 2: fenced JSON.
|
||||
if strings.HasPrefix(s, "```") {
|
||||
rest := s[3:]
|
||||
if nl := strings.IndexByte(rest, '\n'); nl >= 0 {
|
||||
s = rest[nl+1:]
|
||||
if _, after, ok := strings.Cut(rest, "\n"); ok {
|
||||
s = after
|
||||
} else {
|
||||
s = rest
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ func TestMockClient_Analyze_PropagatesErr(t *testing.T) {
|
||||
func TestMockClient_ConcurrentSafe(t *testing.T) {
|
||||
c := NewMockClient(Analysis{Summary: "ok"})
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 20; i++ {
|
||||
for range 20 {
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
+252
-53
@@ -3,15 +3,19 @@ package app
|
||||
import (
|
||||
"bytes"
|
||||
goContext "context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/helmfile/vals"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/argparser"
|
||||
@@ -22,6 +26,7 @@ import (
|
||||
"github.com/helmfile/helmfile/pkg/plugins"
|
||||
"github.com/helmfile/helmfile/pkg/remote"
|
||||
"github.com/helmfile/helmfile/pkg/state"
|
||||
"github.com/helmfile/helmfile/pkg/telemetry"
|
||||
)
|
||||
|
||||
var CleanWaitGroup sync.WaitGroup
|
||||
@@ -62,6 +67,9 @@ type App struct {
|
||||
helms map[helmKey]helmexec.Interface
|
||||
helmsMutex sync.Mutex
|
||||
|
||||
rootHelmfileDir string
|
||||
rootHelmfileDirOnce sync.Once
|
||||
|
||||
ctx goContext.Context
|
||||
}
|
||||
|
||||
@@ -76,7 +84,11 @@ type HelmRelease struct {
|
||||
}
|
||||
|
||||
func New(conf ConfigProvider) *App {
|
||||
ctx := goContext.Background()
|
||||
// telemetry.CommandContext returns context.Background when tracing is
|
||||
// disabled, so this is behavior-identical to the previous explicit
|
||||
// Background() while rooting the app context under the command span when
|
||||
// tracing is on.
|
||||
ctx := telemetry.CommandContext()
|
||||
ctx, Cancel = goContext.WithCancel(ctx)
|
||||
|
||||
return Init(&App{
|
||||
@@ -173,12 +185,14 @@ func (a *App) Diff(c DiffConfigProvider) error {
|
||||
prepErr := run.WithPreparedCharts("diff", state.ChartPrepareOptions{
|
||||
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
|
||||
SkipRefresh: c.SkipRefresh(),
|
||||
AllowFailedReleases: c.AllowFailedReleases(),
|
||||
SkipDeps: c.SkipDeps(),
|
||||
SkipSchemaValidation: c.SkipSchemaValidation(),
|
||||
IncludeCRDs: &includeCRDs,
|
||||
Validate: c.Validate(),
|
||||
Concurrency: c.Concurrency(),
|
||||
IncludeTransitiveNeeds: c.IncludeNeeds(),
|
||||
HelmOCIPlainHTTP: a.HelmOCIPlainHTTP,
|
||||
PrefetchSharedRemoteCharts: true,
|
||||
}, func() []error {
|
||||
msg, matched, affected, errs = a.diff(run, c)
|
||||
@@ -246,6 +260,7 @@ func (a *App) Template(c TemplateConfigProvider) error {
|
||||
prepErr := run.WithPreparedCharts("template", state.ChartPrepareOptions{
|
||||
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
|
||||
SkipRefresh: c.SkipRefresh(),
|
||||
AllowFailedReleases: c.AllowFailedReleases(),
|
||||
SkipDeps: c.SkipDeps(),
|
||||
SkipSchemaValidation: c.SkipSchemaValidation(),
|
||||
IncludeCRDs: &includeCRDs,
|
||||
@@ -274,6 +289,8 @@ 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{
|
||||
// Note: "write-values" never prepares charts (see commandsSkipChartPrep
|
||||
// in run.go), so AllowFailedReleases does not apply here.
|
||||
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
|
||||
SkipRefresh: c.SkipRefresh(),
|
||||
SkipDeps: c.SkipDeps(),
|
||||
@@ -329,10 +346,12 @@ func (a *App) Lint(c LintConfigProvider) error {
|
||||
ForceDownload: true,
|
||||
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
|
||||
SkipRefresh: c.SkipRefresh(),
|
||||
AllowFailedReleases: c.AllowFailedReleases(),
|
||||
SkipDeps: c.SkipDeps(),
|
||||
SkipCleanup: c.SkipCleanup(),
|
||||
Concurrency: c.Concurrency(),
|
||||
IncludeTransitiveNeeds: c.IncludeNeeds(),
|
||||
HelmOCIPlainHTTP: a.HelmOCIPlainHTTP,
|
||||
}, func() []error {
|
||||
ok, lintErrs, errs = a.lint(run, c)
|
||||
return append(errs, lintErrs...)
|
||||
@@ -371,10 +390,12 @@ func (a *App) Unittest(c UnittestConfigProvider) error {
|
||||
ForceDownload: true,
|
||||
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
|
||||
SkipRefresh: c.SkipRefresh(),
|
||||
AllowFailedReleases: c.AllowFailedReleases(),
|
||||
SkipDeps: c.SkipDeps(),
|
||||
SkipCleanup: c.SkipCleanup(),
|
||||
Concurrency: c.Concurrency(),
|
||||
IncludeTransitiveNeeds: c.IncludeTransitiveNeeds(),
|
||||
HelmOCIPlainHTTP: a.HelmOCIPlainHTTP,
|
||||
}, func() []error {
|
||||
ok, unittestErrs, errs = a.unittest(run, c)
|
||||
return append(errs, unittestErrs...)
|
||||
@@ -447,13 +468,15 @@ func (a *App) Fetch(c FetchConfigProvider) error {
|
||||
}
|
||||
|
||||
prepErr := run.WithPreparedCharts("pull", state.ChartPrepareOptions{
|
||||
ForceDownload: true,
|
||||
SkipRefresh: c.SkipRefresh(),
|
||||
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
|
||||
SkipDeps: c.SkipDeps(),
|
||||
OutputDir: c.OutputDir(),
|
||||
OutputDirTemplate: c.OutputDirTemplate(),
|
||||
Concurrency: c.Concurrency(),
|
||||
ForceDownload: true,
|
||||
SkipRefresh: c.SkipRefresh(),
|
||||
AllowFailedReleases: c.AllowFailedReleases(),
|
||||
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
|
||||
SkipDeps: c.SkipDeps(),
|
||||
OutputDir: c.OutputDir(),
|
||||
OutputDirTemplate: c.OutputDirTemplate(),
|
||||
Concurrency: c.Concurrency(),
|
||||
HelmOCIPlainHTTP: a.HelmOCIPlainHTTP,
|
||||
}, func() []error {
|
||||
if c.WriteOutput() {
|
||||
for i := range run.state.Releases {
|
||||
@@ -504,6 +527,7 @@ func (a *App) Sync(c SyncConfigProvider) error {
|
||||
prepErr := run.WithPreparedCharts("sync", state.ChartPrepareOptions{
|
||||
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
|
||||
SkipRefresh: c.SkipRefresh(),
|
||||
AllowFailedReleases: c.AllowFailedReleases(),
|
||||
SkipDeps: c.SkipDeps(),
|
||||
SkipSchemaValidation: c.SkipSchemaValidation(),
|
||||
Wait: c.Wait(),
|
||||
@@ -513,6 +537,7 @@ func (a *App) Sync(c SyncConfigProvider) error {
|
||||
IncludeTransitiveNeeds: c.IncludeNeeds(),
|
||||
Validate: c.Validate(),
|
||||
Concurrency: c.Concurrency(),
|
||||
HelmOCIPlainHTTP: a.HelmOCIPlainHTTP,
|
||||
TemplateArgs: c.TemplateArgs(),
|
||||
PrefetchSharedRemoteCharts: true,
|
||||
}, func() []error {
|
||||
@@ -562,6 +587,7 @@ func (a *App) Apply(c ApplyConfigProvider) error {
|
||||
prepErr := run.WithPreparedCharts("apply", state.ChartPrepareOptions{
|
||||
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
|
||||
SkipRefresh: c.SkipRefresh(),
|
||||
AllowFailedReleases: c.AllowFailedReleases(),
|
||||
SkipDeps: c.SkipDeps(),
|
||||
SkipSchemaValidation: c.SkipSchemaValidation(),
|
||||
Wait: c.Wait(),
|
||||
@@ -571,6 +597,7 @@ func (a *App) Apply(c ApplyConfigProvider) error {
|
||||
SkipCleanup: c.SkipCleanup(),
|
||||
Validate: c.Validate(),
|
||||
Concurrency: c.Concurrency(),
|
||||
HelmOCIPlainHTTP: a.HelmOCIPlainHTTP,
|
||||
IncludeTransitiveNeeds: c.IncludeNeeds(),
|
||||
TemplateArgs: c.TemplateArgs(),
|
||||
PrefetchSharedRemoteCharts: true,
|
||||
@@ -608,9 +635,10 @@ 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{
|
||||
SkipRepos: true,
|
||||
SkipDeps: true,
|
||||
Concurrency: c.Concurrency(),
|
||||
SkipRepos: true,
|
||||
AllowFailedReleases: c.AllowFailedReleases(),
|
||||
SkipDeps: true,
|
||||
Concurrency: c.Concurrency(),
|
||||
}, func() []error {
|
||||
ok, errs = a.status(run, c)
|
||||
return errs
|
||||
@@ -628,12 +656,14 @@ 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{
|
||||
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
|
||||
SkipRefresh: c.SkipRefresh(),
|
||||
SkipDeps: c.SkipDeps(),
|
||||
Concurrency: c.Concurrency(),
|
||||
DeleteWait: c.DeleteWait(),
|
||||
DeleteTimeout: c.DeleteTimeout(),
|
||||
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
|
||||
SkipRefresh: c.SkipRefresh(),
|
||||
AllowFailedReleases: c.AllowFailedReleases(),
|
||||
SkipDeps: c.SkipDeps(),
|
||||
Concurrency: c.Concurrency(),
|
||||
HelmOCIPlainHTTP: a.HelmOCIPlainHTTP,
|
||||
DeleteWait: c.DeleteWait(),
|
||||
DeleteTimeout: c.DeleteTimeout(),
|
||||
}, func() []error {
|
||||
ok, errs = a.delete(run, true, c)
|
||||
return errs
|
||||
@@ -657,10 +687,12 @@ func (a *App) Test(c TestConfigProvider) error {
|
||||
}
|
||||
|
||||
err := run.WithPreparedCharts("test", state.ChartPrepareOptions{
|
||||
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
|
||||
SkipRefresh: c.SkipRefresh(),
|
||||
SkipDeps: c.SkipDeps(),
|
||||
Concurrency: c.Concurrency(),
|
||||
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
|
||||
SkipRefresh: c.SkipRefresh(),
|
||||
AllowFailedReleases: c.AllowFailedReleases(),
|
||||
SkipDeps: c.SkipDeps(),
|
||||
Concurrency: c.Concurrency(),
|
||||
HelmOCIPlainHTTP: a.HelmOCIPlainHTTP,
|
||||
}, func() []error {
|
||||
errs = a.test(run, c)
|
||||
return errs
|
||||
@@ -760,7 +792,14 @@ func (a *App) dag(r *Run) error {
|
||||
}
|
||||
|
||||
func (a *App) ListReleases(c ListConfigProvider) error {
|
||||
releasesChan := make(chan []*HelmRelease, 100)
|
||||
// Collect the per-state results under a mutex. ForEachState may visit
|
||||
// states concurrently, and a bounded channel that is drained only after the
|
||||
// visit finishes blocks forever once more states carry releases than the
|
||||
// buffer can hold (the previous 100-slot channel deadlocked at 101 states).
|
||||
var (
|
||||
releasesMu sync.Mutex
|
||||
releases []*HelmRelease
|
||||
)
|
||||
|
||||
err := a.ForEachState(func(run *Run) (_ bool, errs []error) {
|
||||
var stateReleases []*HelmRelease
|
||||
@@ -768,6 +807,8 @@ func (a *App) ListReleases(c ListConfigProvider) error {
|
||||
|
||||
if !c.SkipCharts() {
|
||||
prepErr := run.WithPreparedCharts("list", state.ChartPrepareOptions{
|
||||
// Note: "list" never prepares charts (see commandsSkipChartPrep in
|
||||
// run.go), so AllowFailedReleases does not apply here.
|
||||
SkipRepos: true,
|
||||
SkipDeps: true,
|
||||
Concurrency: 2,
|
||||
@@ -791,20 +832,14 @@ func (a *App) ListReleases(c ListConfigProvider) error {
|
||||
}
|
||||
|
||||
if len(stateReleases) > 0 {
|
||||
releasesChan <- stateReleases
|
||||
releasesMu.Lock()
|
||||
releases = append(releases, stateReleases...)
|
||||
releasesMu.Unlock()
|
||||
}
|
||||
|
||||
return
|
||||
}, false, SetFilter(true))
|
||||
|
||||
close(releasesChan)
|
||||
|
||||
// Collect all releases from channel
|
||||
var releases []*HelmRelease
|
||||
for rels := range releasesChan {
|
||||
releases = append(releases, rels...)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -921,6 +956,13 @@ func (a *App) loadDesiredStateFromYamlWithBaseDir(file string, baseDir string, o
|
||||
op = opts[0]
|
||||
}
|
||||
|
||||
// The load span covers remote fetching, rendering, and parsing of one
|
||||
// state file; render/parse spans attach through the loader's traceCtx.
|
||||
loadCtx, loadSpan := telemetry.Tracer(telemetry.ScopeHelmfile).Start(a.spanParentCtx(), "helmfile.load",
|
||||
trace.WithAttributes(attribute.String("helmfile.state_file", file)),
|
||||
)
|
||||
defer loadSpan.End()
|
||||
|
||||
ld := &desiredStateLoader{
|
||||
fs: a.fs,
|
||||
env: a.Env,
|
||||
@@ -929,6 +971,7 @@ func (a *App) loadDesiredStateFromYamlWithBaseDir(file string, baseDir string, o
|
||||
logger: a.Logger,
|
||||
remote: a.remote,
|
||||
baseDir: baseDir,
|
||||
traceCtx: loadCtx,
|
||||
|
||||
overrideKubeContext: a.OverrideKubeContext,
|
||||
overrideHelmBinary: a.OverrideHelmBinary,
|
||||
@@ -943,6 +986,11 @@ func (a *App) loadDesiredStateFromYamlWithBaseDir(file string, baseDir string, o
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Per-release spans (pkg/state) parent under the load span.
|
||||
st.SetTraceContext(loadCtx)
|
||||
// Kubedog tracking / buffered helm subprocesses cancel with the app.
|
||||
st.SetCancelContext(a.ctx)
|
||||
|
||||
st.SetKubeconfig(a.Kubeconfig)
|
||||
|
||||
return st, nil
|
||||
@@ -1050,6 +1098,7 @@ func (a *App) processStateFileParallel(relPath string, defOpts LoadOpts, converg
|
||||
}
|
||||
|
||||
st.Selectors = opts.Selectors
|
||||
st.SetRootDir(a.RootHelmfileDir())
|
||||
|
||||
// Track whether any releases matched across nested helmfiles and converge.
|
||||
// Aggregate into a single send to matchChan to avoid overfilling the buffer
|
||||
@@ -1084,7 +1133,7 @@ func (a *App) processStateFileParallel(relPath string, defOpts LoadOpts, converg
|
||||
processed, errs := converge(templated)
|
||||
|
||||
if len(errs) > 0 {
|
||||
errChan <- errs[0]
|
||||
errChan <- aggregateErrors(errs)
|
||||
return
|
||||
}
|
||||
if cleanErr != nil {
|
||||
@@ -1131,16 +1180,29 @@ func (a *App) processNestedHelmfiles(st *state.HelmState, absd, file string, def
|
||||
continue
|
||||
}
|
||||
|
||||
// Selectors that govern this sub-tree. Matches the inheritance model:
|
||||
// CLI (parent) selectors flow down when the entry has none of its own
|
||||
// (and explicit-selector-inheritance is off), or when SelectorsInherited
|
||||
// is set; otherwise the entry's own selectors take over. Used both for
|
||||
// the dir-skip decision below and for the nested LoadOpts.
|
||||
var effectiveSelectors []string
|
||||
if (m.Selectors == nil && !isExplicitSelectorInheritanceEnabled()) || m.SelectorsInherited {
|
||||
effectiveSelectors = opts.Selectors
|
||||
} else {
|
||||
effectiveSelectors = m.Selectors
|
||||
}
|
||||
|
||||
if a.skipForDirFilter(absd, m.Path, effectiveSelectors) {
|
||||
a.Logger.Debugf("skipping subhelmfile %q: outside dir= selector scope", m.Path)
|
||||
continue
|
||||
}
|
||||
|
||||
optsForNestedState := LoadOpts{
|
||||
CalleePath: filepath.Join(absd, file),
|
||||
Environment: m.Environment,
|
||||
Reverse: defOpts.Reverse,
|
||||
RetainValuesFiles: defOpts.RetainValuesFiles,
|
||||
}
|
||||
if (m.Selectors == nil && !isExplicitSelectorInheritanceEnabled()) || m.SelectorsInherited {
|
||||
optsForNestedState.Selectors = opts.Selectors
|
||||
} else {
|
||||
optsForNestedState.Selectors = m.Selectors
|
||||
Selectors: effectiveSelectors,
|
||||
}
|
||||
|
||||
// Carry parent config requested via `inherits:`, and parent repo names
|
||||
@@ -1290,6 +1352,7 @@ func (a *App) visitStatesWithContext(fileOrDir string, defOpts LoadOpts, converg
|
||||
}
|
||||
|
||||
st.Selectors = opts.Selectors
|
||||
st.SetRootDir(a.RootHelmfileDir())
|
||||
|
||||
if !opts.Reverse && len(st.Helmfiles) > 0 {
|
||||
matched, err := a.processNestedHelmfiles(st, absd, file, defOpts, opts, converge, sharedCtx)
|
||||
@@ -1325,7 +1388,7 @@ func (a *App) visitStatesWithContext(fileOrDir string, defOpts LoadOpts, converg
|
||||
processed, errs = converge(templated)
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errs[0]
|
||||
return aggregateErrors(errs)
|
||||
}
|
||||
|
||||
noMatchInHelmfiles = noMatchInHelmfiles && !processed
|
||||
@@ -1474,18 +1537,16 @@ func withBatches(purpose string, templated *state.HelmState, batches [][]state.R
|
||||
logger.Debugf("%s %d groups of releases in this order:\n%s", purpose, numBatches, printBatches(batches))
|
||||
|
||||
any := false
|
||||
var allErrs []error
|
||||
blockedByID := map[string]blockedRelease{}
|
||||
|
||||
for i, batch := range batches {
|
||||
var targets []state.ReleaseSpec
|
||||
|
||||
for _, marked := range batch {
|
||||
targets = append(targets, marked.ReleaseSpec)
|
||||
}
|
||||
targets, skippedErrs := filterBlockedReleases(batch, blockedByID, logger)
|
||||
allErrs = append(allErrs, skippedErrs...)
|
||||
|
||||
var releaseIds []string
|
||||
for _, r := range targets {
|
||||
release := r
|
||||
releaseIds = append(releaseIds, state.ReleaseToID(&release))
|
||||
releaseIds = append(releaseIds, state.ReleaseToID(&r))
|
||||
}
|
||||
|
||||
logger.Debugf("%s releases in group %d/%d: %s", purpose, i+1, numBatches, strings.Join(releaseIds, ", "))
|
||||
@@ -1493,16 +1554,109 @@ func withBatches(purpose string, templated *state.HelmState, batches [][]state.R
|
||||
batchSt := *templated
|
||||
batchSt.Releases = targets
|
||||
|
||||
processed, errs := converge(&batchSt, helm)
|
||||
processed, batchErrs := converge(&batchSt, helm)
|
||||
allErrs = append(allErrs, batchErrs...)
|
||||
|
||||
if len(errs) > 0 {
|
||||
return false, errs
|
||||
if toleratesBatchErrors(batchErrs, blockedByID) {
|
||||
any = any || processed
|
||||
continue
|
||||
}
|
||||
|
||||
any = any || processed
|
||||
return false, allErrs
|
||||
}
|
||||
|
||||
return any, nil
|
||||
return any, allErrs
|
||||
}
|
||||
|
||||
// blockedRelease remembers a release that failed or was skipped, so that its
|
||||
// dependents can be skipped transitively and reported with its plain name
|
||||
// (rather than the kubeContext/namespace-qualified `needs` id).
|
||||
type blockedRelease struct {
|
||||
release *state.ReleaseSpec
|
||||
err error
|
||||
}
|
||||
|
||||
// filterBlockedReleases partitions a batch into the releases that may still be
|
||||
// processed and those that must be skipped because one of their `needs`
|
||||
// dependencies already failed or was skipped. Skipped releases are recorded in
|
||||
// blockedByID (so that their own dependents are skipped transitively) and
|
||||
// reported as errors, so that a failed dependency never results in a
|
||||
// successful command.
|
||||
func filterBlockedReleases(batch []state.Release, blockedByID map[string]blockedRelease, logger *zap.SugaredLogger) ([]state.ReleaseSpec, []error) {
|
||||
var targets []state.ReleaseSpec
|
||||
var skippedErrs []error
|
||||
|
||||
for _, marked := range batch {
|
||||
release := marked.ReleaseSpec
|
||||
|
||||
blocked, ok := firstBlockedDependency(&release, blockedByID)
|
||||
if !ok {
|
||||
targets = append(targets, release)
|
||||
continue
|
||||
}
|
||||
|
||||
skippedErr := fmt.Errorf("release %q was skipped because dependency %q failed: %w", release.Name, blocked.release.Name, blocked.err)
|
||||
skippedErrs = append(skippedErrs, skippedErr)
|
||||
blockedByID[state.ReleaseToID(&release)] = blockedRelease{release: &release, err: skippedErr}
|
||||
|
||||
logger.Warnf("release %q was skipped because dependency %q failed: %v", release.Name, blocked.release.Name, blocked.err)
|
||||
}
|
||||
|
||||
return targets, skippedErrs
|
||||
}
|
||||
|
||||
// toleratesBatchErrors inspects the errors reported while processing a batch.
|
||||
// Each per-release failure is recorded in blockedByID so that its dependents
|
||||
// are skipped in subsequent batches. It returns true when every error is a
|
||||
// release failure whose release opted in to continueOnError, so that
|
||||
// independent branches of the DAG keep being processed. Any other error (a
|
||||
// non-release error, or a release failure without continueOnError) is fatal
|
||||
// and preserves the historical fail-fast behavior.
|
||||
func toleratesBatchErrors(batchErrs []error, blockedByID map[string]blockedRelease) bool {
|
||||
for _, err := range batchErrs {
|
||||
releaseErr := releaseErrorForContinueOnError(err)
|
||||
if releaseErr == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
blockedByID[state.ReleaseToID(releaseErr.ReleaseSpec)] = blockedRelease{release: releaseErr.ReleaseSpec, err: err}
|
||||
if !continueOnErrorEnabled(releaseErr.ReleaseSpec) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func continueOnErrorEnabled(release *state.ReleaseSpec) bool {
|
||||
return release != nil && release.ContinueOnError != nil && *release.ContinueOnError
|
||||
}
|
||||
|
||||
func firstBlockedDependency(release *state.ReleaseSpec, blockedByID map[string]blockedRelease) (blockedRelease, bool) {
|
||||
for _, need := range release.Needs {
|
||||
if blocked, ok := blockedByID[need]; ok {
|
||||
return blocked, true
|
||||
}
|
||||
}
|
||||
|
||||
return blockedRelease{}, false
|
||||
}
|
||||
|
||||
// releaseErrorForContinueOnError returns err as a *state.ReleaseError when it
|
||||
// is a plain release failure. Non-release errors, and release errors carrying
|
||||
// a non-failure code (e.g. the diff "changes detected" exit code 2), are not
|
||||
// tolerable failures and must keep failing fast.
|
||||
func releaseErrorForContinueOnError(err error) *state.ReleaseError {
|
||||
var releaseErr *state.ReleaseError
|
||||
if !errors.As(err, &releaseErr) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if releaseErr.Code != state.ReleaseErrorCodeFailure {
|
||||
return nil
|
||||
}
|
||||
|
||||
return releaseErr
|
||||
}
|
||||
|
||||
type Opts struct {
|
||||
@@ -1510,6 +1664,11 @@ type Opts struct {
|
||||
}
|
||||
|
||||
func (a *App) visitStatesWithSelectorsAndRemoteSupportWithContext(fileOrDir string, converge func(*state.HelmState) (bool, []error), includeTransitiveNeeds bool, sharedCtx *Context, opt ...LoadOption) error {
|
||||
// Resolve the root helmfile directory anchor for dir= filtering before
|
||||
// any chdir or goroutine fan-out happens further down; it uses the
|
||||
// process CWD at command start, which is the user's intent.
|
||||
a.resolveRootHelmfileDir()
|
||||
|
||||
opts := LoadOpts{
|
||||
Selectors: a.Selectors,
|
||||
}
|
||||
@@ -1625,7 +1784,24 @@ func (a *App) WrapWithoutSelector(converge func(*state.HelmState, helmexec.Inter
|
||||
}
|
||||
}
|
||||
|
||||
// spanParentCtx returns the context app-layer spans attach to. Tests
|
||||
// construct App literals without a context, so nil falls back to Background
|
||||
// (with tracing disabled, span starts are no-ops anyway).
|
||||
func (a *App) spanParentCtx() goContext.Context {
|
||||
if a.ctx != nil {
|
||||
return a.ctx
|
||||
}
|
||||
return goContext.Background()
|
||||
}
|
||||
|
||||
func (a *App) findDesiredStateFiles(specifiedPath string, opts LoadOpts) ([]string, error) {
|
||||
_, span := telemetry.Tracer(telemetry.ScopeHelmfile).Start(a.spanParentCtx(), "helmfile.discover_states",
|
||||
// specifiedPath is captured before Remote.Locate resolves it; it may
|
||||
// be a remote reference carrying credentials in userinfo or query.
|
||||
trace.WithAttributes(attribute.String("helmfile.path", helmexec.RedactedRef(specifiedPath))),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
path, err := a.remote.Locate(specifiedPath, "states")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("locate: %v", err)
|
||||
@@ -1692,9 +1868,7 @@ func (a *App) findDesiredStateFiles(specifiedPath string, opts LoadOpts) ([]stri
|
||||
return files[j] < files[i]
|
||||
})
|
||||
} else {
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
return files[i] < files[j]
|
||||
})
|
||||
slices.Sort(files)
|
||||
}
|
||||
|
||||
a.Logger.Debugf("found %d helmfile state files in %s: %s", len(ymlFiles)+len(gotmplFiles), helmfileDir, strings.Join(files, ", "))
|
||||
@@ -1981,7 +2155,9 @@ Do you really want to apply?
|
||||
TrackMode: c.TrackMode(),
|
||||
TrackTimeout: c.TrackTimeout(),
|
||||
TrackLogs: c.TrackLogs(),
|
||||
TrackLogsUntilReady: c.TrackLogsUntilReady(),
|
||||
TrackFailedLogs: c.TrackFailedLogs(),
|
||||
TrackLogsInterval: c.TrackLogsInterval(),
|
||||
HelmStuckGrace: c.HelmStuckGrace(),
|
||||
TrackFailOnError: c.TrackFailOnError(),
|
||||
Description: c.Description(),
|
||||
@@ -2478,7 +2654,9 @@ Do you really want to sync?
|
||||
TrackMode: c.TrackMode(),
|
||||
TrackTimeout: c.TrackTimeout(),
|
||||
TrackLogs: c.TrackLogs(),
|
||||
TrackLogsUntilReady: c.TrackLogsUntilReady(),
|
||||
TrackFailedLogs: c.TrackFailedLogs(),
|
||||
TrackLogsInterval: c.TrackLogsInterval(),
|
||||
HelmStuckGrace: c.HelmStuckGrace(),
|
||||
TrackFailOnError: c.TrackFailOnError(),
|
||||
Description: c.Description(),
|
||||
@@ -2786,6 +2964,27 @@ func appError(msg string, err error) *Error {
|
||||
return &Error{msg: msg, Errors: []error{err}}
|
||||
}
|
||||
|
||||
// aggregateErrors combines multiple errors from processing a state file into a
|
||||
// single *Error that renders all of them. Rendering a single error is
|
||||
// identical to returning it directly, so aggregating does not change the
|
||||
// reported message for the single-error case — but it keeps errors that are
|
||||
// not the first one visible, which matters when continueOnError lets multiple
|
||||
// releases fail or be skipped within one run.
|
||||
func aggregateErrors(errs []error) error {
|
||||
filtered := make([]error, 0, len(errs))
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
filtered = append(filtered, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(filtered) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &Error{Errors: filtered}
|
||||
}
|
||||
|
||||
func (c context) clean(errs []error) error {
|
||||
if errs == nil {
|
||||
errs = []error{}
|
||||
|
||||
@@ -3,8 +3,10 @@ package app
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/helmfile/vals"
|
||||
@@ -303,6 +305,80 @@ func TestListWithJSONOutput(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestListWithManyStates guards against a deadlock in ListReleases: per-state
|
||||
// results used to be collected through a channel with a fixed buffer of 100
|
||||
// that was drained only after every state had been visited, so the 101st state
|
||||
// carrying releases blocked forever and `helmfile list` hung without output.
|
||||
func TestListWithManyStates(t *testing.T) {
|
||||
const stateCount = 101
|
||||
|
||||
files := map[string]string{}
|
||||
for i := 1; i <= stateCount; i++ {
|
||||
files[fmt.Sprintf("/path/to/helmfile.d/helmfile_%03d.yaml", i)] = fmt.Sprintf(`
|
||||
releases:
|
||||
- name: release-%03d
|
||||
namespace: default
|
||||
chart: incubator/raw
|
||||
`, i)
|
||||
}
|
||||
|
||||
stdout := os.Stdout
|
||||
defer func() { os.Stdout = stdout }()
|
||||
|
||||
var buffer bytes.Buffer
|
||||
syncWriter := testhelper.NewSyncWriter(&buffer)
|
||||
logger := helmexec.NewLogger(syncWriter, "debug")
|
||||
|
||||
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating vals runtime: %v", err)
|
||||
}
|
||||
|
||||
app := appWithFs(&App{
|
||||
OverrideHelmBinary: DefaultHelmBinary,
|
||||
fs: ffs.DefaultFileSystem(),
|
||||
OverrideKubeContext: "default",
|
||||
DisableKubeVersionAutoDetection: true,
|
||||
Env: "default",
|
||||
Logger: logger,
|
||||
valsRuntime: valsRuntime,
|
||||
}, files)
|
||||
|
||||
expectNoCallsToHelm(app)
|
||||
|
||||
// A regression shows up as a hang, not as an error, so run the listing in
|
||||
// a goroutine and fail fast instead of waiting for the go test timeout.
|
||||
type result struct {
|
||||
out string
|
||||
err error
|
||||
}
|
||||
done := make(chan result, 1)
|
||||
go func() {
|
||||
var listErr error
|
||||
out, err := testutil.CaptureStdout(func() {
|
||||
listErr = app.ListReleases(configImpl{skipCharts: true, output: "json"})
|
||||
})
|
||||
if err == nil {
|
||||
err = listErr
|
||||
}
|
||||
done <- result{out: out, err: err}
|
||||
}()
|
||||
|
||||
var res result
|
||||
select {
|
||||
case res = <-done:
|
||||
case <-time.After(60 * time.Second):
|
||||
t.Fatalf("ListReleases did not return within 60s for %d states: deadlock while collecting releases", stateCount)
|
||||
}
|
||||
assert.NoError(t, res.err)
|
||||
|
||||
var releases []HelmRelease
|
||||
if err := json.Unmarshal([]byte(res.out), &releases); err != nil {
|
||||
t.Fatalf("failed to parse JSON output: %v", err)
|
||||
}
|
||||
assert.Len(t, releases, stateCount, "expected one release per state")
|
||||
}
|
||||
|
||||
func TestListWithLockFileVersion(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
goContext "context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/helmfile/vals"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
v1 "go.opentelemetry.io/proto/otlp/trace/v1"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/exectest"
|
||||
ffs "github.com/helmfile/helmfile/pkg/filesystem"
|
||||
"github.com/helmfile/helmfile/pkg/helmexec"
|
||||
"github.com/helmfile/helmfile/pkg/telemetry"
|
||||
"github.com/helmfile/helmfile/pkg/telemetry/otlptest"
|
||||
)
|
||||
|
||||
// TestLoadSpanHierarchy drives a template run with telemetry enabled and the
|
||||
// exectest fake helm, then asserts the state-loading span tree:
|
||||
// root → discover_states → load → { render, parse }. This is the golden test
|
||||
// for the app-layer context plumbing (docs/proposals/otel-tracing.md §10.3).
|
||||
func TestLoadSpanHierarchy(t *testing.T) {
|
||||
rec := otlptest.NewRecorder(t)
|
||||
otlptest.SetupTelemetry(t, rec, "helmfile template")
|
||||
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml.gotmpl": `
|
||||
releases:
|
||||
- name: demo
|
||||
chart: incubator/raw
|
||||
`,
|
||||
}
|
||||
|
||||
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
||||
require.NoError(t, err)
|
||||
|
||||
helm := &exectest.Helm{
|
||||
FailOnUnexpectedList: true,
|
||||
FailOnUnexpectedDiff: true,
|
||||
}
|
||||
|
||||
app := appWithFs(&App{
|
||||
OverrideHelmBinary: DefaultHelmBinary,
|
||||
fs: &ffs.FileSystem{Glob: filepath.Glob},
|
||||
OverrideKubeContext: "default",
|
||||
DisableKubeVersionAutoDetection: true,
|
||||
Env: "default",
|
||||
Logger: helmexec.NewLogger(os.Stderr, "warn"),
|
||||
helms: map[helmKey]helmexec.Interface{
|
||||
createHelmKey("helm", "default"): helm,
|
||||
},
|
||||
valsRuntime: valsRuntime,
|
||||
ctx: telemetry.CommandContext(),
|
||||
}, files)
|
||||
|
||||
err = app.Template(applyConfig{
|
||||
concurrency: 1,
|
||||
includeTransitiveNeeds: true,
|
||||
logger: app.Logger,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
otlptest.ShutdownTelemetry(t)
|
||||
|
||||
spans := rec.Spans(t)
|
||||
find := func(name string) *v1.Span {
|
||||
return otlptest.FindSpanWhere(t, spans, func(s *v1.Span) bool { return s.Name == name }, name)
|
||||
}
|
||||
|
||||
root := find("helmfile template")
|
||||
discover := find("helmfile.discover_states")
|
||||
load := find("helmfile.load")
|
||||
render := find("helmfile.render")
|
||||
parse := find("helmfile.parse")
|
||||
|
||||
// Every span joins the command trace started by SetupTelemetry.
|
||||
for _, s := range []*v1.Span{discover, load, render, parse} {
|
||||
assert.Equal(t, root.TraceId, s.TraceId, "%s must join the command trace", s.Name)
|
||||
}
|
||||
|
||||
assert.Equal(t, root.SpanId, discover.ParentSpanId, "discover nests under root")
|
||||
assert.Equal(t, root.SpanId, load.ParentSpanId, "load nests under root")
|
||||
assert.Equal(t, load.SpanId, render.ParentSpanId, "render nests under load")
|
||||
assert.Equal(t, load.SpanId, parse.ParentSpanId, "parse nests under load")
|
||||
|
||||
file, ok := otlptest.AttrString(load, "helmfile.state_file")
|
||||
require.True(t, ok)
|
||||
assert.Contains(t, file, "helmfile.yaml.gotmpl")
|
||||
}
|
||||
|
||||
// TestLoadSpansAbsentWhenTelemetryDisabled pins that with telemetry disabled
|
||||
// (the default) a template run behaves exactly as before and exports nothing.
|
||||
func TestLoadSpansAbsentWhenTelemetryDisabled(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
releases:
|
||||
- name: demo
|
||||
chart: incubator/raw
|
||||
`,
|
||||
}
|
||||
|
||||
helm := &exectest.Helm{}
|
||||
|
||||
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
||||
require.NoError(t, err)
|
||||
|
||||
app := appWithFs(&App{
|
||||
OverrideHelmBinary: DefaultHelmBinary,
|
||||
fs: &ffs.FileSystem{Glob: filepath.Glob},
|
||||
OverrideKubeContext: "default",
|
||||
DisableKubeVersionAutoDetection: true,
|
||||
Env: "default",
|
||||
Logger: helmexec.NewLogger(os.Stderr, "warn"),
|
||||
helms: map[helmKey]helmexec.Interface{
|
||||
createHelmKey("helm", "default"): helm,
|
||||
},
|
||||
valsRuntime: valsRuntime,
|
||||
ctx: goContext.Background(),
|
||||
}, files)
|
||||
|
||||
tmplErr := app.Template(applyConfig{
|
||||
concurrency: 1,
|
||||
logger: app.Logger,
|
||||
})
|
||||
require.NoError(t, tmplErr)
|
||||
}
|
||||
|
||||
// TestReleaseSpanHierarchy runs a sync with the exectest fake helm and
|
||||
// asserts per-release spans: present, parented under the load span, and
|
||||
// carrying the release identity attributes.
|
||||
func TestReleaseSpanHierarchy(t *testing.T) {
|
||||
rec := otlptest.NewRecorder(t)
|
||||
otlptest.SetupTelemetry(t, rec, "helmfile sync")
|
||||
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
releases:
|
||||
- name: demo
|
||||
chart: incubator/raw
|
||||
namespace: apps
|
||||
`,
|
||||
}
|
||||
|
||||
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
||||
require.NoError(t, err)
|
||||
|
||||
helm := &exectest.Helm{}
|
||||
|
||||
app := appWithFs(&App{
|
||||
OverrideHelmBinary: DefaultHelmBinary,
|
||||
fs: &ffs.FileSystem{Glob: filepath.Glob},
|
||||
OverrideKubeContext: "default",
|
||||
DisableKubeVersionAutoDetection: true,
|
||||
Env: "default",
|
||||
Logger: helmexec.NewLogger(os.Stderr, "warn"),
|
||||
helms: map[helmKey]helmexec.Interface{
|
||||
createHelmKey("helm", "default"): helm,
|
||||
},
|
||||
valsRuntime: valsRuntime,
|
||||
ctx: telemetry.CommandContext(),
|
||||
}, files)
|
||||
|
||||
syncErr := app.Sync(applyConfig{
|
||||
concurrency: 1,
|
||||
logger: app.Logger,
|
||||
})
|
||||
require.NoError(t, syncErr)
|
||||
|
||||
otlptest.ShutdownTelemetry(t)
|
||||
|
||||
spans := rec.Spans(t)
|
||||
release := otlptest.FindSpanWhere(t, spans, func(s *v1.Span) bool { return s.Name == "helmfile.release.sync" }, "release sync span")
|
||||
load := otlptest.FindSpanWhere(t, spans, func(s *v1.Span) bool { return s.Name == "helmfile.load" }, "load span")
|
||||
|
||||
assert.Equal(t, load.TraceId, release.TraceId, "release span must join the load span's trace")
|
||||
assert.Equal(t, load.SpanId, release.ParentSpanId, "release span must nest under the load span")
|
||||
|
||||
name, ok := otlptest.AttrString(release, "helmfile.release")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "demo", name)
|
||||
ns, ok := otlptest.AttrString(release, "helmfile.namespace")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "apps", ns)
|
||||
chart, ok := otlptest.AttrString(release, "helmfile.chart")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "incubator/raw", chart)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
gocontext "context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/config"
|
||||
"github.com/helmfile/helmfile/pkg/telemetry"
|
||||
)
|
||||
|
||||
func newAppForTest() *App {
|
||||
globalImpl := config.NewGlobalImpl(&config.GlobalOptions{})
|
||||
printEnvImpl := config.NewPrintEnvImpl(globalImpl, &config.PrintEnvOptions{})
|
||||
return New(printEnvImpl)
|
||||
}
|
||||
|
||||
func requireCancelable(t *testing.T, a *App) {
|
||||
t.Helper()
|
||||
require.NotNil(t, a.ctx)
|
||||
require.NotNil(t, Cancel)
|
||||
Cancel()
|
||||
select {
|
||||
case <-a.ctx.Done():
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("app context was not canceled by app.Cancel()")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewPreservesContextContract pins the app.New context behavior that the
|
||||
// telemetry integration relies on: with tracing disabled (the default), the
|
||||
// app context is rooted at context.Background and remains cancellable exactly
|
||||
// as before the telemetry.CommandContext() change.
|
||||
func TestNewPreservesContextContract(t *testing.T) {
|
||||
a := newAppForTest()
|
||||
requireCancelable(t, a)
|
||||
}
|
||||
|
||||
// TestNewAdoptsTelemetryCommandContext verifies that app.New derives its
|
||||
// context from the telemetry command span when tracing is enabled: the app
|
||||
// context carries the command span's trace, so downstream spans nest under it,
|
||||
// and cancellation still works.
|
||||
func TestNewAdoptsTelemetryCommandContext(t *testing.T) {
|
||||
t.Setenv("OTEL_TRACES_EXPORTER", "none")
|
||||
t.Cleanup(func() {
|
||||
_ = telemetry.Shutdown(gocontext.Background(), nil, 0)
|
||||
})
|
||||
|
||||
telemetry.Setup(gocontext.Background(), telemetry.Options{Enabled: true, Version: "test"})
|
||||
telemetry.StartCommandSpan("helmfile test")
|
||||
|
||||
commandSC := trace.SpanFromContext(telemetry.CommandContext()).SpanContext()
|
||||
require.True(t, commandSC.IsValid(), "command span should be recording")
|
||||
|
||||
a := newAppForTest()
|
||||
|
||||
// WithCancel preserves context values, so the command span — and its trace
|
||||
// ID — must still be visible through the app context.
|
||||
appSC := trace.SpanFromContext(a.ctx).SpanContext()
|
||||
require.True(t, appSC.IsValid())
|
||||
require.Equal(t, commandSC.TraceID(), appSC.TraceID(), "app context must derive from the command span context")
|
||||
|
||||
requireCancelable(t, a)
|
||||
}
|
||||
@@ -38,7 +38,7 @@ releases:
|
||||
|
||||
// Run ListReleases multiple times to verify consistent ordering
|
||||
var outputs []string
|
||||
for i := 0; i < 5; i++ {
|
||||
for i := range 5 {
|
||||
var buffer bytes.Buffer
|
||||
syncWriter := testhelper.NewSyncWriter(&buffer)
|
||||
logger := helmexec.NewLogger(syncWriter, "debug")
|
||||
|
||||
+22
-3
@@ -13,6 +13,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/helmfile/vals"
|
||||
@@ -2369,6 +2370,7 @@ type configImpl struct {
|
||||
skipTests bool
|
||||
skipSchemaValidation bool
|
||||
skipRefresh bool
|
||||
allowFailedReleases bool
|
||||
|
||||
skipNeeds bool
|
||||
includeNeeds bool
|
||||
@@ -2416,6 +2418,10 @@ func (c configImpl) SkipRefresh() bool {
|
||||
return c.skipRefresh
|
||||
}
|
||||
|
||||
func (c configImpl) AllowFailedReleases() bool {
|
||||
return c.allowFailedReleases
|
||||
}
|
||||
|
||||
func (c configImpl) SkipNeeds() bool {
|
||||
return c.skipNeeds
|
||||
}
|
||||
@@ -2508,6 +2514,7 @@ type applyConfig struct {
|
||||
skipCRDs bool
|
||||
skipDeps bool
|
||||
skipRefresh bool
|
||||
allowFailedReleases bool
|
||||
skipNeeds bool
|
||||
includeNeeds bool
|
||||
includeTransitiveNeeds bool
|
||||
@@ -2550,6 +2557,8 @@ type applyConfig struct {
|
||||
trackMode string
|
||||
trackTimeout int
|
||||
trackLogs bool
|
||||
trackLogsUntilReady bool
|
||||
trackLogsInterval time.Duration
|
||||
trackFailOnError bool
|
||||
|
||||
// template-only options
|
||||
@@ -2609,6 +2618,10 @@ func (a applyConfig) SkipRefresh() bool {
|
||||
return a.skipRefresh
|
||||
}
|
||||
|
||||
func (a applyConfig) AllowFailedReleases() bool {
|
||||
return a.allowFailedReleases
|
||||
}
|
||||
|
||||
func (a applyConfig) SkipNeeds() bool {
|
||||
return a.skipNeeds
|
||||
}
|
||||
@@ -2790,10 +2803,18 @@ func (a applyConfig) TrackLogs() bool {
|
||||
return a.trackLogs
|
||||
}
|
||||
|
||||
func (a applyConfig) TrackLogsUntilReady() bool {
|
||||
return a.trackLogsUntilReady
|
||||
}
|
||||
|
||||
func (a applyConfig) TrackFailedLogs() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a applyConfig) TrackLogsInterval() time.Duration {
|
||||
return a.trackLogsInterval
|
||||
}
|
||||
|
||||
func (a applyConfig) HelmStuckGrace() int {
|
||||
return 0
|
||||
}
|
||||
@@ -5059,9 +5080,7 @@ func TestGetArgs(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
Helmdefaults := state.HelmSpec{KubeContext: "test", Args: test.defaultArgs}
|
||||
testState := &state.HelmState{
|
||||
ReleaseSetSpec: state.ReleaseSetSpec{
|
||||
HelmDefaults: Helmdefaults,
|
||||
},
|
||||
HelmDefaults: Helmdefaults,
|
||||
}
|
||||
receivedArgs := GetArgs(test.args, testState)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/agent/llm"
|
||||
@@ -18,6 +20,7 @@ type ConfigProvider interface {
|
||||
RepoRetry() int
|
||||
SkipDeps() bool
|
||||
SkipRefresh() bool
|
||||
AllowFailedReleases() bool
|
||||
SequentialHelmfiles() bool
|
||||
|
||||
FileOrDir() string
|
||||
@@ -62,6 +65,7 @@ type ApplyConfigProvider interface {
|
||||
SkipCRDs() bool
|
||||
SkipDeps() bool
|
||||
SkipRefresh() bool
|
||||
AllowFailedReleases() bool
|
||||
Wait() bool
|
||||
WaitRetries() int
|
||||
WaitForJobs() bool
|
||||
@@ -99,7 +103,9 @@ type ApplyConfigProvider interface {
|
||||
TrackMode() string
|
||||
TrackTimeout() int
|
||||
TrackLogs() bool
|
||||
TrackLogsUntilReady() bool
|
||||
TrackFailedLogs() bool
|
||||
TrackLogsInterval() time.Duration
|
||||
HelmStuckGrace() int
|
||||
TrackFailOnError() bool
|
||||
|
||||
@@ -126,6 +132,7 @@ type SyncConfigProvider interface {
|
||||
SkipCRDs() bool
|
||||
SkipDeps() bool
|
||||
SkipRefresh() bool
|
||||
AllowFailedReleases() bool
|
||||
Wait() bool
|
||||
WaitRetries() int
|
||||
WaitForJobs() bool
|
||||
@@ -142,7 +149,9 @@ type SyncConfigProvider interface {
|
||||
TrackMode() string
|
||||
TrackTimeout() int
|
||||
TrackLogs() bool
|
||||
TrackLogsUntilReady() bool
|
||||
TrackFailedLogs() bool
|
||||
TrackLogsInterval() time.Duration
|
||||
HelmStuckGrace() int
|
||||
TrackFailOnError() bool
|
||||
|
||||
@@ -174,6 +183,7 @@ type DiffConfigProvider interface {
|
||||
SkipCRDs() bool
|
||||
SkipDeps() bool
|
||||
SkipRefresh() bool
|
||||
AllowFailedReleases() bool
|
||||
|
||||
IncludeTests() bool
|
||||
|
||||
@@ -231,6 +241,7 @@ type DestroyConfigProvider interface {
|
||||
|
||||
SkipDeps() bool
|
||||
SkipRefresh() bool
|
||||
AllowFailedReleases() bool
|
||||
SkipCharts() bool
|
||||
DeleteWait() bool
|
||||
DeleteTimeout() int
|
||||
@@ -246,6 +257,7 @@ type TestConfigProvider interface {
|
||||
|
||||
SkipDeps() bool
|
||||
SkipRefresh() bool
|
||||
AllowFailedReleases() bool
|
||||
Timeout() int
|
||||
Cleanup() bool
|
||||
Logs() bool
|
||||
@@ -260,6 +272,7 @@ type LintConfigProvider interface {
|
||||
Set() []string
|
||||
SkipDeps() bool
|
||||
SkipRefresh() bool
|
||||
AllowFailedReleases() bool
|
||||
SkipCleanup() bool
|
||||
|
||||
DAGConfig
|
||||
@@ -277,6 +290,7 @@ type UnittestConfigProvider interface {
|
||||
DebugPlugin() bool
|
||||
SkipDeps() bool
|
||||
SkipRefresh() bool
|
||||
AllowFailedReleases() bool
|
||||
SkipCleanup() bool
|
||||
|
||||
DAGConfig
|
||||
@@ -287,6 +301,7 @@ type UnittestConfigProvider interface {
|
||||
type FetchConfigProvider interface {
|
||||
SkipDeps() bool
|
||||
SkipRefresh() bool
|
||||
AllowFailedReleases() bool
|
||||
OutputDir() string
|
||||
OutputDirTemplate() string
|
||||
WriteOutput() bool
|
||||
@@ -306,6 +321,7 @@ type TemplateConfigProvider interface {
|
||||
Validate() bool
|
||||
SkipDeps() bool
|
||||
SkipRefresh() bool
|
||||
AllowFailedReleases() bool
|
||||
SkipCleanup() bool
|
||||
SkipTests() bool
|
||||
OutputDir() string
|
||||
@@ -341,6 +357,7 @@ type WriteValuesConfigProvider interface {
|
||||
|
||||
type StatusesConfigProvider interface {
|
||||
Args() string
|
||||
AllowFailedReleases() bool
|
||||
|
||||
concurrencyConfig
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@ func TestContextConcurrentAccess(t *testing.T) {
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
// Launch multiple goroutines that concurrently update the repos map
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
for i := range numGoroutines {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < numReposPerGoroutine; j++ {
|
||||
for j := range numReposPerGoroutine {
|
||||
repoKey := "repo-" + string(rune('0'+goroutineID)) + "-" + string(rune('0'+j))
|
||||
|
||||
ctx.mu.Lock()
|
||||
@@ -129,10 +129,10 @@ func TestContextConcurrentReadWrite(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Launch multiple goroutines for each repo
|
||||
for i := 0; i < numRepos; i++ {
|
||||
for i := range numRepos {
|
||||
repoKey := "repo-" + string(rune('0'+i)) + "=https://example.com"
|
||||
|
||||
for j := 0; j < numGoroutinesPerRepo; j++ {
|
||||
for range numGoroutinesPerRepo {
|
||||
wg.Add(1)
|
||||
go func(key string) {
|
||||
defer wg.Done()
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/helmfile/vals"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/exectest"
|
||||
ffs "github.com/helmfile/helmfile/pkg/filesystem"
|
||||
"github.com/helmfile/helmfile/pkg/helmexec"
|
||||
"github.com/helmfile/helmfile/pkg/state"
|
||||
)
|
||||
|
||||
func fakeConverge(failures map[string]bool, executed *[]string) func(*state.HelmState, helmexec.Interface) (bool, []error) {
|
||||
return func(st *state.HelmState, _ helmexec.Interface) (bool, []error) {
|
||||
var errs []error
|
||||
for i := range st.Releases {
|
||||
release := &st.Releases[i]
|
||||
*executed = append(*executed, release.Name)
|
||||
if failures[release.Name] {
|
||||
err := fmt.Errorf("failed processing release %s: boom", release.Name)
|
||||
errs = append(errs, state.NewReleaseError(release, err, state.ReleaseErrorCodeFailure))
|
||||
}
|
||||
}
|
||||
|
||||
return len(st.Releases) > 0, errs
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithBatches_FailFastByDefault(t *testing.T) {
|
||||
templated := &state.HelmState{}
|
||||
var executed []string
|
||||
|
||||
batches := [][]state.Release{
|
||||
{
|
||||
state.Release{Name: "database", Needs: nil, ContinueOnError: nil},
|
||||
state.Release{Name: "cache", Needs: nil, ContinueOnError: nil},
|
||||
},
|
||||
{
|
||||
state.Release{Name: "worker", Needs: nil, ContinueOnError: nil},
|
||||
},
|
||||
}
|
||||
|
||||
any, errs := withBatches("processing", templated, batches, &exectest.Helm{}, zap.NewNop().Sugar(), fakeConverge(map[string]bool{"database": true}, &executed))
|
||||
|
||||
require.False(t, any)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, []string{"database", "cache"}, executed)
|
||||
require.Equal(t, "failed processing release database: boom", errs[0].Error())
|
||||
}
|
||||
|
||||
func TestWithBatches_ContinueOnErrorKeepsIndependentBranches(t *testing.T) {
|
||||
templated := &state.HelmState{}
|
||||
var executed []string
|
||||
|
||||
batches := [][]state.Release{
|
||||
{
|
||||
state.Release{Name: "database", Needs: nil, ContinueOnError: new(true)},
|
||||
state.Release{Name: "cache", Needs: nil, ContinueOnError: nil},
|
||||
},
|
||||
{
|
||||
state.Release{Name: "worker", Needs: nil, ContinueOnError: nil},
|
||||
},
|
||||
}
|
||||
|
||||
any, errs := withBatches("processing", templated, batches, &exectest.Helm{}, zap.NewNop().Sugar(), fakeConverge(map[string]bool{"database": true}, &executed))
|
||||
|
||||
require.True(t, any)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, []string{"database", "cache", "worker"}, executed)
|
||||
require.Equal(t, "failed processing release database: boom", errs[0].Error())
|
||||
}
|
||||
|
||||
func TestWithBatches_BlocksDependentReleasesAndAggregatesErrors(t *testing.T) {
|
||||
templated := &state.HelmState{}
|
||||
var executed []string
|
||||
|
||||
batches := [][]state.Release{
|
||||
{
|
||||
state.Release{Name: "database", Needs: nil, ContinueOnError: new(true)},
|
||||
},
|
||||
{
|
||||
state.Release{Name: "backend", Needs: []string{"database"}, ContinueOnError: new(true)},
|
||||
state.Release{Name: "worker", Needs: nil, ContinueOnError: nil},
|
||||
},
|
||||
{
|
||||
state.Release{Name: "frontend", Needs: []string{"backend"}, ContinueOnError: new(true)},
|
||||
},
|
||||
}
|
||||
|
||||
any, errs := withBatches("processing", templated, batches, &exectest.Helm{}, zap.NewNop().Sugar(), fakeConverge(map[string]bool{"database": true}, &executed))
|
||||
|
||||
require.True(t, any)
|
||||
require.Len(t, errs, 3)
|
||||
require.Equal(t, []string{"database", "worker"}, executed)
|
||||
require.Equal(t, "failed processing release database: boom", errs[0].Error())
|
||||
require.Contains(t, errs[1].Error(), "release \"backend\" was skipped because dependency \"database\" failed")
|
||||
require.Contains(t, errs[2].Error(), "release \"frontend\" was skipped because dependency \"backend\" failed")
|
||||
}
|
||||
|
||||
func TestWithBatches_AggregatesMultipleReleaseErrors(t *testing.T) {
|
||||
templated := &state.HelmState{}
|
||||
var executed []string
|
||||
|
||||
batches := [][]state.Release{
|
||||
{
|
||||
state.Release{Name: "app-a", Needs: nil, ContinueOnError: new(true)},
|
||||
state.Release{Name: "app-b", Needs: nil, ContinueOnError: new(true)},
|
||||
},
|
||||
{
|
||||
state.Release{Name: "worker", Needs: nil, ContinueOnError: nil},
|
||||
},
|
||||
}
|
||||
|
||||
any, errs := withBatches("processing", templated, batches, &exectest.Helm{}, zap.NewNop().Sugar(), fakeConverge(map[string]bool{"app-a": true, "app-b": true}, &executed))
|
||||
|
||||
require.True(t, any)
|
||||
require.Len(t, errs, 2)
|
||||
require.Equal(t, []string{"app-a", "app-b", "worker"}, executed)
|
||||
for _, releaseName := range []string{"app-a", "app-b"} {
|
||||
require.Contains(t, fmt.Sprint(errs), releaseName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithBatches_TreatsNonFailureReleaseErrorCodeAsFatal(t *testing.T) {
|
||||
templated := &state.HelmState{}
|
||||
var executed []string
|
||||
|
||||
// e.g. the helm-diff "changes detected" exit code 2, which is not a
|
||||
// release failure and must never enable continuation.
|
||||
converge := func(st *state.HelmState, _ helmexec.Interface) (bool, []error) {
|
||||
var errs []error
|
||||
for i := range st.Releases {
|
||||
release := &st.Releases[i]
|
||||
executed = append(executed, release.Name)
|
||||
errs = append(errs, state.NewReleaseError(release, fmt.Errorf("release %s: changed", release.Name), 2))
|
||||
}
|
||||
return false, errs
|
||||
}
|
||||
|
||||
batches := [][]state.Release{
|
||||
{
|
||||
state.Release{Name: "database", Needs: nil, ContinueOnError: new(true)},
|
||||
},
|
||||
{
|
||||
state.Release{Name: "worker", Needs: nil, ContinueOnError: nil},
|
||||
},
|
||||
}
|
||||
|
||||
any, errs := withBatches("processing", templated, batches, &exectest.Helm{}, zap.NewNop().Sugar(), converge)
|
||||
|
||||
require.False(t, any)
|
||||
require.Len(t, errs, 1)
|
||||
require.Equal(t, []string{"database"}, executed)
|
||||
}
|
||||
|
||||
// TestSync_ContinueOnError exercises the whole sync pipeline with the exectest
|
||||
// fake helm: the release named "error-*" fails to upgrade, and continueOnError
|
||||
// on that release must (1) let the independent release still be deployed,
|
||||
// (2) skip the dependent release with an explicit error, and (3) keep the
|
||||
// overall command failing with a non-zero exit code.
|
||||
func TestSync_ContinueOnError(t *testing.T) {
|
||||
newApp := func(t *testing.T, helm *exectest.Helm, files map[string]string, logger *zap.SugaredLogger) *App {
|
||||
t.Helper()
|
||||
|
||||
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
||||
require.NoError(t, err)
|
||||
|
||||
return appWithFs(&App{
|
||||
OverrideHelmBinary: DefaultHelmBinary,
|
||||
fs: ffs.DefaultFileSystem(),
|
||||
OverrideKubeContext: "default",
|
||||
DisableKubeVersionAutoDetection: true,
|
||||
Env: "default",
|
||||
Logger: logger,
|
||||
helms: map[helmKey]helmexec.Interface{
|
||||
createHelmKey("helm", "default"): helm,
|
||||
},
|
||||
valsRuntime: valsRuntime,
|
||||
}, files)
|
||||
}
|
||||
|
||||
t.Run("independent release continues and dependent release is skipped", func(t *testing.T) {
|
||||
var helm = &exectest.Helm{
|
||||
FailOnUnexpectedList: true,
|
||||
DiffMutex: &sync.Mutex{},
|
||||
ChartsMutex: &sync.Mutex{},
|
||||
ReleasesMutex: &sync.Mutex{},
|
||||
}
|
||||
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
releases:
|
||||
- name: error-database
|
||||
chart: incubator/raw
|
||||
namespace: default
|
||||
continueOnError: true
|
||||
|
||||
- name: independent-app
|
||||
chart: incubator/raw
|
||||
namespace: default
|
||||
|
||||
- name: dependent-backend
|
||||
chart: incubator/raw
|
||||
namespace: default
|
||||
needs:
|
||||
- default/error-database
|
||||
`,
|
||||
}
|
||||
|
||||
logger := zap.NewNop().Sugar()
|
||||
|
||||
app := newApp(t, helm, files, logger)
|
||||
syncErr := app.Sync(applyConfig{
|
||||
concurrency: 1,
|
||||
logger: logger,
|
||||
})
|
||||
|
||||
require.Error(t, syncErr, "sync must exit non-zero even when errors are tolerated")
|
||||
|
||||
appErr, ok := syncErr.(*Error)
|
||||
require.True(t, ok, "expected *app.Error, got %T", syncErr)
|
||||
assert.Equal(t, 1, appErr.Code())
|
||||
|
||||
// independent-app must have been deployed despite error-database failing
|
||||
synced := make([]string, 0, len(helm.Releases))
|
||||
for _, r := range helm.Releases {
|
||||
synced = append(synced, r.Name)
|
||||
}
|
||||
assert.Contains(t, synced, "independent-app")
|
||||
assert.NotContains(t, synced, "dependent-backend", "release depending on a failed release must be skipped")
|
||||
assert.NotContains(t, synced, "error-database")
|
||||
|
||||
assert.Contains(t, syncErr.Error(), `release "dependent-backend" was skipped because dependency "error-database" failed`)
|
||||
})
|
||||
|
||||
t.Run("fail-fast remains the default", func(t *testing.T) {
|
||||
var helm = &exectest.Helm{
|
||||
FailOnUnexpectedList: true,
|
||||
DiffMutex: &sync.Mutex{},
|
||||
ChartsMutex: &sync.Mutex{},
|
||||
ReleasesMutex: &sync.Mutex{},
|
||||
}
|
||||
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
releases:
|
||||
- name: error-database
|
||||
chart: incubator/raw
|
||||
namespace: default
|
||||
|
||||
- name: base
|
||||
chart: incubator/raw
|
||||
namespace: default
|
||||
|
||||
- name: later-app
|
||||
chart: incubator/raw
|
||||
namespace: default
|
||||
needs:
|
||||
- default/base
|
||||
`,
|
||||
}
|
||||
|
||||
logger := zap.NewNop().Sugar()
|
||||
|
||||
app := newApp(t, helm, files, logger)
|
||||
syncErr := app.Sync(applyConfig{
|
||||
concurrency: 1,
|
||||
logger: logger,
|
||||
})
|
||||
|
||||
require.Error(t, syncErr)
|
||||
|
||||
synced := make([]string, 0, len(helm.Releases))
|
||||
for _, r := range helm.Releases {
|
||||
synced = append(synced, r.Name)
|
||||
}
|
||||
// base shares the first batch with error-database and is therefore
|
||||
// still attempted, but the second batch must never run once the
|
||||
// failure is reported.
|
||||
assert.Contains(t, synced, "base")
|
||||
assert.NotContains(t, synced, "later-app", "without continueOnError the default fail-fast behavior must stop the run")
|
||||
})
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
goContext "context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -10,6 +11,8 @@ import (
|
||||
|
||||
"dario.cat/mergo"
|
||||
"github.com/helmfile/vals"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/environment"
|
||||
@@ -19,6 +22,7 @@ import (
|
||||
"github.com/helmfile/helmfile/pkg/policy"
|
||||
"github.com/helmfile/helmfile/pkg/remote"
|
||||
"github.com/helmfile/helmfile/pkg/state"
|
||||
"github.com/helmfile/helmfile/pkg/telemetry"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -32,6 +36,11 @@ type desiredStateLoader struct {
|
||||
overrideKustomizeBinary string
|
||||
enableLiveOutput bool
|
||||
|
||||
// traceCtx carries the helmfile.load span context; render/parse spans
|
||||
// attach to it. nil falls back to context.Background, which with tracing
|
||||
// disabled makes span starts no-ops.
|
||||
traceCtx goContext.Context
|
||||
|
||||
env string
|
||||
namespace string
|
||||
chart string
|
||||
@@ -47,6 +56,14 @@ type desiredStateLoader struct {
|
||||
lockFilePath string
|
||||
}
|
||||
|
||||
// spanCtx returns the loader's span parent context.
|
||||
func (ld *desiredStateLoader) spanCtx() goContext.Context {
|
||||
if ld.traceCtx != nil {
|
||||
return ld.traceCtx
|
||||
}
|
||||
return goContext.Background()
|
||||
}
|
||||
|
||||
func (ld *desiredStateLoader) Load(f string, opts LoadOpts) (*state.HelmState, error) {
|
||||
var overrodeEnv *environment.Environment
|
||||
|
||||
@@ -238,6 +255,16 @@ func (a *desiredStateLoader) rawLoad(yaml []byte, baseDir, file string, evaluate
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// parsePart wraps rawLoad for one document part in a helmfile.parse span.
|
||||
func (ld *desiredStateLoader) parsePart(rawContent []byte, baseDir, filename string, evaluateBases bool, env, overrodeEnv *environment.Environment) (*state.HelmState, error) {
|
||||
_, span := telemetry.Tracer(telemetry.ScopeHelmfile).Start(ld.spanCtx(), "helmfile.parse",
|
||||
trace.WithAttributes(attribute.String("helmfile.state_file", filename)),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
return ld.rawLoad(rawContent, baseDir, filename, evaluateBases, env, overrodeEnv)
|
||||
}
|
||||
|
||||
func (ld *desiredStateLoader) load(env, overrodeEnv *environment.Environment, baseDir, filename string, content []byte, evaluateBases bool) (*state.HelmState, error) {
|
||||
// Allows part-splitting to work with CLRF-ed content
|
||||
normalizedContent := bytes.ReplaceAll(content, []byte("\r\n"), []byte("\n"))
|
||||
@@ -280,14 +307,7 @@ func (ld *desiredStateLoader) load(env, overrodeEnv *environment.Environment, ba
|
||||
rawContent = part
|
||||
}
|
||||
|
||||
currentState, err := ld.rawLoad(
|
||||
rawContent,
|
||||
baseDir,
|
||||
filename,
|
||||
evaluateBases,
|
||||
env,
|
||||
overrodeEnv,
|
||||
)
|
||||
currentState, err := ld.parsePart(rawContent, baseDir, filename, evaluateBases, env, overrodeEnv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ type destroyConfig struct {
|
||||
interactive bool
|
||||
skipDeps bool
|
||||
skipRefresh bool
|
||||
allowFailedReleases bool
|
||||
logger *zap.SugaredLogger
|
||||
includeTransitiveNeeds bool
|
||||
skipCharts bool
|
||||
@@ -78,6 +79,10 @@ func (d destroyConfig) SkipRefresh() bool {
|
||||
return d.skipRefresh
|
||||
}
|
||||
|
||||
func (d destroyConfig) AllowFailedReleases() bool {
|
||||
return d.allowFailedReleases
|
||||
}
|
||||
|
||||
func (d destroyConfig) IncludeTransitiveNeeds() bool {
|
||||
return d.includeTransitiveNeeds
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ type diffConfig struct {
|
||||
skipCRDs bool
|
||||
skipDeps bool
|
||||
skipRefresh bool
|
||||
allowFailedReleases bool
|
||||
includeTests bool
|
||||
skipNeeds bool
|
||||
includeNeeds bool
|
||||
@@ -87,6 +88,10 @@ func (a diffConfig) SkipRefresh() bool {
|
||||
return a.skipRefresh
|
||||
}
|
||||
|
||||
func (a diffConfig) AllowFailedReleases() bool {
|
||||
return a.allowFailedReleases
|
||||
}
|
||||
|
||||
func (a diffConfig) IncludeTests() bool {
|
||||
return a.includeTests
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/filesystem"
|
||||
"github.com/helmfile/helmfile/pkg/remote"
|
||||
"github.com/helmfile/helmfile/pkg/state"
|
||||
)
|
||||
|
||||
// RootHelmfileDir returns the absolute directory of the top-level helmfile,
|
||||
// or "" when the root is a remote URL or otherwise unresolvable. Callers
|
||||
// treat "" as "dir-based filtering not available". The value is cached by
|
||||
// resolveRootHelmfileDir; it must be invoked before any goroutine fan-out
|
||||
// or working-directory change so that the CWD-sensitive resolution lands
|
||||
// against the process CWD at command start, not the (possibly chdir'd)
|
||||
// directory of a leaf helmfile being processed.
|
||||
func (a *App) RootHelmfileDir() string {
|
||||
return a.rootHelmfileDir
|
||||
}
|
||||
|
||||
// resolveRootHelmfileDir computes and caches the root directory anchor for
|
||||
// dir-based filtering. Idempotent; safe to call multiple times. Must be
|
||||
// called from the top-level entry of each command, before within() or any
|
||||
// goroutine spawn.
|
||||
func (a *App) resolveRootHelmfileDir() {
|
||||
a.rootHelmfileDirOnce.Do(func() {
|
||||
a.rootHelmfileDir = a.computeRootHelmfileDir()
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) computeRootHelmfileDir() string {
|
||||
dir, err := DirSelectorRoot(a.fs, a.FileOrDir)
|
||||
if err != nil {
|
||||
a.Logger.Debugf("dir selector unavailable: cannot resolve root helmfile directory: %v", err)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// DirSelectorRoot returns the absolute directory that dir= selector values
|
||||
// are relative to for the given -f value: the working directory when it is
|
||||
// empty, the directory itself when it points to one, the file's directory
|
||||
// otherwise. Returns "" for a remote helmfile.
|
||||
func DirSelectorRoot(fs *filesystem.FileSystem, fileOrDir string) (string, error) {
|
||||
if fileOrDir == "" {
|
||||
return fs.Getwd()
|
||||
}
|
||||
if remote.IsRemote(fileOrDir) {
|
||||
return "", nil
|
||||
}
|
||||
absPath, err := fs.Abs(fileOrDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if fs.DirectoryExistsAt(absPath) {
|
||||
return absPath, nil
|
||||
}
|
||||
return filepath.Dir(absPath), nil
|
||||
}
|
||||
|
||||
// dirSelectorGroup holds the positive dir= target values inside one -l
|
||||
// argument. An empty targets slice means the group has no dir= constraint
|
||||
// and is therefore path-permissive during traversal-skip.
|
||||
type dirSelectorGroup struct {
|
||||
targets []string
|
||||
}
|
||||
|
||||
// extractDirSelectorTargets returns one dirSelectorGroup per -l argument
|
||||
// with that group's positive dir= values. Negative dir!= is intentionally
|
||||
// dropped: skipping a branch on a negative constraint would require proving
|
||||
// it contains only excluded releases, which generally requires loading it.
|
||||
// Malformed groups are treated as path-permissive so traversal does not
|
||||
// short-circuit on selector strings the filter will later report on.
|
||||
// Returns nil when no positive dir= appears, so callers can skip the check.
|
||||
func extractDirSelectorTargets(selectors []string) []dirSelectorGroup {
|
||||
if len(selectors) == 0 {
|
||||
return nil
|
||||
}
|
||||
groups := make([]dirSelectorGroup, 0, len(selectors))
|
||||
anyDir := false
|
||||
for _, s := range selectors {
|
||||
targets, err := state.PositiveDirTargets(s)
|
||||
if err != nil {
|
||||
groups = append(groups, dirSelectorGroup{})
|
||||
continue
|
||||
}
|
||||
groups = append(groups, dirSelectorGroup{targets: targets})
|
||||
if len(targets) > 0 {
|
||||
anyDir = true
|
||||
}
|
||||
}
|
||||
if !anyDir {
|
||||
return nil
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
// skipForDirFilter reports whether a `helmfiles:` entry can be skipped for
|
||||
// the given selectors. Remote entries and unresolvable roots always descend.
|
||||
func (a *App) skipForDirFilter(stateDir, entryPath string, selectors []string) bool {
|
||||
rootDir := a.RootHelmfileDir()
|
||||
if rootDir == "" || remote.IsRemote(entryPath) {
|
||||
return false
|
||||
}
|
||||
groups := extractDirSelectorTargets(selectors)
|
||||
if len(groups) == 0 {
|
||||
return false
|
||||
}
|
||||
entryDir, ok := a.entryDirRelativeToRoot(rootDir, stateDir, entryPath)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return !shouldDescendForDirFilter(entryDir, groups)
|
||||
}
|
||||
|
||||
// entryDirRelativeToRoot returns the directory holding the helmfile(s) of a
|
||||
// `helmfiles:` entry, relative to rootDir: the entry itself when it is a
|
||||
// directory, its parent otherwise. Returns false when the entry lies outside
|
||||
// rootDir, signaling the caller to descend conservatively.
|
||||
func (a *App) entryDirRelativeToRoot(rootDir, stateDir, entryPath string) (string, bool) {
|
||||
abs := entryPath
|
||||
if !filepath.IsAbs(abs) {
|
||||
abs = filepath.Join(stateDir, entryPath)
|
||||
}
|
||||
if !a.fs.DirectoryExistsAt(abs) {
|
||||
abs = filepath.Dir(abs)
|
||||
}
|
||||
return state.DirRelativeToRoot(rootDir, abs)
|
||||
}
|
||||
|
||||
// shouldDescendForDirFilter returns true when a sub-helmfile located in
|
||||
// entryDir (relative to the root) could contain a release matching at least
|
||||
// one of the dir-selector groups. A branch is relevant for a dir=target when
|
||||
// it lives at or below target, or when target lives below it (e.g. an
|
||||
// aggregator at apps/helmfile.yaml for target apps/x). The decision only
|
||||
// looks at the entry's own location, so it assumes nested entries stay inside
|
||||
// their parent's directory.
|
||||
func shouldDescendForDirFilter(entryDir string, groups []dirSelectorGroup) bool {
|
||||
if len(groups) == 0 || entryDir == "." {
|
||||
return true
|
||||
}
|
||||
for _, g := range groups {
|
||||
conflicts := slices.ContainsFunc(g.targets, func(target string) bool {
|
||||
return !state.DirsCompatible(entryDir, target)
|
||||
})
|
||||
if !conflicts {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/helmfile/vals"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/testhelper"
|
||||
)
|
||||
|
||||
func TestExtractDirSelectorTargets(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
selectors []string
|
||||
wantNil bool
|
||||
wantPer []int
|
||||
}{
|
||||
{
|
||||
name: "nil selectors",
|
||||
selectors: nil,
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "no dir selectors",
|
||||
selectors: []string{"name=foo", "namespace=bar"},
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "single dir selector",
|
||||
selectors: []string{"dir=apps/x"},
|
||||
wantPer: []int{1},
|
||||
},
|
||||
{
|
||||
name: "dir with companion in same group",
|
||||
selectors: []string{"dir=apps/x,name=foo"},
|
||||
wantPer: []int{1},
|
||||
},
|
||||
{
|
||||
name: "dir spread across groups",
|
||||
selectors: []string{"dir=apps/x", "dir=apps/y"},
|
||||
wantPer: []int{1, 1},
|
||||
},
|
||||
{
|
||||
name: "negative dir is ignored",
|
||||
selectors: []string{"dir!=apps/x"},
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "mixed group with no dir and group with dir",
|
||||
selectors: []string{"name=foo", "dir=apps/x"},
|
||||
wantPer: []int{0, 1},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
groups := extractDirSelectorTargets(tt.selectors)
|
||||
if tt.wantNil {
|
||||
assert.Nil(t, groups)
|
||||
return
|
||||
}
|
||||
assert.Len(t, groups, len(tt.wantPer))
|
||||
for i, want := range tt.wantPer {
|
||||
assert.Len(t, groups[i].targets, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldDescendForDirFilter(t *testing.T) {
|
||||
const target = "opendesk/helmfile/apps/openproject"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
entryDir string
|
||||
groups []dirSelectorGroup
|
||||
wantSkip bool
|
||||
}{
|
||||
{
|
||||
name: "no groups means caller skipped check; treat as descend",
|
||||
entryDir: "opendesk/helmfile/apps/openproject",
|
||||
groups: nil,
|
||||
},
|
||||
{
|
||||
name: "entry in target dir → descend",
|
||||
entryDir: "opendesk/helmfile/apps/openproject",
|
||||
groups: []dirSelectorGroup{{targets: []string{target}}},
|
||||
},
|
||||
{
|
||||
name: "entry below target → descend",
|
||||
entryDir: "opendesk/helmfile/apps/openproject/sub",
|
||||
groups: []dirSelectorGroup{{targets: []string{target}}},
|
||||
},
|
||||
{
|
||||
name: "sibling service → skip",
|
||||
entryDir: "opendesk/helmfile/apps/xwiki",
|
||||
groups: []dirSelectorGroup{{targets: []string{target}}},
|
||||
wantSkip: true,
|
||||
},
|
||||
{
|
||||
name: "name-collision is not a match → skip",
|
||||
entryDir: "opendesk/helmfile/apps/openproject-extras",
|
||||
groups: []dirSelectorGroup{{targets: []string{target}}},
|
||||
wantSkip: true,
|
||||
},
|
||||
{
|
||||
name: "entry is ancestor of target → descend",
|
||||
entryDir: "opendesk",
|
||||
groups: []dirSelectorGroup{{targets: []string{target}}},
|
||||
},
|
||||
{
|
||||
name: "entry in root dir → descend",
|
||||
entryDir: ".",
|
||||
groups: []dirSelectorGroup{{targets: []string{target}}},
|
||||
},
|
||||
{
|
||||
name: "group with no targets is permissive",
|
||||
entryDir: "opendesk/helmfile/apps/xwiki",
|
||||
groups: []dirSelectorGroup{{targets: nil}, {targets: []string{target}}},
|
||||
},
|
||||
{
|
||||
name: "multiple targets in one group: all must allow",
|
||||
entryDir: "opendesk/helmfile/apps/openproject",
|
||||
groups: []dirSelectorGroup{{targets: []string{target, "opendesk/helmfile/apps/xwiki"}}},
|
||||
wantSkip: true,
|
||||
},
|
||||
{
|
||||
name: "OR across groups: either matches descends",
|
||||
entryDir: "opendesk/helmfile/apps/openproject",
|
||||
groups: []dirSelectorGroup{{targets: []string{"opendesk/helmfile/apps/xwiki"}}, {targets: []string{target}}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, !tt.wantSkip, shouldDescendForDirFilter(tt.entryDir, tt.groups))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newDirFilterTestApp(t *testing.T, files map[string]string, fileOrDir string, selectors []string) (*App, *testhelper.TestFs) {
|
||||
t.Helper()
|
||||
|
||||
testFs := testhelper.NewTestFs(files)
|
||||
testFs.Cwd = "/workspace"
|
||||
|
||||
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
||||
require.NoError(t, err)
|
||||
|
||||
app := &App{
|
||||
OverrideHelmBinary: DefaultHelmBinary,
|
||||
OverrideKubeContext: "default",
|
||||
DisableKubeVersionAutoDetection: true,
|
||||
Env: "default",
|
||||
Logger: newAppTestLogger(),
|
||||
valsRuntime: valsRuntime,
|
||||
FileOrDir: fileOrDir,
|
||||
Selectors: selectors,
|
||||
}
|
||||
app = injectFs(app, testFs)
|
||||
expectNoCallsToHelm(app)
|
||||
return app, testFs
|
||||
}
|
||||
|
||||
// TestProcessNestedHelmfiles_DirFilterSkipsSiblings drives the full visit
|
||||
// chain against an opendesk-shaped fixture: a root helmfile that includes an
|
||||
// aggregator, which in turn lists several sibling service helmfiles. With
|
||||
// `-l dir=…/openproject` only the matching service helmfile should be opened,
|
||||
// asserted by counting how often a helmfile-child.yaml file is read. The
|
||||
// release names also let us confirm the post-load filter sees only the
|
||||
// expected entry.
|
||||
func TestProcessNestedHelmfiles_DirFilterSkipsSiblings(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/workspace/helmfile.yaml": `
|
||||
helmfiles:
|
||||
- opendesk/helmfile_generic.yaml
|
||||
`,
|
||||
"/workspace/opendesk/helmfile_generic.yaml": `
|
||||
helmfiles:
|
||||
- apps/openproject/helmfile-child.yaml
|
||||
- apps/xwiki/helmfile-child.yaml
|
||||
- apps/jitsi/helmfile-child.yaml
|
||||
`,
|
||||
"/workspace/opendesk/apps/openproject/helmfile-child.yaml": `
|
||||
releases:
|
||||
- name: openproject-web
|
||||
chart: stable/openproject
|
||||
`,
|
||||
"/workspace/opendesk/apps/xwiki/helmfile-child.yaml": `
|
||||
releases:
|
||||
- name: xwiki
|
||||
chart: stable/xwiki
|
||||
`,
|
||||
"/workspace/opendesk/apps/jitsi/helmfile-child.yaml": `
|
||||
releases:
|
||||
- name: jitsi
|
||||
chart: stable/jitsi
|
||||
`,
|
||||
}
|
||||
|
||||
app, testFs := newDirFilterTestApp(t, files, "/workspace/helmfile.yaml", []string{"dir=opendesk/apps/openproject"})
|
||||
|
||||
err := app.ForEachState(Noop, false, SetFilter(true))
|
||||
assert.NoError(t, err)
|
||||
|
||||
childReads := 0
|
||||
for _, r := range testFs.SuccessfulReads() {
|
||||
if strings.HasSuffix(r, "helmfile-child.yaml") {
|
||||
childReads++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, 1, childReads, "exactly one helmfile-child.yaml should be read (the matching one), got reads=%v", testFs.SuccessfulReads())
|
||||
}
|
||||
|
||||
// TestProcessNestedHelmfiles_DirFilterExcludesTopLevelReleases verifies that a
|
||||
// dir filter targeting a nested subtree excludes releases declared directly
|
||||
// in the root helmfile (whose dirLabel is ".") while still descending into the
|
||||
// matching sub-helmfile. The root file itself must still be read because its
|
||||
// `helmfiles:` directive is what points to the nested branch. Uses distinct
|
||||
// filenames per level so the read log is unambiguous.
|
||||
func TestProcessNestedHelmfiles_DirFilterExcludesTopLevelReleases(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/workspace/root.yaml": `
|
||||
releases:
|
||||
- name: root-release
|
||||
chart: stable/root
|
||||
helmfiles:
|
||||
- apps/openproject/leaf.yaml
|
||||
`,
|
||||
"/workspace/apps/openproject/leaf.yaml": `
|
||||
releases:
|
||||
- name: openproject-web
|
||||
chart: stable/openproject
|
||||
`,
|
||||
}
|
||||
|
||||
app, testFs := newDirFilterTestApp(t, files, "/workspace/root.yaml", []string{"dir=apps/openproject"})
|
||||
|
||||
err := app.ForEachState(Noop, false, SetFilter(true))
|
||||
assert.NoError(t, err)
|
||||
|
||||
reads := testFs.SuccessfulReads()
|
||||
readSet := map[string]bool{}
|
||||
for _, r := range reads {
|
||||
readSet[r] = true
|
||||
}
|
||||
assert.True(t, readSet["root.yaml"], "root helmfile must be parsed to learn its helmfiles: list, reads=%v", reads)
|
||||
assert.True(t, readSet["leaf.yaml"], "matching nested helmfile must be descended into, reads=%v", reads)
|
||||
}
|
||||
|
||||
// TestProcessNestedHelmfiles_DirDotIsRejected pins down the current behavior
|
||||
// that `dir=.` and equivalent forms (`dir=./`, `dir=apps/..`) are rejected at
|
||||
// selector parse time. Semantics for `.` were under discussion: matching
|
||||
// everything (no-op), matching only root-level releases (useful but uneven),
|
||||
// or rejecting outright. Outright rejection is the current decision; this
|
||||
// test makes any change to that policy a deliberate code change.
|
||||
func TestProcessNestedHelmfiles_DirDotIsRejected(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/workspace/root.yaml": `
|
||||
releases:
|
||||
- name: root-release
|
||||
chart: stable/root
|
||||
`,
|
||||
}
|
||||
for _, selector := range []string{"dir=.", "dir=./", "dir=apps/.."} {
|
||||
t.Run(selector, func(t *testing.T) {
|
||||
app, _ := newDirFilterTestApp(t, files, "/workspace/root.yaml", []string{selector})
|
||||
|
||||
err := app.ForEachState(Noop, false, SetFilter(true))
|
||||
assert.Error(t, err, "dir=. and equivalents should be rejected at parse time")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkipForDirFilter(t *testing.T) {
|
||||
const rootDir = "/workspace"
|
||||
const stateDir = "/workspace/opendesk"
|
||||
selectors := []string{"dir=opendesk/apps/openproject"}
|
||||
|
||||
testFs := testhelper.NewTestFs(map[string]string{
|
||||
"/workspace/opendesk/apps/openproject/helmfile.yaml": "",
|
||||
"/workspace/opendesk/apps/xwiki/helmfile.yaml": "",
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
entryPath string
|
||||
selectors []string
|
||||
rootDir string
|
||||
skip bool
|
||||
}{
|
||||
{name: "matching entry descends", entryPath: "apps/openproject/helmfile.yaml", selectors: selectors, rootDir: rootDir, skip: false},
|
||||
{name: "sibling entry is skipped", entryPath: "apps/xwiki/helmfile.yaml", selectors: selectors, rootDir: rootDir, skip: true},
|
||||
{name: "remote entry is never short-circuited", entryPath: "git::https://github.com/example/xwiki.git@helmfile.yaml?ref=main", selectors: selectors, rootDir: rootDir, skip: false},
|
||||
{name: "matching directory entry descends", entryPath: "apps/openproject", selectors: selectors, rootDir: rootDir, skip: false},
|
||||
{name: "sibling directory entry is skipped", entryPath: "apps/xwiki", selectors: selectors, rootDir: rootDir, skip: true},
|
||||
{name: "ancestor directory entry descends", entryPath: "apps", selectors: selectors, rootDir: rootDir, skip: false},
|
||||
{name: "absolute sibling entry is skipped", entryPath: "/workspace/opendesk/apps/xwiki/helmfile.yaml", selectors: selectors, rootDir: rootDir, skip: true},
|
||||
{name: "entry outside root descends", entryPath: "../../shared/helmfile.yaml", selectors: selectors, rootDir: rootDir, skip: false},
|
||||
{name: "no dir selector descends", entryPath: "apps/xwiki/helmfile.yaml", selectors: []string{"tier=frontend"}, rootDir: rootDir, skip: false},
|
||||
{name: "negative dir selector does not prune", entryPath: "apps/xwiki/helmfile.yaml", selectors: []string{"dir!=opendesk/apps/xwiki"}, rootDir: rootDir, skip: false},
|
||||
{name: "unresolvable root descends", entryPath: "apps/xwiki/helmfile.yaml", selectors: selectors, rootDir: "", skip: false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
app := injectFs(&App{rootHelmfileDir: tc.rootDir}, testFs)
|
||||
assert.Equal(t, tc.skip, app.skipForDirFilter(stateDir, tc.entryPath, tc.selectors))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessNestedHelmfiles_DirFilterSkipsSiblingDirectoryEntries(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/workspace/helmfile.yaml": `
|
||||
helmfiles:
|
||||
- apps/openproject
|
||||
- apps/xwiki
|
||||
`,
|
||||
"/workspace/apps/openproject/leaf.yaml": `
|
||||
releases:
|
||||
- name: openproject-web
|
||||
chart: stable/openproject
|
||||
`,
|
||||
"/workspace/apps/xwiki/leaf.yaml": `
|
||||
releases:
|
||||
- name: xwiki
|
||||
chart: stable/xwiki
|
||||
`,
|
||||
}
|
||||
|
||||
app, testFs := newDirFilterTestApp(t, files, "/workspace/helmfile.yaml", []string{"dir=apps/openproject"})
|
||||
for _, dir := range []string{"/workspace/apps/openproject", "/workspace/apps/xwiki"} {
|
||||
testFs.GlobFixtures[dir] = []string{dir}
|
||||
}
|
||||
|
||||
var selected []string
|
||||
err := app.ForEachState(func(run *Run) (bool, []error) {
|
||||
releases, err := run.state.GetSelectedReleases(false)
|
||||
if err != nil {
|
||||
return false, []error{err}
|
||||
}
|
||||
for _, r := range releases {
|
||||
selected = append(selected, r.Name)
|
||||
}
|
||||
return len(releases) > 0, nil
|
||||
}, false, SetFilter(true))
|
||||
require.NoError(t, err)
|
||||
|
||||
leafReads := 0
|
||||
for _, r := range testFs.SuccessfulReads() {
|
||||
if strings.HasSuffix(r, "leaf.yaml") {
|
||||
leafReads++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, 1, leafReads, "only the matching directory entry should be read, got reads=%v", testFs.SuccessfulReads())
|
||||
assert.Equal(t, []string{"openproject-web"}, selected)
|
||||
}
|
||||
+1
-2
@@ -257,8 +257,7 @@ func isDetectedChanges(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var appErr *Error
|
||||
if errors.As(err, &appErr) {
|
||||
if appErr, ok := errors.AsType[*Error](err); ok {
|
||||
if appErr.code != nil && *appErr.code == 2 {
|
||||
return true
|
||||
}
|
||||
|
||||
+2
-2
@@ -19,8 +19,8 @@ import (
|
||||
|
||||
const (
|
||||
HelmRequiredVersion = "v3.18.6" // Minimum required version (supports Helm 3.x and 4.x)
|
||||
HelmDiffRecommendedVersion = "v3.15.12"
|
||||
HelmRecommendedVersion = "v4.2.4" // Recommended Helm 4 version
|
||||
HelmDiffRecommendedVersion = "v3.15.13"
|
||||
HelmRecommendedVersion = "v4.3.1" // 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"
|
||||
|
||||
+9
-16
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -143,10 +144,8 @@ func createPluginYAML(t *testing.T, pluginsDir, pluginDirName, name, version str
|
||||
func newHelmPluginMockRunner(pluginErr error) *initMockRunner {
|
||||
return &initMockRunner{
|
||||
executeFunc: func(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
|
||||
for _, a := range args {
|
||||
if a == "--short" {
|
||||
return []byte("v3.18.6"), nil
|
||||
}
|
||||
if slices.Contains(args, "--short") {
|
||||
return []byte("v3.18.6"), nil
|
||||
}
|
||||
// Fail any "helm plugin ..." subcommand (install, update, etc.)
|
||||
if len(args) > 0 && args[0] == "plugin" {
|
||||
@@ -168,10 +167,8 @@ func TestCheckHelmPlugins_InstallErrorButPluginPresent(t *testing.T) {
|
||||
// plugin.yaml on disk and then returns an error.
|
||||
runner := &initMockRunner{
|
||||
executeFunc: func(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
|
||||
for _, a := range args {
|
||||
if a == "--short" {
|
||||
return []byte("v3.18.6"), nil
|
||||
}
|
||||
if slices.Contains(args, "--short") {
|
||||
return []byte("v3.18.6"), nil
|
||||
}
|
||||
if len(args) > 0 && args[0] == "plugin" && len(args) >= 3 && args[1] == "install" {
|
||||
// Find which plugin is being installed by matching the repo URL.
|
||||
@@ -228,10 +225,8 @@ func TestCheckHelmPlugins_UpdateUsesUninstallReinstall(t *testing.T) {
|
||||
// that writes the required version to disk.
|
||||
runner := &initMockRunner{
|
||||
executeFunc: func(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
|
||||
for _, a := range args {
|
||||
if a == "--short" {
|
||||
return []byte("v3.18.6"), nil
|
||||
}
|
||||
if slices.Contains(args, "--short") {
|
||||
return []byte("v3.18.6"), nil
|
||||
}
|
||||
if len(args) >= 2 && args[0] == "plugin" {
|
||||
switch args[1] {
|
||||
@@ -309,10 +304,8 @@ func TestCheckHelmPlugins_UpdateErrorButPluginAtRequiredVersion(t *testing.T) {
|
||||
// verifies the version and warns instead of returning an error.
|
||||
runner := &initMockRunner{
|
||||
executeFunc: func(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
|
||||
for _, a := range args {
|
||||
if a == "--short" {
|
||||
return []byte("v3.18.6"), nil
|
||||
}
|
||||
if slices.Contains(args, "--short") {
|
||||
return []byte("v3.18.6"), nil
|
||||
}
|
||||
if len(args) >= 2 && args[0] == "plugin" {
|
||||
switch args[1] {
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/helmfile/vals"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/exectest"
|
||||
ffs "github.com/helmfile/helmfile/pkg/filesystem"
|
||||
"github.com/helmfile/helmfile/pkg/helmexec"
|
||||
)
|
||||
|
||||
// issue2616HelmfileContent defines two releases: one that is templated fine
|
||||
// (logging) and one that always fails chart preparation (error), because OCI
|
||||
// charts do not support the "latest" version tag.
|
||||
const issue2616HelmfileContent = `
|
||||
releases:
|
||||
- name: logging
|
||||
chart: incubator/raw
|
||||
namespace: kube-system
|
||||
|
||||
- name: error
|
||||
chart: oci://example.com/chart/error
|
||||
namespace: failed
|
||||
version: latest
|
||||
`
|
||||
|
||||
// TestTemplateAllowFailedReleases verifies the behavior of the
|
||||
// --allow-failed-releases flag end to end for `helmfile template`:
|
||||
// when chart preparation fails for one release, the remaining releases are
|
||||
// still templated, the failed release is skipped (i.e. never executed against
|
||||
// its un-prepared chart reference) and all failures are reported at the end.
|
||||
func TestTemplateAllowFailedReleases(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
allowFailedRelease bool
|
||||
|
||||
// wantTemplated contains the releases that must have been passed to
|
||||
// `helm template`; the release failing chart preparation must never
|
||||
// appear here.
|
||||
wantTemplated []exectest.Release
|
||||
}{
|
||||
{
|
||||
name: "default: abort on chart preparation failure",
|
||||
allowFailedRelease: false,
|
||||
wantTemplated: nil,
|
||||
},
|
||||
{
|
||||
name: "allow-failed-releases: skip failed release, template the rest",
|
||||
allowFailedRelease: true,
|
||||
wantTemplated: []exectest.Release{{Name: "logging", Flags: []string{"--kube-context", "default", "--namespace", "kube-system"}}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var helm = &exectest.Helm{
|
||||
FailOnUnexpectedList: true,
|
||||
FailOnUnexpectedDiff: true,
|
||||
DiffMutex: &sync.Mutex{},
|
||||
ChartsMutex: &sync.Mutex{},
|
||||
ReleasesMutex: &sync.Mutex{},
|
||||
}
|
||||
|
||||
_ = runWithLogCapture(t, "debug", func(t *testing.T, logger *zap.SugaredLogger) {
|
||||
t.Helper()
|
||||
|
||||
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
||||
require.NoError(t, err)
|
||||
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": issue2616HelmfileContent,
|
||||
}
|
||||
|
||||
app := appWithFs(&App{
|
||||
OverrideHelmBinary: DefaultHelmBinary,
|
||||
fs: &ffs.FileSystem{Glob: filepath.Glob},
|
||||
OverrideKubeContext: "default",
|
||||
DisableKubeVersionAutoDetection: true,
|
||||
Env: "default",
|
||||
Logger: logger,
|
||||
helms: map[helmKey]helmexec.Interface{
|
||||
createHelmKey("helm", "default"): helm,
|
||||
},
|
||||
valsRuntime: valsRuntime,
|
||||
}, files)
|
||||
|
||||
tmplErr := app.Template(applyConfig{
|
||||
// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
|
||||
concurrency: 1,
|
||||
logger: logger,
|
||||
allowFailedReleases: tc.allowFailedRelease,
|
||||
})
|
||||
|
||||
// The chart preparation failure must be reported in both modes.
|
||||
require.Error(t, tmplErr)
|
||||
assert.Contains(t, tmplErr.Error(), "the version for OCI charts should be semver compliant")
|
||||
|
||||
// The release that failed chart preparation must never be executed.
|
||||
require.Equal(t, tc.wantTemplated, helm.Templated)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
+57
-11
@@ -1,6 +1,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
@@ -48,18 +49,23 @@ func (r *Run) askForConfirmation(msg string) bool {
|
||||
// When skipRepos is false, SyncReposOnce still runs normally for all repos.
|
||||
var commandsSkipChartPrep = []string{"write-values", "list"}
|
||||
|
||||
func (r *Run) prepareChartsIfNeeded(helmfileCommand string, dir string, concurrency int, opts state.ChartPrepareOptions) (map[state.PrepareChartKey]string, error) {
|
||||
func (r *Run) prepareChartsIfNeeded(helmfileCommand string, dir string, concurrency int, opts state.ChartPrepareOptions) (map[state.PrepareChartKey]string, map[state.PrepareChartKey]error, error) {
|
||||
// Skip chart preparation for commands that don't need chart pulls
|
||||
if slices.Contains(commandsSkipChartPrep, strings.ToLower(helmfileCommand)) {
|
||||
return nil, nil
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
releaseToChart, errs := r.state.PrepareCharts(r.helm, dir, concurrency, helmfileCommand, opts)
|
||||
releaseToChart, failedReleases, errs := r.state.PrepareCharts(r.helm, dir, concurrency, helmfileCommand, opts)
|
||||
if len(errs) > 0 {
|
||||
return nil, fmt.Errorf("%v", errs)
|
||||
if !opts.AllowFailedReleases {
|
||||
// abort on first error
|
||||
return nil, nil, fmt.Errorf("%v", errs)
|
||||
}
|
||||
// return partial results, along with the per-release errors for the failed ones
|
||||
return releaseToChart, failedReleases, &MultiError{Errors: errs}
|
||||
}
|
||||
|
||||
return releaseToChart, nil
|
||||
return releaseToChart, failedReleases, nil
|
||||
}
|
||||
|
||||
func (r *Run) WithPreparedCharts(helmfileCommand string, opts state.ChartPrepareOptions, f func() []error) error {
|
||||
@@ -113,11 +119,25 @@ func (r *Run) WithPreparedCharts(helmfileCommand string, opts state.ChartPrepare
|
||||
// remain available for the entire operation lifecycle. See issue #1799.
|
||||
defer r.state.CleanupChartifyTempDirs()
|
||||
|
||||
releaseToChart, err := r.prepareChartsIfNeeded(helmfileCommand, dir, opts.Concurrency, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
releaseToChart, failedReleases, prepareErr := r.prepareChartsIfNeeded(helmfileCommand, dir, opts.Concurrency, opts)
|
||||
// A prepare error with no per-release attribution (state parsing, dependency
|
||||
// resolution, ...) is a general failure: always abort, even with
|
||||
// opts.AllowFailedReleases, since there are no processable partial results.
|
||||
// Otherwise, abort only when partial results are not allowed; the failed
|
||||
// releases are skipped below and their errors are reported at the end.
|
||||
if prepareErr != nil && (len(failedReleases) == 0 || !opts.AllowFailedReleases) {
|
||||
return prepareErr
|
||||
}
|
||||
|
||||
// Releases whose chart preparation failed are removed from the state, so that
|
||||
// the operation below never executes them against their original, un-prepared
|
||||
// chart reference. That would either fail again with a duplicate error or,
|
||||
// worse, bypass chartify modifications (patches, dependencies) and produce an
|
||||
// unintended result. Their preparation errors are reported via prepareErr.
|
||||
releases := r.state.Releases
|
||||
if len(failedReleases) > 0 {
|
||||
releases = make([]state.ReleaseSpec, 0, len(r.state.Releases))
|
||||
}
|
||||
for i := range r.state.Releases {
|
||||
rel := &r.state.Releases[i]
|
||||
key := state.PrepareChartKey{
|
||||
@@ -125,7 +145,10 @@ func (r *Run) WithPreparedCharts(helmfileCommand string, opts state.ChartPrepare
|
||||
Namespace: rel.Namespace,
|
||||
KubeContext: rel.KubeContext,
|
||||
}
|
||||
if chart := releaseToChart[key]; chart != rel.Chart {
|
||||
if _, failed := failedReleases[key]; failed {
|
||||
continue
|
||||
}
|
||||
if chart, ok := releaseToChart[key]; ok && chart != rel.Chart {
|
||||
// The chart has been downloaded and modified by Helmfile (and chartify under the hood).
|
||||
// We let the later step use the modified version of the chart, located under the `chart` variable,
|
||||
// instead of the original chart path.
|
||||
@@ -133,7 +156,11 @@ func (r *Run) WithPreparedCharts(helmfileCommand string, opts state.ChartPrepare
|
||||
// if it has been modified or not.
|
||||
rel.ChartPath = chart
|
||||
}
|
||||
if len(failedReleases) > 0 {
|
||||
releases = append(releases, *rel)
|
||||
}
|
||||
}
|
||||
r.state.Releases = releases
|
||||
|
||||
r.ReleaseToChart = releaseToChart
|
||||
|
||||
@@ -146,8 +173,27 @@ func (r *Run) WithPreparedCharts(helmfileCommand string, opts state.ChartPrepare
|
||||
}
|
||||
}
|
||||
|
||||
_, err = r.state.TriggerGlobalCleanupEvent(helmfileCommand, firstErr)
|
||||
return err
|
||||
_, cleanupErr := r.state.TriggerGlobalCleanupEvent(helmfileCommand, firstErr)
|
||||
if !opts.AllowFailedReleases {
|
||||
return cleanupErr
|
||||
}
|
||||
|
||||
// merge the preparation and cleanup errors into a single error output
|
||||
var merged []error
|
||||
if prepareErr != nil {
|
||||
if me, ok := errors.AsType[*MultiError](prepareErr); ok {
|
||||
merged = append(merged, me.Errors...)
|
||||
} else {
|
||||
merged = append(merged, prepareErr)
|
||||
}
|
||||
}
|
||||
if cleanupErr != nil {
|
||||
merged = append(merged, fmt.Errorf("error during global cleanup event: %w", cleanupErr))
|
||||
}
|
||||
if len(merged) > 0 {
|
||||
return &MultiError{Errors: merged}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Run) Deps(c DepsConfigProvider) []error {
|
||||
|
||||
@@ -26,15 +26,13 @@ func runWithLogCapture(t *testing.T, logLevel string, f func(*testing.T, *zap.Su
|
||||
|
||||
logFlushed := &sync.WaitGroup{}
|
||||
// Ensure all the log is consumed into `bs` by calling `logWriter.Close()` followed by `logFlushed.Wait()`
|
||||
logFlushed.Add(1)
|
||||
go func() {
|
||||
logFlushed.Go(func() {
|
||||
scanner := bufio.NewScanner(logReader)
|
||||
for scanner.Scan() {
|
||||
bs.Write(scanner.Bytes())
|
||||
bs.WriteString("\n")
|
||||
}
|
||||
logFlushed.Done()
|
||||
}()
|
||||
})
|
||||
|
||||
defer func() {
|
||||
// This is here to avoid data-trace on bytes buffer `bs` to capture logs
|
||||
@@ -76,7 +74,7 @@ func assertEqualsToSnapshot(t *testing.T, name string, data string) {
|
||||
"Snapshot file %q does not exist. Rerun this test with `HELMFILE_UPDATE_SNAPSHOT=1 go test -v -run %s %s` to create the snapshot",
|
||||
snapshotFileName,
|
||||
t.Name(),
|
||||
reflect.TypeOf(thisPkgLocator{}).PkgPath(),
|
||||
reflect.TypeFor[thisPkgLocator]().PkgPath(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -87,7 +85,7 @@ func assertEqualsToSnapshot(t *testing.T, name string, data string) {
|
||||
t.Errorf(
|
||||
"If you think this is due to the snapshot file being outdated, rerun this test with `HELMFILE_UPDATE_SNAPSHOT=1 go test -v -run %s %s` to update the snapshot",
|
||||
t.Name(),
|
||||
reflect.TypeOf(thisPkgLocator{}).PkgPath(),
|
||||
reflect.TypeFor[thisPkgLocator]().PkgPath(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,12 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/environment"
|
||||
"github.com/helmfile/helmfile/pkg/state"
|
||||
"github.com/helmfile/helmfile/pkg/telemetry"
|
||||
"github.com/helmfile/helmfile/pkg/tmpl"
|
||||
)
|
||||
|
||||
@@ -33,6 +37,11 @@ func (r *desiredStateLoader) renderTemplatesToYamlWithEnv(baseDir, filename stri
|
||||
}
|
||||
|
||||
func (r *desiredStateLoader) twoPassRenderTemplateToYaml(inherited, overrode *environment.Environment, baseDir, filename string, content []byte) (*bytes.Buffer, error) {
|
||||
_, span := telemetry.Tracer(telemetry.ScopeHelmfile).Start(r.spanCtx(), "helmfile.render",
|
||||
trace.WithAttributes(attribute.String("helmfile.state_file", filename)),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
var phase string
|
||||
r.logger.Debugf("%srendering starting for \"%s\": inherited=%v, overrode=%v", phase, filename, inherited, overrode)
|
||||
|
||||
|
||||
+25
-1
@@ -3,6 +3,9 @@ package config
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ApplyOptoons is the options for the apply command
|
||||
@@ -93,8 +96,12 @@ type ApplyOptions struct {
|
||||
TrackTimeout int
|
||||
// TrackLogs enables log streaming with kubedog
|
||||
TrackLogs bool
|
||||
// TrackLogsUntilReady stops pod log streaming when each pod becomes ready. Requires TrackLogs.
|
||||
TrackLogsUntilReady bool
|
||||
// TrackFailedLogs streams logs only for pods that enter a failed state.
|
||||
TrackFailedLogs bool
|
||||
// TrackLogsInterval is the interval between kubedog log output updates.
|
||||
TrackLogsInterval time.Duration
|
||||
// HelmStuckGrace, when > 0, enables the helm-killer safety valve. See
|
||||
// ReleaseSpec.HelmStuckGrace for details. Value is in seconds.
|
||||
HelmStuckGrace int
|
||||
@@ -109,13 +116,14 @@ type ApplyOptions struct {
|
||||
|
||||
// NewApply creates a new Apply
|
||||
func NewApplyOptions() *ApplyOptions {
|
||||
return &ApplyOptions{}
|
||||
return &ApplyOptions{TrackLogsInterval: defaultTrackLogsInterval}
|
||||
}
|
||||
|
||||
// ApplyImpl is impl for applyOptions
|
||||
type ApplyImpl struct {
|
||||
*GlobalImpl
|
||||
*ApplyOptions
|
||||
Cmd *cobra.Command
|
||||
}
|
||||
|
||||
// NewApplyImpl creates a new ApplyImpl
|
||||
@@ -341,11 +349,24 @@ func (a *ApplyImpl) TrackLogs() bool {
|
||||
return a.ApplyOptions.TrackLogs
|
||||
}
|
||||
|
||||
// TrackLogsUntilReady returns the track-logs-until-ready flag.
|
||||
func (a *ApplyImpl) TrackLogsUntilReady() bool {
|
||||
return a.ApplyOptions.TrackLogsUntilReady
|
||||
}
|
||||
|
||||
// TrackFailedLogs returns the track-failed-logs flag.
|
||||
func (a *ApplyImpl) TrackFailedLogs() bool {
|
||||
return a.ApplyOptions.TrackFailedLogs
|
||||
}
|
||||
|
||||
// TrackLogsInterval returns the CLI interval, or zero when the flag was not supplied.
|
||||
func (a *ApplyImpl) TrackLogsInterval() time.Duration {
|
||||
if a.Cmd != nil && !a.Cmd.Flags().Changed("track-logs-interval") {
|
||||
return 0
|
||||
}
|
||||
return a.ApplyOptions.TrackLogsInterval
|
||||
}
|
||||
|
||||
// HelmStuckGrace returns the helm-stuck-grace value (seconds, 0 = disabled).
|
||||
func (a *ApplyImpl) HelmStuckGrace() int {
|
||||
return a.ApplyOptions.HelmStuckGrace
|
||||
@@ -371,5 +392,8 @@ func (a *ApplyImpl) ValidateConfig() error {
|
||||
if a.ApplyOptions.TrackMode != "" && !slices.Contains(validTrackModes, a.ApplyOptions.TrackMode) {
|
||||
return fmt.Errorf("--track-mode must be 'helm', 'helm-legacy', or 'kubedog', got: %s", a.ApplyOptions.TrackMode)
|
||||
}
|
||||
if a.ApplyOptions.TrackLogsInterval < time.Second {
|
||||
return fmt.Errorf("--track-logs-interval must be at least 1s, got: %s", a.ApplyOptions.TrackLogsInterval)
|
||||
}
|
||||
return a.GlobalImpl.ValidateConfig()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestApplyImpl_TrackLogsInterval(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setFlag string // explicit --track-logs-interval value; empty means not supplied
|
||||
noCmd bool // construct ApplyImpl without a Cmd, like non-cobra callers
|
||||
want time.Duration
|
||||
}{
|
||||
{
|
||||
name: "zero when flag not supplied",
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "explicit value returned when flag supplied",
|
||||
setFlag: "3s",
|
||||
want: 3 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "explicit default value still counts as supplied",
|
||||
setFlag: "10s",
|
||||
want: 10 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "nil cmd falls back to option value",
|
||||
noCmd: true,
|
||||
want: defaultTrackLogsInterval,
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
tt := tests[i]
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
options := NewApplyOptions()
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().DurationVar(&options.TrackLogsInterval, "track-logs-interval", options.TrackLogsInterval, "")
|
||||
if tt.setFlag != "" {
|
||||
require.NoError(t, cmd.Flags().Set("track-logs-interval", tt.setFlag))
|
||||
}
|
||||
|
||||
impl := NewApplyImpl(nil, options)
|
||||
if !tt.noCmd {
|
||||
impl.Cmd = cmd
|
||||
}
|
||||
require.Equal(t, tt.want, impl.TrackLogsInterval())
|
||||
})
|
||||
}
|
||||
}
|
||||
+126
-29
@@ -1,44 +1,141 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/maputil"
|
||||
)
|
||||
|
||||
// stateValuesSetStringRE matches a single key=value assignment within one
|
||||
// --state-values-set-string flag value. The value is either a quoted string,
|
||||
// which may contain commas, or a run of plain or backslash-escaped characters
|
||||
// ending at the next unescaped comma.
|
||||
var stateValuesSetStringRE = regexp.MustCompile(`(?s)(?:,|^)([^\s=]+)=(['"][^'"]*['"]|(?:\\.|[^,\\])*\\?)`)
|
||||
|
||||
// NewCLIConfigImpl parses the raw --state-values-set-string and
|
||||
// --state-values-set flag values into state value overrides on g.
|
||||
func NewCLIConfigImpl(g *GlobalImpl) error {
|
||||
re := regexp.MustCompile(`(?:,|^)([^\s=]+)=(['"][^'"]*['"]|[^,]+)`)
|
||||
if err := parseStateValuesSetString(g); err != nil {
|
||||
return err
|
||||
}
|
||||
return parseStateValuesSet(g)
|
||||
}
|
||||
|
||||
// parseStateValuesSetString applies the --state-values-set-string flag values
|
||||
// to g. Unlike --state-values-set, values are kept as strings without type
|
||||
// conversion.
|
||||
func parseStateValuesSetString(g *GlobalImpl) error {
|
||||
optsSet := g.RawStateValuesSetString()
|
||||
if len(optsSet) > 0 {
|
||||
set := map[string]any{}
|
||||
for i := range optsSet {
|
||||
ops := re.FindAllStringSubmatch(optsSet[i], -1)
|
||||
for j := range ops {
|
||||
op := ops[j]
|
||||
k := maputil.ParseKey(op[1])
|
||||
v := op[2]
|
||||
|
||||
maputil.Set(set, k, v, true)
|
||||
}
|
||||
}
|
||||
g.SetSet(set)
|
||||
}
|
||||
optsSet = g.RawStateValuesSet()
|
||||
if len(optsSet) > 0 {
|
||||
set := map[string]any{}
|
||||
for i := range optsSet {
|
||||
ops := strings.Split(optsSet[i], ",")
|
||||
for j := range ops {
|
||||
op := strings.SplitN(ops[j], "=", 2)
|
||||
k := maputil.ParseKey(op[0])
|
||||
v := op[1]
|
||||
|
||||
maputil.Set(set, k, v, false)
|
||||
}
|
||||
}
|
||||
g.SetSet(set)
|
||||
if len(optsSet) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
set := map[string]any{}
|
||||
for _, raw := range optsSet {
|
||||
for _, match := range stateValuesSetStringRE.FindAllStringSubmatch(raw, -1) {
|
||||
key, value := match[1], match[2]
|
||||
maputil.Set(set, maputil.ParseKey(key), unescapeCommas(value), true)
|
||||
}
|
||||
}
|
||||
g.SetSet(set)
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseStateValuesSet applies the --state-values-set flag values to g. Values
|
||||
// are type-converted like Helm's --set.
|
||||
func parseStateValuesSet(g *GlobalImpl) error {
|
||||
optsSet := g.RawStateValuesSet()
|
||||
if len(optsSet) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
set := map[string]any{}
|
||||
for _, raw := range optsSet {
|
||||
for _, assignment := range splitOnUnescapedCommas(raw) {
|
||||
if err := setAssignment(set, assignment); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
g.SetSet(set)
|
||||
return nil
|
||||
}
|
||||
|
||||
// setAssignment parses a single key=value assignment into set, returning an
|
||||
// error instead of panicking when the assignment is malformed.
|
||||
func setAssignment(set map[string]any, assignment string) error {
|
||||
key, value, found := strings.Cut(unescapeCommas(assignment), "=")
|
||||
if !found || key == "" {
|
||||
return fmt.Errorf("--state-values-set: invalid assignment %q: expected <key>=<value>", assignment)
|
||||
}
|
||||
|
||||
maputil.Set(set, maputil.ParseKey(key), value, false)
|
||||
return nil
|
||||
}
|
||||
|
||||
// splitOnUnescapedCommas splits the input on commas that are not escaped by a
|
||||
// preceding backslash, keeping every backslash intact so that escapes remain
|
||||
// available to key and value parsing.
|
||||
func splitOnUnescapedCommas(input string) []string {
|
||||
segments := make([]string, 0, 1+strings.Count(input, ","))
|
||||
var current strings.Builder
|
||||
escaped := false
|
||||
|
||||
for i := range len(input) {
|
||||
char := input[i]
|
||||
|
||||
switch {
|
||||
case escaped:
|
||||
current.WriteByte('\\')
|
||||
current.WriteByte(char)
|
||||
escaped = false
|
||||
case char == '\\':
|
||||
escaped = true
|
||||
case char == ',':
|
||||
segments = append(segments, current.String())
|
||||
current.Reset()
|
||||
default:
|
||||
current.WriteByte(char)
|
||||
}
|
||||
}
|
||||
if escaped {
|
||||
current.WriteByte('\\')
|
||||
}
|
||||
return append(segments, current.String())
|
||||
}
|
||||
|
||||
// unescapeCommas turns every backslash-escaped comma into a literal comma,
|
||||
// preserving all other backslashes, including a trailing one.
|
||||
func unescapeCommas(input string) string {
|
||||
if !strings.Contains(input, `\,`) {
|
||||
return input
|
||||
}
|
||||
|
||||
var out strings.Builder
|
||||
out.Grow(len(input))
|
||||
escaped := false
|
||||
|
||||
for i := range len(input) {
|
||||
char := input[i]
|
||||
|
||||
switch {
|
||||
case escaped && char == ',':
|
||||
out.WriteByte(',')
|
||||
escaped = false
|
||||
case escaped:
|
||||
out.WriteByte('\\')
|
||||
out.WriteByte(char)
|
||||
escaped = false
|
||||
case char == '\\':
|
||||
escaped = true
|
||||
default:
|
||||
out.WriteByte(char)
|
||||
}
|
||||
}
|
||||
if escaped {
|
||||
out.WriteByte('\\')
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewCLIConfigImplStateValuesSet(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
set []string
|
||||
want map[string]any
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "comma separated assignments retain their types",
|
||||
set: []string{"count=2,enabled=true,disabled=false,empty=null,zero=0,code=001"},
|
||||
want: map[string]any{
|
||||
"count": int64(2), "enabled": true, "disabled": false,
|
||||
"empty": nil, "zero": int64(0), "code": "001",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "escaped commas within multiple values",
|
||||
set: []string{`a=1,b=2\,3,c=9\,10\,11`},
|
||||
want: map[string]any{"a": int64(1), "b": "2,3", "c": "9,10,11"},
|
||||
},
|
||||
{
|
||||
name: "escaped commas at the start and end of a value",
|
||||
set: []string{`message=\,hello\,`},
|
||||
want: map[string]any{"message": ",hello,"},
|
||||
},
|
||||
{
|
||||
name: "repeated flags overwrite earlier values",
|
||||
set: []string{`message=first\,second`, `message=third\,fourth,count=2`},
|
||||
want: map[string]any{"message": "third,fourth", "count": int64(2)},
|
||||
},
|
||||
{
|
||||
name: "equals signs remain within values",
|
||||
set: []string{`message=first\,second=value,count=2`},
|
||||
want: map[string]any{"message": "first,second=value", "count": int64(2)},
|
||||
},
|
||||
{
|
||||
name: "unicode values",
|
||||
set: []string{`message=你好\,世界`},
|
||||
want: map[string]any{"message": "你好,世界"},
|
||||
},
|
||||
{
|
||||
name: "empty value",
|
||||
set: []string{"message=,count=2"},
|
||||
want: map[string]any{"message": "", "count": int64(2)},
|
||||
},
|
||||
{
|
||||
name: "backslashes before other characters are preserved",
|
||||
set: []string{`path=C:\tmp\config`},
|
||||
want: map[string]any{"path": `C:\tmp\config`},
|
||||
},
|
||||
{
|
||||
name: "trailing backslash is preserved",
|
||||
set: []string{`path=C:\tmp\`},
|
||||
want: map[string]any{"path": `C:\tmp\`},
|
||||
},
|
||||
{
|
||||
name: "escaped dots in keys are preserved for key parsing",
|
||||
set: []string{`annotations.example\.com/key=first\,second`},
|
||||
want: map[string]any{
|
||||
"annotations": map[string]any{"example.com/key": "first,second"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested array keys",
|
||||
set: []string{`servers[0].host=east\,west,enabled=true`},
|
||||
want: map[string]any{
|
||||
"servers": []any{map[string]any{"host": "east,west"}},
|
||||
"enabled": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "even backslashes leave comma as an assignment separator",
|
||||
set: []string{`path=C:\\,count=2`},
|
||||
want: map[string]any{"path": `C:\\`, "count": int64(2)},
|
||||
},
|
||||
{
|
||||
name: "odd backslashes escape the comma",
|
||||
set: []string{`message=first\\\,second`},
|
||||
want: map[string]any{"message": `first\\,second`},
|
||||
},
|
||||
{
|
||||
name: "assignment without equals sign is rejected",
|
||||
set: []string{"message"},
|
||||
wantErr: `invalid assignment "message": expected <key>=<value>`,
|
||||
},
|
||||
{
|
||||
name: "trailing comma is rejected",
|
||||
set: []string{"message=hello,"},
|
||||
wantErr: `invalid assignment "": expected <key>=<value>`,
|
||||
},
|
||||
{
|
||||
name: "empty key is rejected",
|
||||
set: []string{"=hello"},
|
||||
wantErr: `invalid assignment "=hello": expected <key>=<value>`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewGlobalImpl(&GlobalOptions{StateValuesSet: tt.set})
|
||||
err := NewCLIConfigImpl(g)
|
||||
if tt.wantErr != "" {
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, g.StateValuesSet())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCLIConfigImplStateValuesSetString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
set []string
|
||||
want map[string]any
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "values are kept as strings without type conversion",
|
||||
set: []string{"count=2,enabled=true"},
|
||||
want: map[string]any{"count": "2", "enabled": "true"},
|
||||
},
|
||||
{
|
||||
name: "quoted values may contain commas",
|
||||
set: []string{`zone="zone1,zone2",imageTag=1.23.3`},
|
||||
want: map[string]any{"zone": `"zone1,zone2"`, "imageTag": "1.23.3"},
|
||||
},
|
||||
{
|
||||
name: "escaped commas within values",
|
||||
set: []string{`zone=zone1\,zone2,imageTag=1.23.3`},
|
||||
want: map[string]any{"zone": "zone1,zone2", "imageTag": "1.23.3"},
|
||||
},
|
||||
{
|
||||
name: "backslashes and trailing backslash are preserved",
|
||||
set: []string{`path=C:\tmp\config,trailing=C:\tmp\`},
|
||||
want: map[string]any{"path": `C:\tmp\config`, "trailing": `C:\tmp\`},
|
||||
},
|
||||
{
|
||||
name: "even backslashes leave comma as an assignment separator",
|
||||
set: []string{`path=C:\\,count=2`},
|
||||
want: map[string]any{"path": `C:\\`, "count": "2"},
|
||||
},
|
||||
{
|
||||
name: "empty value",
|
||||
set: []string{"message=,count=2"},
|
||||
want: map[string]any{"message": "", "count": "2"},
|
||||
},
|
||||
{
|
||||
name: "repeated flags overwrite earlier values",
|
||||
set: []string{`zone=a\,b`, `zone=c\,d`},
|
||||
want: map[string]any{"zone": "c,d"},
|
||||
},
|
||||
{
|
||||
name: "escaped dots in keys are preserved for key parsing",
|
||||
set: []string{`annotations.example\.com/key=first\,second`},
|
||||
want: map[string]any{
|
||||
"annotations": map[string]any{"example.com/key": "first,second"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewGlobalImpl(&GlobalOptions{StateValuesSetString: tt.set})
|
||||
err := NewCLIConfigImpl(g)
|
||||
if tt.wantErr != "" {
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, g.StateValuesSet())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCLIConfigImplBothSetFlags(t *testing.T) {
|
||||
g := NewGlobalImpl(&GlobalOptions{
|
||||
StateValuesSet: []string{"count=2"},
|
||||
StateValuesSetString: []string{"imageTag=1.23.3"},
|
||||
})
|
||||
require.NoError(t, NewCLIConfigImpl(g))
|
||||
|
||||
want := map[string]any{
|
||||
"count": int64(2),
|
||||
"imageTag": "1.23.3",
|
||||
}
|
||||
assert.Equal(t, want, g.StateValuesSet())
|
||||
}
|
||||
|
||||
func TestSplitOnUnescapedCommas(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want []string
|
||||
}{
|
||||
{name: "no commas", input: "a=1", want: []string{"a=1"}},
|
||||
{name: "plain separators", input: "a=1,b=2", want: []string{"a=1", "b=2"}},
|
||||
{name: "escaped separator", input: `a=1\,2,b=3`, want: []string{`a=1\,2`, "b=3"}},
|
||||
{name: "escaped separator only", input: `a=1\,2`, want: []string{`a=1\,2`}},
|
||||
{name: "even backslashes unescape the separator", input: `a=1\\,b=2`, want: []string{`a=1\\`, "b=2"}},
|
||||
{name: "trailing separator", input: "a=1,", want: []string{"a=1", ""}},
|
||||
{name: "trailing backslash", input: `a=1\`, want: []string{`a=1\`}},
|
||||
{name: "unicode", input: `a=你好\,世界,b=2`, want: []string{`a=你好\,世界`, "b=2"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, splitOnUnescapedCommas(tt.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnescapeCommas(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{name: "no escapes", input: "a=1,b=2", want: "a=1,b=2"},
|
||||
{name: "escaped comma", input: `a=1\,2`, want: "a=1,2"},
|
||||
{name: "escaped dot preserved", input: `a\.b=1`, want: `a\.b=1`},
|
||||
{name: "even backslashes preserved", input: `a=1\\`, want: `a=1\\`},
|
||||
{name: "trailing backslash preserved", input: `a=1\`, want: `a=1\`},
|
||||
{name: "unicode preserved", input: `a=你好\,世界`, want: "a=你好,世界"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, unescapeCommas(tt.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,8 @@ type GlobalOptions struct {
|
||||
SkipDeps bool
|
||||
// SkipRefresh is true if the running "helm repo update" should be skipped
|
||||
SkipRefresh bool
|
||||
// AllowFailedReleases is true if partial errors during release processing are allowed
|
||||
AllowFailedReleases bool
|
||||
// StripArgsValuesOnExitError is true if the ARGS output on exit error should be suppressed
|
||||
StripArgsValuesOnExitError bool
|
||||
// DisableForceUpdate is true if force updating repos is not desirable when executing "helm repo add" (Helm 3)
|
||||
@@ -82,6 +84,8 @@ type GlobalOptions struct {
|
||||
LogOutput io.Writer
|
||||
// SequentialHelmfiles is true if helmfile.d files should be processed sequentially instead of in parallel.
|
||||
SequentialHelmfiles bool
|
||||
// OtelTracing is true if OpenTelemetry tracing should be enabled for this run.
|
||||
OtelTracing bool
|
||||
}
|
||||
|
||||
// Logger returns the logger to use.
|
||||
@@ -264,6 +268,11 @@ func (g *GlobalImpl) SkipRefresh() bool {
|
||||
return g.GlobalOptions.SkipRefresh
|
||||
}
|
||||
|
||||
// AllowFailedReleases returns true if partial errors during release processing are allowed
|
||||
func (g *GlobalImpl) AllowFailedReleases() bool {
|
||||
return g.GlobalOptions.AllowFailedReleases
|
||||
}
|
||||
|
||||
// StripArgsValuesOnExitError return if the ARGS output on exit error should be suppressed
|
||||
func (g *GlobalImpl) StripArgsValuesOnExitError() bool {
|
||||
return g.GlobalOptions.StripArgsValuesOnExitError
|
||||
@@ -376,6 +385,15 @@ func (g *GlobalImpl) Interactive() bool {
|
||||
return os.Getenv(envvar.Interactive) == "true"
|
||||
}
|
||||
|
||||
// OtelTracing returns true if OpenTelemetry tracing is enabled via the
|
||||
// --otel-tracing flag or the HELMFILE_OTEL_TRACING environment variable.
|
||||
func (g *GlobalImpl) OtelTracing() bool {
|
||||
if g.GlobalOptions.OtelTracing {
|
||||
return true
|
||||
}
|
||||
return os.Getenv(envvar.OtelTracing) == "true"
|
||||
}
|
||||
|
||||
// Args returns the args to use for helm
|
||||
func (g *GlobalImpl) Args() string {
|
||||
args := g.GlobalOptions.Args
|
||||
|
||||
+30
-1
@@ -3,8 +3,16 @@ package config
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// defaultTrackLogsInterval is the default --track-logs-interval shared by
|
||||
// the sync and apply commands. It mirrors kubedog's defaultLogsInterval;
|
||||
// keep the two in sync.
|
||||
const defaultTrackLogsInterval = 10 * time.Second
|
||||
|
||||
// SyncOptions is the options for the build command
|
||||
type SyncOptions struct {
|
||||
// Set is the set flag
|
||||
@@ -61,8 +69,12 @@ type SyncOptions struct {
|
||||
TrackTimeout int
|
||||
// TrackLogs enables log streaming with kubedog
|
||||
TrackLogs bool
|
||||
// TrackLogsUntilReady stops pod log streaming when each pod becomes ready. Requires TrackLogs.
|
||||
TrackLogsUntilReady bool
|
||||
// TrackFailedLogs streams logs only for pods that enter a failed state.
|
||||
TrackFailedLogs bool
|
||||
// TrackLogsInterval is the interval between kubedog log output updates.
|
||||
TrackLogsInterval time.Duration
|
||||
// HelmStuckGrace, when > 0, enables the helm-killer safety valve. See
|
||||
// ReleaseSpec.HelmStuckGrace for details. Value is in seconds.
|
||||
HelmStuckGrace int
|
||||
@@ -93,13 +105,14 @@ type SyncOptions struct {
|
||||
|
||||
// NewSyncOptions creates a new Apply
|
||||
func NewSyncOptions() *SyncOptions {
|
||||
return &SyncOptions{}
|
||||
return &SyncOptions{TrackLogsInterval: defaultTrackLogsInterval}
|
||||
}
|
||||
|
||||
// SyncImpl is impl for applyOptions
|
||||
type SyncImpl struct {
|
||||
*GlobalImpl
|
||||
*SyncOptions
|
||||
Cmd *cobra.Command
|
||||
}
|
||||
|
||||
// NewSyncImpl creates a new SyncImpl
|
||||
@@ -249,11 +262,24 @@ func (t *SyncImpl) TrackLogs() bool {
|
||||
return t.SyncOptions.TrackLogs
|
||||
}
|
||||
|
||||
// TrackLogsUntilReady returns the track-logs-until-ready flag.
|
||||
func (t *SyncImpl) TrackLogsUntilReady() bool {
|
||||
return t.SyncOptions.TrackLogsUntilReady
|
||||
}
|
||||
|
||||
// TrackFailedLogs returns the track-failed-logs flag.
|
||||
func (t *SyncImpl) TrackFailedLogs() bool {
|
||||
return t.SyncOptions.TrackFailedLogs
|
||||
}
|
||||
|
||||
// TrackLogsInterval returns the CLI interval, or zero when the flag was not supplied.
|
||||
func (t *SyncImpl) TrackLogsInterval() time.Duration {
|
||||
if t.Cmd != nil && !t.Cmd.Flags().Changed("track-logs-interval") {
|
||||
return 0
|
||||
}
|
||||
return t.SyncOptions.TrackLogsInterval
|
||||
}
|
||||
|
||||
// HelmStuckGrace returns the helm-stuck-grace value (seconds, 0 = disabled).
|
||||
func (t *SyncImpl) HelmStuckGrace() int {
|
||||
return t.SyncOptions.HelmStuckGrace
|
||||
@@ -349,5 +375,8 @@ func (t *SyncImpl) ValidateConfig() error {
|
||||
if t.SyncOptions.TrackMode != "" && !slices.Contains(validTrackModes, t.SyncOptions.TrackMode) {
|
||||
return fmt.Errorf("--track-mode must be 'helm', 'helm-legacy', or 'kubedog', got: %s", t.SyncOptions.TrackMode)
|
||||
}
|
||||
if t.SyncOptions.TrackLogsInterval < time.Second {
|
||||
return fmt.Errorf("--track-logs-interval must be at least 1s, got: %s", t.SyncOptions.TrackLogsInterval)
|
||||
}
|
||||
return t.GlobalImpl.ValidateConfig()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSyncImpl_TrackLogsInterval(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setFlag string // explicit --track-logs-interval value; empty means not supplied
|
||||
noCmd bool // construct SyncImpl without a Cmd, like non-cobra callers
|
||||
want time.Duration
|
||||
}{
|
||||
{
|
||||
name: "zero when flag not supplied",
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "explicit value returned when flag supplied",
|
||||
setFlag: "3s",
|
||||
want: 3 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "explicit default value still counts as supplied",
|
||||
setFlag: "10s",
|
||||
want: 10 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "nil cmd falls back to option value",
|
||||
noCmd: true,
|
||||
want: defaultTrackLogsInterval,
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
tt := tests[i]
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
options := NewSyncOptions()
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().DurationVar(&options.TrackLogsInterval, "track-logs-interval", options.TrackLogsInterval, "")
|
||||
if tt.setFlag != "" {
|
||||
require.NoError(t, cmd.Flags().Set("track-logs-interval", tt.setFlag))
|
||||
}
|
||||
|
||||
impl := NewSyncImpl(nil, options)
|
||||
if !tt.noCmd {
|
||||
impl.Cmd = cmd
|
||||
}
|
||||
require.Equal(t, tt.want, impl.TrackLogsInterval())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTrackLogsIntervalDefault(t *testing.T) {
|
||||
require.Equal(t, 10*time.Second, NewApplyOptions().TrackLogsInterval)
|
||||
require.Equal(t, 10*time.Second, NewSyncOptions().TrackLogsInterval)
|
||||
}
|
||||
|
||||
func TestTrackLogsIntervalValidation(t *testing.T) {
|
||||
commands := []struct {
|
||||
name string
|
||||
validate func(time.Duration) error
|
||||
}{
|
||||
{
|
||||
name: "apply",
|
||||
validate: func(interval time.Duration) error {
|
||||
options := NewApplyOptions()
|
||||
options.TrackLogsInterval = interval
|
||||
return NewApplyImpl(NewGlobalImpl(&GlobalOptions{}), options).ValidateConfig()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sync",
|
||||
validate: func(interval time.Duration) error {
|
||||
options := NewSyncOptions()
|
||||
options.TrackLogsInterval = interval
|
||||
return NewSyncImpl(NewGlobalImpl(&GlobalOptions{}), options).ValidateConfig()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, command := range commands {
|
||||
t.Run(command.name, func(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
interval time.Duration
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "zero", interval: 0, wantErr: true},
|
||||
{name: "negative", interval: -time.Second, wantErr: true},
|
||||
{name: "below minimum", interval: 500 * time.Millisecond, wantErr: true},
|
||||
{name: "minimum", interval: time.Second},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := command.validate(tt.interval)
|
||||
if tt.wantErr {
|
||||
require.ErrorContains(t, err, "--track-logs-interval must be at least 1s")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,10 @@ const (
|
||||
DisableInsecureFeatures = "HELMFILE_DISABLE_INSECURE_FEATURES"
|
||||
DisableInsecureTemplateFunctions = "HELMFILE_DISABLE_INSECURE_TEMPLATE_FUNCTIONS"
|
||||
DisableHooks = "HELMFILE_DISABLE_HOOKS"
|
||||
// DisableVals passes `ref+` values through unchanged for external vals processing
|
||||
DisableVals = "HELMFILE_DISABLE_VALS"
|
||||
// DisableValsStrict errors when any `ref+` value is detected
|
||||
DisableValsStrict = "HELMFILE_DISABLE_VALS_STRICT"
|
||||
|
||||
// use helm status to check if a release exists before installing it
|
||||
UseHelmStatusToCheckReleaseExistence = "HELMFILE_USE_HELM_STATUS_TO_CHECK_RELEASE_EXISTENCE"
|
||||
@@ -26,6 +30,8 @@ const (
|
||||
CacheHome = "HELMFILE_CACHE_HOME"
|
||||
Interactive = "HELMFILE_INTERACTIVE"
|
||||
RepoRetry = "HELMFILE_REPO_RETRIES"
|
||||
OtelTracing = "HELMFILE_OTEL_TRACING"
|
||||
OtelMetricsPerRelease = "HELMFILE_OTEL_METRICS_PER_RELEASE"
|
||||
RenderYaml = "HELMFILE_RENDER_YAML" // force helmfile.yaml to be rendered as template regardless of extension, expecting "true" lower case
|
||||
|
||||
// AWSSDKLogLevel controls AWS SDK logging level
|
||||
|
||||
+149
-71
@@ -3,16 +3,21 @@ package event
|
||||
import (
|
||||
goContext "context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/environment"
|
||||
"github.com/helmfile/helmfile/pkg/envvar"
|
||||
"github.com/helmfile/helmfile/pkg/filesystem"
|
||||
"github.com/helmfile/helmfile/pkg/helmexec"
|
||||
"github.com/helmfile/helmfile/pkg/telemetry"
|
||||
"github.com/helmfile/helmfile/pkg/tmpl"
|
||||
)
|
||||
|
||||
@@ -34,6 +39,12 @@ type Bus struct {
|
||||
Runner helmexec.Runner
|
||||
Hooks []Hook
|
||||
|
||||
// Ctx, when set, is used by the lazily-constructed default Runner so that
|
||||
// hook subprocesses join the current trace. It should carry trace context
|
||||
// without cancellation (see the WithoutCancel bridge in pkg/state); nil
|
||||
// falls back to context.TODO(), the historical behavior.
|
||||
Ctx goContext.Context
|
||||
|
||||
BasePath string
|
||||
StateFilePath string
|
||||
Namespace string
|
||||
@@ -59,12 +70,16 @@ func (bus *Bus) Trigger(evt string, evtErr error, context map[string]any) (bool,
|
||||
}
|
||||
|
||||
if bus.Runner == nil {
|
||||
ctx := bus.Ctx
|
||||
if ctx == nil {
|
||||
// It would be better to pass app.Ctx here, but it requires a lot of work.
|
||||
// It seems that this code only for running hooks, which took not to long time as helm.
|
||||
ctx = goContext.TODO()
|
||||
}
|
||||
bus.Runner = helmexec.ShellRunner{
|
||||
Dir: bus.BasePath,
|
||||
Logger: bus.Logger,
|
||||
// It would be better to pass app.Ctx here, but it requires a lot of work.
|
||||
// It seems that this code only for running hooks, which took not to long time as helm.
|
||||
Ctx: goContext.TODO(),
|
||||
Ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,77 +94,140 @@ func (bus *Bus) Trigger(evt string, evtErr error, context map[string]any) (bool,
|
||||
continue
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
name := hook.Name
|
||||
if name == "" {
|
||||
if hook.Kubectl != nil {
|
||||
name = "kubectlApply"
|
||||
} else {
|
||||
name = hook.Command
|
||||
}
|
||||
}
|
||||
|
||||
if hook.Kubectl != nil {
|
||||
if hook.Command != "" {
|
||||
bus.Logger.Warnf("warn: ignoring command '%s' given within a kubectlApply hook", hook.Command)
|
||||
}
|
||||
hook.Command = "kubectl"
|
||||
if val, found := hook.Kubectl["filename"]; found {
|
||||
if _, found := hook.Kubectl["kustomize"]; found {
|
||||
return false, fmt.Errorf("hook[%s]: kustomize & filename cannot be used together", name)
|
||||
}
|
||||
hook.Args = append([]string{"apply", "-f"}, val)
|
||||
} else if val, found := hook.Kubectl["kustomize"]; found {
|
||||
hook.Args = append([]string{"apply", "-k"}, val)
|
||||
} else {
|
||||
return false, fmt.Errorf("hook[%s]: either kustomize or filename must be given", name)
|
||||
}
|
||||
}
|
||||
|
||||
bus.Logger.Debugf("hook[%s]: stateFilePath=%s, basePath=%s\n", name, bus.StateFilePath, bus.BasePath)
|
||||
|
||||
data := map[string]any{
|
||||
"Environment": bus.Env,
|
||||
"Namespace": bus.Namespace,
|
||||
"Event": event{
|
||||
Name: evt,
|
||||
Error: evtErr,
|
||||
},
|
||||
}
|
||||
for k, v := range context {
|
||||
data[k] = v
|
||||
}
|
||||
render := tmpl.NewTextRenderer(bus.Fs, bus.BasePath, data)
|
||||
|
||||
bus.Logger.Debugf("hook[%s]: triggered by event \"%s\"\n", name, evt)
|
||||
|
||||
command, err := render.RenderTemplateText(hook.Command)
|
||||
hookExecuted, err := bus.runHook(hook, evt, evtErr, context)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("hook[%s]: %v", name, err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
args := make([]string, len(hook.Args))
|
||||
for i, raw := range hook.Args {
|
||||
args[i], err = render.RenderTemplateText(raw)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("hook[%s]: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
bytes, err := bus.Runner.Execute(command, args, map[string]string{}, false)
|
||||
bus.Logger.Debugf("hook[%s]: %s\n", name, string(bytes))
|
||||
if hook.ShowLogs {
|
||||
prefix := fmt.Sprintf("\nhook[%s] logs | ", evt)
|
||||
bus.Logger.Infow(prefix + strings.ReplaceAll(string(bytes), "\n", prefix))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("hook[%s]: command `%s` failed: %v", name, command, err)
|
||||
}
|
||||
|
||||
executed = true
|
||||
executed = executed || hookExecuted
|
||||
}
|
||||
|
||||
return executed, nil
|
||||
}
|
||||
|
||||
// runHook renders and executes a single hook; the returned bool reports
|
||||
// whether the hook ran. The whole hook execution is wrapped in one
|
||||
// helmfile.hook span (a no-op when telemetry is disabled), and the hook's
|
||||
// subprocess span nests under it via hookRunner.
|
||||
func (bus *Bus) runHook(hook Hook, evt string, evtErr error, context map[string]any) (executed bool, err error) {
|
||||
name := hook.Name
|
||||
if name == "" {
|
||||
if hook.Kubectl != nil {
|
||||
name = "kubectlApply"
|
||||
} else {
|
||||
name = hook.Command
|
||||
}
|
||||
}
|
||||
|
||||
hookCtx, span := telemetry.Tracer(telemetry.ScopeHelmfile).Start(bus.spanParent(), "helmfile.hook",
|
||||
trace.WithAttributes(
|
||||
attribute.String("hook.event", evt),
|
||||
attribute.String("hook.name", name),
|
||||
),
|
||||
)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// The raw error embeds the rendered command (which may contain
|
||||
// templated credentials) and the runner's detailed exit error;
|
||||
// keep the span description generic.
|
||||
span.SetStatus(codes.Error, "hook failed")
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
|
||||
if hook.Kubectl != nil {
|
||||
if err := bus.prepareKubectlHook(&hook, name); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
bus.Logger.Debugf("hook[%s]: stateFilePath=%s, basePath=%s\n", name, bus.StateFilePath, bus.BasePath)
|
||||
|
||||
data := map[string]any{
|
||||
"Environment": bus.Env,
|
||||
"Namespace": bus.Namespace,
|
||||
"Event": event{
|
||||
Name: evt,
|
||||
Error: evtErr,
|
||||
},
|
||||
}
|
||||
maps.Copy(data, context)
|
||||
render := tmpl.NewTextRenderer(bus.Fs, bus.BasePath, data)
|
||||
|
||||
bus.Logger.Debugf("hook[%s]: triggered by event \"%s\"\n", name, evt)
|
||||
|
||||
command, err := render.RenderTemplateText(hook.Command)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("hook[%s]: %v", name, err)
|
||||
}
|
||||
|
||||
args := make([]string, len(hook.Args))
|
||||
for i, raw := range hook.Args {
|
||||
args[i], err = render.RenderTemplateText(raw)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("hook[%s]: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
bytes, err := bus.hookRunner(hookCtx).Execute(command, args, map[string]string{}, false)
|
||||
bus.Logger.Debugf("hook[%s]: %s\n", name, string(bytes))
|
||||
if hook.ShowLogs {
|
||||
prefix := fmt.Sprintf("\nhook[%s] logs | ", evt)
|
||||
bus.Logger.Infow(prefix + strings.ReplaceAll(string(bytes), "\n", prefix))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("hook[%s]: command `%s` failed: %v", name, command, err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// prepareKubectlHook rewrites a kubectlApply hook into the equivalent
|
||||
// explicit kubectl command, rejecting invalid configurations.
|
||||
func (bus *Bus) prepareKubectlHook(hook *Hook, name string) error {
|
||||
if hook.Command != "" {
|
||||
bus.Logger.Warnf("warn: ignoring command '%s' given within a kubectlApply hook", hook.Command)
|
||||
}
|
||||
hook.Command = "kubectl"
|
||||
|
||||
if val, found := hook.Kubectl["filename"]; found {
|
||||
if _, found := hook.Kubectl["kustomize"]; found {
|
||||
return fmt.Errorf("hook[%s]: kustomize & filename cannot be used together", name)
|
||||
}
|
||||
hook.Args = append([]string{"apply", "-f"}, val)
|
||||
return nil
|
||||
}
|
||||
if val, found := hook.Kubectl["kustomize"]; found {
|
||||
hook.Args = append([]string{"apply", "-k"}, val)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("hook[%s]: either kustomize or filename must be given", name)
|
||||
}
|
||||
|
||||
// spanParent returns the context hook spans attach to.
|
||||
func (bus *Bus) spanParent() goContext.Context {
|
||||
if bus.Ctx != nil {
|
||||
return bus.Ctx
|
||||
}
|
||||
return goContext.Background()
|
||||
}
|
||||
|
||||
// hookRunner returns a runner whose context is the hook span's, so the
|
||||
// subprocess span started inside ShellRunner nests under the hook span. The
|
||||
// cancellation semantics are unchanged: hookCtx derives from Bus.Ctx, which
|
||||
// by contract never carries cancellation. Non-ShellRunner runners (test
|
||||
// fakes) are returned unchanged.
|
||||
func (bus *Bus) hookRunner(hookCtx goContext.Context) helmexec.Runner {
|
||||
switch r := bus.Runner.(type) {
|
||||
case *helmexec.ShellRunner:
|
||||
clone := *r
|
||||
clone.Ctx = hookCtx
|
||||
return &clone
|
||||
case helmexec.ShellRunner:
|
||||
clone := r
|
||||
clone.Ctx = hookCtx
|
||||
return clone
|
||||
default:
|
||||
return bus.Runner
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
goContext "context"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
v1 "go.opentelemetry.io/proto/otlp/trace/v1"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/filesystem"
|
||||
"github.com/helmfile/helmfile/pkg/telemetry"
|
||||
"github.com/helmfile/helmfile/pkg/telemetry/otlptest"
|
||||
)
|
||||
|
||||
// TestHookSpanExported runs one hook through the default ShellRunner with
|
||||
// telemetry enabled and asserts the helmfile.hook span and its child os.exec
|
||||
// span (proving the hook's subprocess nests under the hook span, not the
|
||||
// command span directly).
|
||||
func TestHookSpanExported(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("uses the unix true binary")
|
||||
}
|
||||
|
||||
rec := otlptest.NewRecorder(t)
|
||||
otlptest.SetupTelemetry(t, rec, "helmfile test")
|
||||
|
||||
bus := &Bus{
|
||||
Hooks: []Hook{
|
||||
{
|
||||
Name: "migrate",
|
||||
Events: []string{"presync"},
|
||||
Command: "true",
|
||||
},
|
||||
{
|
||||
Name: "other-event-hook",
|
||||
Events: []string{"postsync"},
|
||||
Command: "true",
|
||||
},
|
||||
},
|
||||
Logger: zap.NewNop().Sugar(),
|
||||
Fs: filesystem.DefaultFileSystem(),
|
||||
Ctx: goContext.WithoutCancel(telemetry.CommandContext()),
|
||||
}
|
||||
|
||||
executed, err := bus.Trigger("presync", nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, executed, "the presync hook should have run")
|
||||
|
||||
otlptest.ShutdownTelemetry(t)
|
||||
|
||||
spans := rec.Spans(t)
|
||||
hook := otlptest.FindSpanWhere(t, spans, func(s *v1.Span) bool { return s.Name == "helmfile.hook" }, "hook span")
|
||||
|
||||
evt, ok := otlptest.AttrString(hook, "hook.event")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "presync", evt)
|
||||
name, ok := otlptest.AttrString(hook, "hook.name")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "migrate", name)
|
||||
|
||||
// Exactly one hook span: the postsync hook must not run for presync.
|
||||
for _, s := range spans {
|
||||
if s.Name == "helmfile.hook" {
|
||||
id, _ := otlptest.AttrString(s, "hook.name")
|
||||
assert.Equal(t, "migrate", id, "only the matching hook should produce a span")
|
||||
}
|
||||
}
|
||||
|
||||
// The hook's subprocess span nests under the hook span.
|
||||
exec := otlptest.FindSpanWhere(t, spans, func(s *v1.Span) bool { return s.Name == "os.exec" }, "hook exec span")
|
||||
assert.Equal(t, hook.TraceId, exec.TraceId)
|
||||
assert.Equal(t, hook.SpanId, exec.ParentSpanId, "os.exec span must nest under the helmfile.hook span")
|
||||
}
|
||||
|
||||
// TestHookSpanFailureMessageIsGeneric runs a failing hook and asserts the
|
||||
// span's error status does not embed the rendered command or runner details.
|
||||
func TestHookSpanFailureMessageIsGeneric(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("uses the unix false binary")
|
||||
}
|
||||
|
||||
rec := otlptest.NewRecorder(t)
|
||||
otlptest.SetupTelemetry(t, rec, "helmfile test")
|
||||
|
||||
bus := &Bus{
|
||||
Hooks: []Hook{
|
||||
{
|
||||
Name: "fail",
|
||||
Events: []string{"presync"},
|
||||
Command: "false",
|
||||
Args: []string{"--secret", "hunter2"},
|
||||
},
|
||||
},
|
||||
Logger: zap.NewNop().Sugar(),
|
||||
Fs: filesystem.DefaultFileSystem(),
|
||||
Ctx: goContext.WithoutCancel(telemetry.CommandContext()),
|
||||
}
|
||||
|
||||
_, err := bus.Trigger("presync", nil, nil)
|
||||
require.Error(t, err)
|
||||
|
||||
otlptest.ShutdownTelemetry(t)
|
||||
|
||||
hook := otlptest.FindSpanWhere(t, rec.Spans(t), func(s *v1.Span) bool { return s.Name == "helmfile.hook" }, "hook span")
|
||||
require.NotNil(t, hook.Status)
|
||||
assert.Equal(t, v1.Status_STATUS_CODE_ERROR, hook.Status.Code)
|
||||
assert.Equal(t, "hook failed", hook.Status.Message)
|
||||
assert.NotContains(t, hook.Status.Message, "false")
|
||||
assert.NotContains(t, hook.Status.Message, "hunter2")
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
goContext "context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/environment"
|
||||
ffs "github.com/helmfile/helmfile/pkg/filesystem"
|
||||
"github.com/helmfile/helmfile/pkg/helmexec"
|
||||
)
|
||||
|
||||
type runner struct {
|
||||
@@ -343,3 +345,27 @@ func TestTriggerCleanupEventWithNilError(t *testing.T) {
|
||||
t.Errorf("expected arg %q, got %q", expectedArg, call.args[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBusDefaultRunnerUsesCtxWhenSet(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
bus := &Bus{Ctx: ctx, Logger: zap.NewNop().Sugar()}
|
||||
|
||||
_, err := bus.Trigger("presync", nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
runner, ok := bus.Runner.(helmexec.ShellRunner)
|
||||
require.True(t, ok, "default runner should be a ShellRunner")
|
||||
require.Equal(t, ctx, runner.Ctx, "default runner must use Bus.Ctx when set")
|
||||
}
|
||||
|
||||
func TestBusDefaultRunnerFallsBackToTODO(t *testing.T) {
|
||||
bus := &Bus{Logger: zap.NewNop().Sugar()}
|
||||
|
||||
_, err := bus.Trigger("presync", nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
runner, ok := bus.Runner.(helmexec.ShellRunner)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, goContext.TODO(), runner.Ctx, "nil Bus.Ctx must preserve the historical TODO context")
|
||||
}
|
||||
|
||||
@@ -50,6 +50,10 @@ type Helm struct {
|
||||
|
||||
UpdateDepsCallbacks map[string]func(string) error
|
||||
|
||||
// ShowChartWithFlagsFunc lets tests stub `helm show chart <ref> --version <constraint>`,
|
||||
// which state.HelmState uses to resolve OCI constraint versions before caching.
|
||||
ShowChartWithFlagsFunc func(chartPath string, flags ...string) (chart.Metadata, error)
|
||||
|
||||
DiffMutex *sync.Mutex
|
||||
ChartsMutex *sync.Mutex
|
||||
ReleasesMutex *sync.Mutex
|
||||
@@ -332,6 +336,18 @@ func (helm *Helm) ShowChart(chartPath string) (chart.Metadata, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// ShowChartWithFlags mimics `helm show chart <ref> --version <constraint>`
|
||||
// resolution used by state.HelmState to convert a version constraint to a
|
||||
// concrete semver before caching. Test cases can inject a fake resolver via
|
||||
// helm.ShowChartWithFlagsFunc; the default falls back to ShowChart to keep
|
||||
// unrelated tests working.
|
||||
func (helm *Helm) ShowChartWithFlags(chartPath string, flags ...string) (chart.Metadata, error) {
|
||||
if helm.ShowChartWithFlagsFunc != nil {
|
||||
return helm.ShowChartWithFlagsFunc(chartPath, flags...)
|
||||
}
|
||||
return helm.ShowChart(chartPath)
|
||||
}
|
||||
|
||||
// IsHelm4Enabled detects the installed Helm version by executing the helm binary.
|
||||
// It returns true if Helm 4.x is installed, false for Helm 3.x or earlier.
|
||||
// Falls back to environment variable HELMFILE_HELM4 if helm binary is not available.
|
||||
|
||||
@@ -3,6 +3,7 @@ package hcllang
|
||||
import (
|
||||
nativejson "encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
@@ -49,9 +50,7 @@ func ctyMergeValues(a, b cty.Value) cty.Value {
|
||||
(a.Type().IsMapType() && b.Type().IsMapType()) {
|
||||
mergedAttrs := make(map[string]cty.Value)
|
||||
// Start with all attrs from a.
|
||||
for name, av := range a.AsValueMap() {
|
||||
mergedAttrs[name] = av
|
||||
}
|
||||
maps.Copy(mergedAttrs, a.AsValueMap())
|
||||
// Overlay attrs from b.
|
||||
for name, bv := range b.AsValueMap() {
|
||||
if av, ok := mergedAttrs[name]; ok {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package helmexec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
)
|
||||
|
||||
@@ -8,4 +9,12 @@ type HelmContext struct {
|
||||
HistoryMax int
|
||||
WorkerIndex int
|
||||
Writer io.Writer
|
||||
|
||||
// Ctx, when set, carries the per-release span context so that helm
|
||||
// subprocesses started for this release nest under the release span
|
||||
// (consumed by the execer's execWithContext funnel; nil falls back to the
|
||||
// runner's own context, i.e. the historical behavior). Cancellation
|
||||
// semantics are unchanged either way: callers derive it from the app
|
||||
// context, which is where the runner's context comes from too.
|
||||
Ctx context.Context
|
||||
}
|
||||
|
||||
+85
-58
@@ -181,12 +181,20 @@ func GetPluginVersion(name, pluginsDir string) (*semver.Version, error) {
|
||||
return nil, fmt.Errorf("plugin %s not installed", name)
|
||||
}
|
||||
|
||||
func redactedURL(chart string) string {
|
||||
chartURL, err := url.ParseRequestURI(chart)
|
||||
// RedactedURL returns ref with any password in the URL userinfo replaced,
|
||||
// matching helmfile's log redaction (url.URL.Redacted). Non-URL strings are
|
||||
// returned unchanged. Telemetry reuses it so span attributes are sanitized at
|
||||
// least as strictly as log output.
|
||||
func RedactedURL(ref string) string {
|
||||
refURL, err := url.ParseRequestURI(ref)
|
||||
if err != nil {
|
||||
return chart
|
||||
return ref
|
||||
}
|
||||
return chartURL.Redacted()
|
||||
return refURL.Redacted()
|
||||
}
|
||||
|
||||
func redactedURL(chart string) string {
|
||||
return RedactedURL(chart)
|
||||
}
|
||||
|
||||
// New for running helm commands
|
||||
@@ -298,14 +306,8 @@ func (helm *execer) retryRepoOp(name string, op func() ([]byte, error)) ([]byte,
|
||||
}
|
||||
// Cap the shift exponent at 5 (2^5 = 32x already exceeds the 30x cap)
|
||||
// so very large --repo-retries values can't overflow time.Duration.
|
||||
shift := attempt
|
||||
if shift > 5 {
|
||||
shift = 5
|
||||
}
|
||||
backoff := repoRetryBaseBackoff * time.Duration(1<<shift)
|
||||
if backoff > 30*repoRetryBaseBackoff {
|
||||
backoff = 30 * repoRetryBaseBackoff
|
||||
}
|
||||
shift := min(attempt, 5)
|
||||
backoff := min(repoRetryBaseBackoff*time.Duration(1<<shift), 30*repoRetryBaseBackoff)
|
||||
helm.logger.Warnf("repo operation %q failed (%s); retry %d/%d in %v",
|
||||
name, conciseError(err), attempt+1, maxRetries, backoff)
|
||||
// sleepCtx returns false if interrupted by context cancellation; in that
|
||||
@@ -321,8 +323,7 @@ func (helm *execer) retryRepoOp(name string, op func() ([]byte, error)) ([]byte,
|
||||
// reports just the exit status, avoiding the very verbose PATH/ARGS/OUTPUT
|
||||
// dump that Error() produces.
|
||||
func conciseError(err error) string {
|
||||
var ee ExitError
|
||||
if errors.As(err, &ee) {
|
||||
if ee, ok := errors.AsType[ExitError](err); ok {
|
||||
return fmt.Sprintf("exit status %d", ee.ExitStatus())
|
||||
}
|
||||
return err.Error()
|
||||
@@ -415,7 +416,7 @@ func (helm *execer) AddRepo(name, repository, cafile, certfile, keyfile, usernam
|
||||
fmt.Fprintf(&buffer, "%s\n", password)
|
||||
return helm.execStdIn(args, map[string]string{}, &buffer)
|
||||
}
|
||||
return helm.exec(args, map[string]string{}, nil)
|
||||
return helm.exec(args, map[string]string{})
|
||||
})
|
||||
default:
|
||||
helm.logger.Errorf("ERROR: unknown type '%v' for repository %v", managed, name)
|
||||
@@ -434,7 +435,7 @@ func (helm *execer) UpdateRepo() error {
|
||||
helm.extra = savedExtra
|
||||
}()
|
||||
out, err := helm.retryRepoOp("update", func() ([]byte, error) {
|
||||
return helm.exec([]string{"repo", "update"}, map[string]string{}, nil)
|
||||
return helm.exec([]string{"repo", "update"}, map[string]string{})
|
||||
})
|
||||
helm.info(out)
|
||||
return err
|
||||
@@ -507,8 +508,7 @@ func getSupportedDependencyFlags() map[string]bool {
|
||||
// Get global flags from Helm 3 cli.EnvSettings
|
||||
envSettings := cliv3.New()
|
||||
envType := reflect.TypeOf(*envSettings)
|
||||
for i := 0; i < envType.NumField(); i++ {
|
||||
field := envType.Field(i)
|
||||
for field := range envType.Fields() {
|
||||
if field.IsExported() {
|
||||
flagName := "--" + toKebabCase(field.Name)
|
||||
supported[flagName] = true
|
||||
@@ -521,8 +521,7 @@ func getSupportedDependencyFlags() map[string]bool {
|
||||
// Get dependency-specific flags from Helm 3 action.Dependency
|
||||
dep := actionv3.NewDependency()
|
||||
depType := reflect.TypeOf(*dep)
|
||||
for i := 0; i < depType.NumField(); i++ {
|
||||
field := depType.Field(i)
|
||||
for field := range depType.Fields() {
|
||||
if field.IsExported() {
|
||||
flagName := "--" + toKebabCase(field.Name)
|
||||
supported[flagName] = true
|
||||
@@ -532,8 +531,7 @@ func getSupportedDependencyFlags() map[string]bool {
|
||||
// Get global flags from Helm 4 cli.EnvSettings
|
||||
envSettings := cliv4.New()
|
||||
envType := reflect.TypeOf(*envSettings)
|
||||
for i := 0; i < envType.NumField(); i++ {
|
||||
field := envType.Field(i)
|
||||
for field := range envType.Fields() {
|
||||
if field.IsExported() {
|
||||
flagName := "--" + toKebabCase(field.Name)
|
||||
supported[flagName] = true
|
||||
@@ -546,8 +544,7 @@ func getSupportedDependencyFlags() map[string]bool {
|
||||
// Get dependency-specific flags from Helm 4 action.Dependency
|
||||
dep := actionv4.NewDependency()
|
||||
depType := reflect.TypeOf(*dep)
|
||||
for i := 0; i < depType.NumField(); i++ {
|
||||
field := depType.Field(i)
|
||||
for field := range depType.Fields() {
|
||||
if field.IsExported() {
|
||||
flagName := "--" + toKebabCase(field.Name)
|
||||
supported[flagName] = true
|
||||
@@ -581,8 +578,8 @@ func filterDependencyUnsupportedFlags(flags []string) []string {
|
||||
for _, flag := range flags {
|
||||
// Extract flag name without value (e.g., "--dry-run=server" -> "--dry-run")
|
||||
flagName := flag
|
||||
if idx := strings.Index(flag, "="); idx != -1 {
|
||||
flagName = flag[:idx]
|
||||
if before, _, ok := strings.Cut(flag, "="); ok {
|
||||
flagName = before
|
||||
}
|
||||
|
||||
// Check if this flag or any prefix of it is supported
|
||||
@@ -624,7 +621,7 @@ func (helm *execer) BuildDeps(name, chart string, flags ...string) error {
|
||||
args = append(args, "--plain-http")
|
||||
}
|
||||
|
||||
out, err := helm.exec(args, map[string]string{}, nil)
|
||||
out, err := helm.exec(args, map[string]string{})
|
||||
helm.info(out)
|
||||
return err
|
||||
}
|
||||
@@ -646,7 +643,7 @@ func (helm *execer) UpdateDeps(chart string) error {
|
||||
args = append(args, "--plain-http")
|
||||
}
|
||||
|
||||
out, err := helm.exec(args, map[string]string{}, nil)
|
||||
out, err := helm.exec(args, map[string]string{})
|
||||
helm.info(out)
|
||||
return err
|
||||
}
|
||||
@@ -658,7 +655,7 @@ func (helm *execer) SyncRelease(context HelmContext, name, chart, namespace stri
|
||||
|
||||
flags = append(flags, "--history-max", strconv.Itoa(context.HistoryMax))
|
||||
|
||||
out, err := helm.exec(append(append(preArgs, "upgrade", "--install", name, chart), flags...), env, nil)
|
||||
out, err := helm.execWithContext(context.Ctx, append(append(preArgs, "upgrade", "--install", name, chart), flags...), env, nil)
|
||||
helm.info(out)
|
||||
return err
|
||||
}
|
||||
@@ -667,7 +664,7 @@ func (helm *execer) ReleaseStatus(context HelmContext, name string, flags ...str
|
||||
helm.logger.Infof("Getting status %v", name)
|
||||
preArgs := make([]string, 0)
|
||||
env := make(map[string]string)
|
||||
out, err := helm.exec(append(append(preArgs, "status", name), flags...), env, nil)
|
||||
out, err := helm.execWithContext(context.Ctx, append(append(preArgs, "status", name), flags...), env, nil)
|
||||
helm.info(out)
|
||||
return err
|
||||
}
|
||||
@@ -679,7 +676,7 @@ func (helm *execer) List(context HelmContext, filter string, flags ...string) (s
|
||||
args := []string{"list", "--filter", filter}
|
||||
|
||||
enableLiveOutput := false
|
||||
out, err := helm.exec(append(append(preArgs, args...), flags...), env, &enableLiveOutput)
|
||||
out, err := helm.execWithContext(context.Ctx, append(append(preArgs, args...), flags...), env, &enableLiveOutput)
|
||||
// In v2 we have been expecting `helm list FILTER` prints nothing.
|
||||
// In v3 helm still prints the header like `NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION`,
|
||||
// which confuses helmfile's existing logic that treats any non-empty output from `helm list` is considered as the indication
|
||||
@@ -734,7 +731,7 @@ func (helm *execer) DecryptSecret(context HelmContext, name string, flags ...str
|
||||
secretArg = "decrypt"
|
||||
}
|
||||
enableLiveOutput := false
|
||||
secretBytes, err := helm.exec(append(append(preArgs, "secrets", secretArg, absPath), flags...), env, &enableLiveOutput)
|
||||
secretBytes, err := helm.execWithContext(context.Ctx, append(append(preArgs, "secrets", secretArg, absPath), flags...), env, &enableLiveOutput)
|
||||
if err != nil {
|
||||
secret.err = err
|
||||
return "", err
|
||||
@@ -833,8 +830,8 @@ func (helm *execer) TemplateRelease(name string, chart string, flags ...string)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(flags[i], "--output-dir=") {
|
||||
outputDir = strings.TrimPrefix(flags[i], "--output-dir=")
|
||||
if after, ok := strings.CutPrefix(flags[i], "--output-dir="); ok {
|
||||
outputDir = after
|
||||
continue
|
||||
}
|
||||
filteredFlags = append(filteredFlags, flags[i])
|
||||
@@ -844,7 +841,7 @@ func (helm *execer) TemplateRelease(name string, chart string, flags ...string)
|
||||
return fmt.Errorf("output dir not found for template command")
|
||||
}
|
||||
|
||||
out, err := helm.exec(append(args, filteredFlags...), map[string]string{}, nil)
|
||||
out, err := helm.exec(append(args, filteredFlags...), map[string]string{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -876,7 +873,7 @@ func (helm *execer) TemplateRelease(name string, chart string, flags ...string)
|
||||
return nil
|
||||
}
|
||||
|
||||
out, err := helm.exec(append(args, flags...), map[string]string{}, nil)
|
||||
out, err := helm.exec(append(args, flags...), map[string]string{})
|
||||
|
||||
if outputToFile {
|
||||
// With --output-dir is passed to helm-template,
|
||||
@@ -921,7 +918,7 @@ func (helm *execer) DiffRelease(context HelmContext, name, chart, namespace stri
|
||||
flags = helm.filterColorFlagsForHelm4(flags, env)
|
||||
}
|
||||
|
||||
out, err := helm.exec(append(append(preArgs, "diff", "upgrade", "--allow-unreleased", name, chart), flags...), env, overrideEnableLiveOutput)
|
||||
out, err := helm.execWithContext(context.Ctx, append(append(preArgs, "diff", "upgrade", "--allow-unreleased", name, chart), flags...), env, overrideEnableLiveOutput)
|
||||
// Do our best to write STDOUT only when diff existed
|
||||
// Unfortunately, this works only when you run helmfile with `--detailed-exitcode`
|
||||
detailedExitcodeEnabled := false
|
||||
@@ -978,7 +975,7 @@ func (helm *execer) filterColorFlagsForHelm4(flags []string, env map[string]stri
|
||||
|
||||
func (helm *execer) Lint(name, chart string, flags ...string) error {
|
||||
helm.logger.Infof("Linting release=%v, chart=%v", name, chart)
|
||||
out, err := helm.exec(append([]string{"lint", chart}, flags...), map[string]string{}, nil)
|
||||
out, err := helm.exec(append([]string{"lint", chart}, flags...), map[string]string{})
|
||||
// Always write to stdout to write the linting result to eg. a file
|
||||
helm.write(nil, out)
|
||||
return err
|
||||
@@ -1003,14 +1000,14 @@ func (helm *execer) Unittest(name, chart string, flags ...string) error {
|
||||
}
|
||||
|
||||
helm.logger.Infof("Unit testing release=%v, chart=%v", name, chart)
|
||||
out, err := helm.exec(append([]string{"unittest", chart}, flags...), map[string]string{}, nil)
|
||||
out, err := helm.exec(append([]string{"unittest", chart}, flags...), map[string]string{})
|
||||
helm.write(nil, out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) Fetch(chart string, flags ...string) error {
|
||||
helm.logger.Infof("Fetching %v", redactedURL(chart))
|
||||
out, err := helm.exec(append([]string{"fetch", chart}, flags...), map[string]string{}, nil)
|
||||
out, err := helm.exec(append([]string{"fetch", chart}, flags...), map[string]string{})
|
||||
helm.info(out)
|
||||
return err
|
||||
}
|
||||
@@ -1032,7 +1029,7 @@ func (helm *execer) ChartPull(chart string, path string, flags ...string) error
|
||||
} else {
|
||||
helmArgs = []string{"chart", "pull", chart}
|
||||
}
|
||||
out, err := helm.exec(helmArgs, map[string]string{"HELM_EXPERIMENTAL_OCI": "1"}, nil)
|
||||
out, err := helm.exec(helmArgs, map[string]string{"HELM_EXPERIMENTAL_OCI": "1"})
|
||||
helm.info(out)
|
||||
return err
|
||||
}
|
||||
@@ -1048,7 +1045,7 @@ func (helm *execer) ChartExport(chart string, path string) error {
|
||||
helm.logger.Infof("Exporting %v", chart)
|
||||
helmArgs = []string{"chart", "export", chart, "--destination", path}
|
||||
// no extra flags for before v3.7.0, details in helm chart export --help
|
||||
out, err := helm.exec(helmArgs, map[string]string{"HELM_EXPERIMENTAL_OCI": "1"}, nil)
|
||||
out, err := helm.exec(helmArgs, map[string]string{"HELM_EXPERIMENTAL_OCI": "1"})
|
||||
helm.info(out)
|
||||
return err
|
||||
}
|
||||
@@ -1057,7 +1054,7 @@ func (helm *execer) DeleteRelease(context HelmContext, name string, flags ...str
|
||||
helm.logger.Infof("Deleting %v", name)
|
||||
preArgs := make([]string, 0)
|
||||
env := make(map[string]string)
|
||||
out, err := helm.exec(append(append(preArgs, "delete", name), flags...), env, nil)
|
||||
out, err := helm.execWithContext(context.Ctx, append(append(preArgs, "delete", name), flags...), env, nil)
|
||||
helm.info(out)
|
||||
return err
|
||||
}
|
||||
@@ -1067,7 +1064,7 @@ func (helm *execer) TestRelease(context HelmContext, name string, flags ...strin
|
||||
preArgs := make([]string, 0)
|
||||
env := make(map[string]string)
|
||||
args := []string{"test", name}
|
||||
out, err := helm.exec(append(append(preArgs, args...), flags...), env, nil)
|
||||
out, err := helm.execWithContext(context.Ctx, append(append(preArgs, args...), flags...), env, nil)
|
||||
helm.info(out)
|
||||
return err
|
||||
}
|
||||
@@ -1081,7 +1078,7 @@ func (helm *execer) AddPlugin(name, path, version string) error {
|
||||
}
|
||||
|
||||
// Try with verification first
|
||||
out, err := helm.exec([]string{"plugin", "install", path, "--version", version}, map[string]string{}, nil)
|
||||
out, err := helm.exec([]string{"plugin", "install", path, "--version", version}, map[string]string{})
|
||||
|
||||
// If verification fails, retry without verification (unless enforced)
|
||||
if err != nil && strings.Contains(err.Error(), "does not support verification") {
|
||||
@@ -1090,7 +1087,7 @@ func (helm *execer) AddPlugin(name, path, version string) error {
|
||||
return fmt.Errorf("plugin %s does not support verification (remove --enforce-plugin-verification flag to allow unverified plugins)", name)
|
||||
}
|
||||
helm.logger.Debugf("Plugin %v does not support verification, retrying with --verify=false", name)
|
||||
out, err = helm.exec([]string{"plugin", "install", path, "--version", version, "--verify=false"}, map[string]string{}, nil)
|
||||
out, err = helm.exec([]string{"plugin", "install", path, "--version", version, "--verify=false"}, map[string]string{})
|
||||
}
|
||||
|
||||
helm.info(out)
|
||||
@@ -1121,7 +1118,7 @@ func (helm *execer) installHelmSecretsV4(version string) error {
|
||||
args = append(args, verifyFlag)
|
||||
}
|
||||
|
||||
out, err := helm.exec(args, map[string]string{}, nil)
|
||||
out, err := helm.exec(args, map[string]string{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to install %s: %w", plugin, err)
|
||||
}
|
||||
@@ -1159,7 +1156,7 @@ func helmSecretsRequiresSplitInstall(version string) bool {
|
||||
|
||||
func (helm *execer) uninstallPlugin(name string) error {
|
||||
helm.logger.Infof("Uninstalling helm plugin %v", name)
|
||||
out, err := helm.exec([]string{"plugin", "uninstall", name}, map[string]string{}, nil)
|
||||
out, err := helm.exec([]string{"plugin", "uninstall", name}, map[string]string{})
|
||||
if err == nil {
|
||||
helm.info(out)
|
||||
}
|
||||
@@ -1204,7 +1201,28 @@ func (helm *execer) UpdatePlugin(name, repo, version string) error {
|
||||
return helm.AddPlugin(name, repo, version)
|
||||
}
|
||||
|
||||
func (helm *execer) exec(args []string, env map[string]string, overrideEnableLiveOutput *bool) ([]byte, error) {
|
||||
func (helm *execer) exec(args []string, env map[string]string) ([]byte, error) {
|
||||
return helm.execWithRunner(helm.runner, args, env, nil)
|
||||
}
|
||||
|
||||
// execWithContext behaves like exec but attaches the span carried by ctx
|
||||
// (from HelmContext.Ctx) so the subprocess span nests under the per-release
|
||||
// span. Crucially, the subprocess keeps the runner's own context: replacing
|
||||
// it would override specialized cancellation contexts such as the kubedog
|
||||
// safety valve installed via execer.WithContext. A nil ctx is exactly exec.
|
||||
func (helm *execer) execWithContext(ctx context.Context, args []string, env map[string]string, overrideEnableLiveOutput *bool) ([]byte, error) {
|
||||
runner := helm.runner
|
||||
if ctx != nil {
|
||||
runner = withRunnerCtx(runner, func(runnerCtx context.Context) context.Context {
|
||||
return spanAttachedContext(runnerCtx, ctx)
|
||||
})
|
||||
}
|
||||
return helm.execWithRunner(runner, args, env, overrideEnableLiveOutput)
|
||||
}
|
||||
|
||||
func (helm *execer) execWithRunner(runner Runner, args []string, env map[string]string, overrideEnableLiveOutput *bool) ([]byte, error) {
|
||||
runner = markHelmRunner(runner)
|
||||
|
||||
cmdargs := args
|
||||
if len(helm.extra) > 0 {
|
||||
cmdargs = append(cmdargs, helm.extra...)
|
||||
@@ -1221,7 +1239,7 @@ func (helm *execer) exec(args []string, env map[string]string, overrideEnableLiv
|
||||
if overrideEnableLiveOutput != nil {
|
||||
enableLiveOutput = *overrideEnableLiveOutput
|
||||
}
|
||||
outBytes, err := helm.runner.Execute(helm.helmBinary, cmdargs, env, enableLiveOutput)
|
||||
outBytes, err := runner.Execute(helm.helmBinary, cmdargs, env, enableLiveOutput)
|
||||
return outBytes, err
|
||||
}
|
||||
|
||||
@@ -1238,7 +1256,7 @@ func (helm *execer) execStdIn(args []string, env map[string]string, stdin io.Rea
|
||||
}
|
||||
cmd := fmt.Sprintf("exec: %s %s", helm.helmBinary, strings.Join(cmdargs, " "))
|
||||
helm.logger.Debug(cmd)
|
||||
outBytes, err := helm.runner.ExecuteStdIn(helm.helmBinary, cmdargs, env, stdin)
|
||||
outBytes, err := markHelmRunner(helm.runner).ExecuteStdIn(helm.helmBinary, cmdargs, env, stdin)
|
||||
return outBytes, err
|
||||
}
|
||||
|
||||
@@ -1348,15 +1366,24 @@ func resolveOciChart(ociChart string) (ociChartURL, ociChartTag string) {
|
||||
}
|
||||
|
||||
func (helm *execer) ShowChart(chartPath string) (chart.Metadata, error) {
|
||||
var helmArgs = []string{"show", "chart", chartPath}
|
||||
out, error := helm.exec(helmArgs, map[string]string{}, nil)
|
||||
if error != nil {
|
||||
return chart.Metadata{}, error
|
||||
return helm.ShowChartWithFlags(chartPath)
|
||||
}
|
||||
|
||||
// ShowChartWithFlags runs `helm show chart` and unmarshals the resulting
|
||||
// Chart.yaml. Callers may pass additional helm flags (for example --version,
|
||||
// --plain-http, --registry-config, --ca-file, --insecure-skip-tls-verify).
|
||||
// When --version references a semver constraint, helm resolves it against the
|
||||
// registry and returns the concrete matching Chart.yaml, so callers can read
|
||||
// metadata.Version to obtain the resolved version.
|
||||
func (helm *execer) ShowChartWithFlags(chartPath string, flags ...string) (chart.Metadata, error) {
|
||||
helmArgs := append([]string{"show", "chart", chartPath}, flags...)
|
||||
out, err := helm.exec(helmArgs, map[string]string{})
|
||||
if err != nil {
|
||||
return chart.Metadata{}, err
|
||||
}
|
||||
var metadata chart.Metadata
|
||||
error = yaml.Unmarshal(out, &metadata)
|
||||
if error != nil {
|
||||
return chart.Metadata{}, error
|
||||
if err := yaml.Unmarshal(out, &metadata); err != nil {
|
||||
return chart.Metadata{}, err
|
||||
}
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
@@ -56,8 +56,7 @@ func TestFilterDependencyFlags_AllGlobalFlags(t *testing.T) {
|
||||
}
|
||||
|
||||
var expectedFlags []string
|
||||
for i := 0; i < envType.NumField(); i++ {
|
||||
field := envType.Field(i)
|
||||
for field := range envType.Fields() {
|
||||
if field.IsExported() {
|
||||
flagName := "--" + toKebabCase(field.Name)
|
||||
expectedFlags = append(expectedFlags, flagName)
|
||||
@@ -106,8 +105,7 @@ func TestFilterDependencyFlags_AllDependencyFlags(t *testing.T) {
|
||||
}
|
||||
|
||||
var expectedFlags []string
|
||||
for i := 0; i < depType.NumField(); i++ {
|
||||
field := depType.Field(i)
|
||||
for field := range depType.Fields() {
|
||||
if field.IsExported() {
|
||||
flagName := "--" + toKebabCase(field.Name)
|
||||
expectedFlags = append(expectedFlags, flagName)
|
||||
|
||||
@@ -1182,7 +1182,7 @@ func Test_exec(t *testing.T) {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
env := map[string]string{}
|
||||
_, err = helm.exec([]string{"version"}, env, nil)
|
||||
_, err = helm.exec([]string{"version"}, env)
|
||||
expected := `exec: helm version
|
||||
`
|
||||
if err != nil {
|
||||
@@ -1196,7 +1196,7 @@ func Test_exec(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
ret, _ := helm.exec([]string{"diff"}, env, nil)
|
||||
ret, _ := helm.exec([]string{"diff"}, env)
|
||||
if len(ret) != 0 {
|
||||
t.Error("helmexec.exec() - expected empty return value")
|
||||
}
|
||||
@@ -1206,7 +1206,7 @@ func Test_exec(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
_, err = helm.exec([]string{"diff", "release", "chart", "--timeout 10", "--wait", "--wait-for-jobs"}, env, nil)
|
||||
_, err = helm.exec([]string{"diff", "release", "chart", "--timeout 10", "--wait", "--wait-for-jobs"}, env)
|
||||
expected = `exec: helm --kubeconfig config --kube-context dev diff release chart --timeout 10 --wait --wait-for-jobs
|
||||
`
|
||||
if err != nil {
|
||||
@@ -1217,7 +1217,7 @@ func Test_exec(t *testing.T) {
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
_, err = helm.exec([]string{"version"}, env, nil)
|
||||
_, err = helm.exec([]string{"version"}, env)
|
||||
expected = `exec: helm --kubeconfig config --kube-context dev version
|
||||
`
|
||||
if err != nil {
|
||||
@@ -1229,7 +1229,7 @@ func Test_exec(t *testing.T) {
|
||||
|
||||
buffer.Reset()
|
||||
helm.SetExtraArgs("foo")
|
||||
_, err = helm.exec([]string{"version"}, env, nil)
|
||||
_, err = helm.exec([]string{"version"}, env)
|
||||
expected = `exec: helm --kubeconfig config --kube-context dev version foo
|
||||
`
|
||||
if err != nil {
|
||||
@@ -1245,7 +1245,7 @@ func Test_exec(t *testing.T) {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
helm.SetHelmBinary("overwritten")
|
||||
_, err = helm.exec([]string{"version"}, env, nil)
|
||||
_, err = helm.exec([]string{"version"}, env)
|
||||
expected = `exec: overwritten version
|
||||
`
|
||||
if err != nil {
|
||||
|
||||
+16
-13
@@ -6,32 +6,35 @@ import (
|
||||
)
|
||||
|
||||
func newExitError(path string, args []string, exitStatus int, err error, stderr, combined string, stripArgsValuesOnExitError bool) ExitError {
|
||||
var out string
|
||||
var out strings.Builder
|
||||
|
||||
out += fmt.Sprintf("PATH:\n%s", Indent(path, " "))
|
||||
fmt.Fprintf(&out, "PATH:\n%s", Indent(path, " "))
|
||||
|
||||
out += "\n\nARGS:"
|
||||
for i, a := range args {
|
||||
if i > 0 && strings.HasPrefix(args[i-1], "--set") && stripArgsValuesOnExitError {
|
||||
a = "*** STRIP ***"
|
||||
}
|
||||
out += fmt.Sprintf("\n%s", Indent(fmt.Sprintf("%d: %s (%d bytes)", i, a, len(a)), " "))
|
||||
out.WriteString("\n\nARGS:")
|
||||
// The legacy profile is byte-identical to the historical inline logic;
|
||||
// the goldens in exit_error_test.go pin its exact output.
|
||||
redacted := RedactArgs(args, RedactionLegacy)
|
||||
if !stripArgsValuesOnExitError {
|
||||
redacted = args
|
||||
}
|
||||
for i, a := range redacted {
|
||||
fmt.Fprintf(&out, "\n%s", Indent(fmt.Sprintf("%d: %s (%d bytes)", i, a, len(a)), " "))
|
||||
}
|
||||
|
||||
out += fmt.Sprintf("\n\nERROR:\n%s", Indent(err.Error(), " "))
|
||||
fmt.Fprintf(&out, "\n\nERROR:\n%s", Indent(err.Error(), " "))
|
||||
|
||||
out += fmt.Sprintf("\n\nEXIT STATUS\n%s", Indent(fmt.Sprintf("%d", exitStatus), " "))
|
||||
fmt.Fprintf(&out, "\n\nEXIT STATUS\n%s", Indent(fmt.Sprintf("%d", exitStatus), " "))
|
||||
|
||||
if len(stderr) > 0 {
|
||||
out += fmt.Sprintf("\n\nSTDERR:\n%s", Indent(stderr, " "))
|
||||
fmt.Fprintf(&out, "\n\nSTDERR:\n%s", Indent(stderr, " "))
|
||||
}
|
||||
|
||||
if len(combined) > 0 {
|
||||
out += fmt.Sprintf("\n\nCOMBINED OUTPUT:\n%s", Indent(combined, " "))
|
||||
fmt.Fprintf(&out, "\n\nCOMBINED OUTPUT:\n%s", Indent(combined, " "))
|
||||
}
|
||||
|
||||
return ExitError{
|
||||
Message: fmt.Sprintf("command %q exited with non-zero status:\n\n%s", path, out),
|
||||
Message: fmt.Sprintf("command %q exited with non-zero status:\n\n%s", path, out.String()),
|
||||
Code: exitStatus,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,3 +46,18 @@ type DependencyUpdater interface {
|
||||
IsHelm3() bool
|
||||
IsHelm4() bool
|
||||
}
|
||||
|
||||
// ChartInspector is a capability interface exposing `helm show chart` with
|
||||
// arbitrary flags. It is implemented by helmexec's concrete execer and by
|
||||
// stubs in tests. Callers should type-assert Interface to ChartInspector and
|
||||
// fall back gracefully when the implementation does not satisfy it, so that
|
||||
// third-party implementations of helmexec.Interface keep compiling. See
|
||||
// state.HelmState.resolveOCIConstraintVersion for the usage pattern.
|
||||
type ChartInspector interface {
|
||||
// ShowChartWithFlags runs `helm show chart <chart> [flags...]` and returns
|
||||
// the parsed Chart.yaml metadata. Unlike ShowChart, callers pass flags
|
||||
// such as --version, --plain-http, or --registry-config so that
|
||||
// constraint versions (e.g. "~1", "^2.0.0") can be resolved against an
|
||||
// OCI registry to a concrete semver.
|
||||
ShowChartWithFlags(chart string, flags ...string) (chart.Metadata, error)
|
||||
}
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ type logWriter struct {
|
||||
}
|
||||
|
||||
func (w *logWriter) Write(p []byte) (int, error) {
|
||||
for _, line := range strings.Split(string(p), "\n") {
|
||||
for line := range strings.SplitSeq(string(p), "\n") {
|
||||
w.log.Debugf("%s%s", w.prefix, strings.TrimSpace(line))
|
||||
}
|
||||
return len(p), nil
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package helmexec
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RedactionProfile selects how aggressively RedactArgs masks secret-bearing
|
||||
// command-line arguments.
|
||||
type RedactionProfile int
|
||||
|
||||
const (
|
||||
// RedactionLegacy reproduces the historical exit-error behavior
|
||||
// byte-for-byte: only the argument *following* a flag whose name starts
|
||||
// with "--set" is masked. The goldens in exit_error_test.go pin this
|
||||
// output; changing it changes observable error messages.
|
||||
RedactionLegacy RedactionProfile = iota
|
||||
|
||||
// RedactionStrict masks every secret-bearing argument form known today:
|
||||
// in addition to the legacy behavior it covers single-argument forms
|
||||
// (--set=key=value) and credential flags (--username, --password,
|
||||
// --key-file). Used for telemetry span attributes; span visibility must
|
||||
// be at least as redacted as error messages.
|
||||
RedactionStrict
|
||||
)
|
||||
|
||||
// redactedArg is the placeholder written in place of secret values; its exact
|
||||
// bytes are pinned by exit_error_test.go.
|
||||
const redactedArg = "*** STRIP ***"
|
||||
|
||||
// strictNextArgFlags are the flags whose following argument is a secret.
|
||||
var strictNextArgFlags = []string{
|
||||
"--set",
|
||||
"--set-string",
|
||||
"--set-file",
|
||||
"--set-json",
|
||||
"--set-literal",
|
||||
"--username",
|
||||
"--password",
|
||||
"--key-file",
|
||||
"--kube-token",
|
||||
}
|
||||
|
||||
// RedactArgs returns a copy of args with secret-bearing values masked
|
||||
// according to profile. The input slice is never mutated.
|
||||
func RedactArgs(args []string, profile RedactionProfile) []string {
|
||||
if len(args) == 0 {
|
||||
return args
|
||||
}
|
||||
|
||||
out := make([]string, len(args))
|
||||
copy(out, args)
|
||||
|
||||
for i := range out {
|
||||
// The previous token must be read from the ORIGINAL slice: reading
|
||||
// the progressively redacted output would let a masked value hide a
|
||||
// following secret (e.g. {"--set", "--set-string", "secret"}).
|
||||
var prev string
|
||||
if i > 0 {
|
||||
prev = args[i-1]
|
||||
}
|
||||
|
||||
switch profile {
|
||||
case RedactionLegacy:
|
||||
if strings.HasPrefix(prev, "--set") {
|
||||
out[i] = redactedArg
|
||||
}
|
||||
case RedactionStrict:
|
||||
if isStrictNextArgFlag(prev) {
|
||||
out[i] = redactedArg
|
||||
} else if flag := strictInlineFlag(out[i]); flag != "" {
|
||||
out[i] = flag + "=" + redactedArg
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func isStrictNextArgFlag(arg string) bool {
|
||||
return slices.Contains(strictNextArgFlags, arg)
|
||||
}
|
||||
|
||||
// strictInlineFlag returns the flag name when arg is a single-argument secret
|
||||
// form such as "--set=key=value", or "" otherwise.
|
||||
func strictInlineFlag(arg string) string {
|
||||
for _, flag := range strictNextArgFlags {
|
||||
if strings.HasPrefix(arg, flag+"=") {
|
||||
return flag
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func equalArgs(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package helmexec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
func TestRedactArgsLegacy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "two-argument --set is masked",
|
||||
args: []string{"upgrade", "--set", "secret=1"},
|
||||
want: []string{"upgrade", "--set", redactedArg},
|
||||
},
|
||||
{
|
||||
name: "--set prefix covers --set-string/--set-file/--set-json",
|
||||
args: []string{"--set-string", "a=b", "--set-file", "f", "--set-json", "{}"},
|
||||
want: []string{"--set-string", redactedArg, "--set-file", redactedArg, "--set-json", redactedArg},
|
||||
},
|
||||
{
|
||||
name: "single-argument --set=k=v is NOT masked (documented legacy gap)",
|
||||
args: []string{"--set=secret=1"},
|
||||
want: []string{"--set=secret=1"},
|
||||
},
|
||||
{
|
||||
name: "credential flags are NOT masked (documented legacy gap)",
|
||||
args: []string{"--username", "bob", "--password", "hunter2"},
|
||||
want: []string{"--username", "bob", "--password", "hunter2"},
|
||||
},
|
||||
{
|
||||
name: "first argument is never masked",
|
||||
args: []string{"--set"},
|
||||
want: []string{"--set"},
|
||||
},
|
||||
{
|
||||
name: "adjacent secret flags do not leak (previous token read from original)",
|
||||
args: []string{"--set", "--set-string", "secret"},
|
||||
want: []string{"--set", redactedArg, redactedArg},
|
||||
},
|
||||
{
|
||||
name: "strict: adjacent secret flags do not leak",
|
||||
args: []string{"--set", "--set", "a=b"},
|
||||
want: []string{"--set", redactedArg, redactedArg},
|
||||
},
|
||||
{
|
||||
name: "empty args",
|
||||
args: []string{},
|
||||
want: []string{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := RedactArgs(tt.args, RedactionLegacy)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactArgsStrict(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "legacy two-argument forms remain masked",
|
||||
args: []string{"upgrade", "--set", "secret=1", "--set-string", "s=2"},
|
||||
want: []string{"upgrade", "--set", redactedArg, "--set-string", redactedArg},
|
||||
},
|
||||
{
|
||||
name: "single-argument --set=k=v is masked",
|
||||
args: []string{"--set=secret=1", "--set-string=a=b", "--set-json={}"},
|
||||
want: []string{"--set=" + redactedArg, "--set-string=" + redactedArg, "--set-json=" + redactedArg},
|
||||
},
|
||||
{
|
||||
name: "credential flags are masked",
|
||||
args: []string{"registry", "login", "--username", "bob", "--password", "hunter2"},
|
||||
want: []string{"registry", "login", "--username", redactedArg, "--password", redactedArg},
|
||||
},
|
||||
{
|
||||
name: "key-file is masked",
|
||||
args: []string{"--key-file", "/etc/secrets/key"},
|
||||
want: []string{"--key-file", redactedArg},
|
||||
},
|
||||
{
|
||||
name: "password-stdin has no value to mask",
|
||||
args: []string{"--password-stdin"},
|
||||
want: []string{"--password-stdin"},
|
||||
},
|
||||
{
|
||||
name: "kube-token is masked (two-argument form)",
|
||||
args: []string{"--kube-context", "ctx", "upgrade", "--kube-token", "eyJhbGciOi..."},
|
||||
want: []string{"--kube-context", "ctx", "upgrade", "--kube-token", redactedArg},
|
||||
},
|
||||
{
|
||||
name: "kube-token is masked (inline form)",
|
||||
args: []string{"upgrade", "--kube-token=eyJhbGciOi..."},
|
||||
want: []string{"upgrade", "--kube-token=" + redactedArg},
|
||||
},
|
||||
{
|
||||
name: "benign flags untouched",
|
||||
args: []string{"upgrade", "--install", "envoy", "./chart", "--namespace", "ingress", "--reset-values"},
|
||||
want: []string{"upgrade", "--install", "envoy", "./chart", "--namespace", "ingress", "--reset-values"},
|
||||
},
|
||||
{
|
||||
name: "prefix lookalikes untouched",
|
||||
args: []string{"--settlement", "x", "--usernames", "y"},
|
||||
want: []string{"--settlement", "x", "--usernames", "y"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := RedactArgs(tt.args, RedactionStrict)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactArgsDoesNotMutateInput(t *testing.T) {
|
||||
orig := []string{"--set", "secret=1", "--set=x=y"}
|
||||
before := reflect.ValueOf(orig).Pointer()
|
||||
|
||||
_ = RedactArgs(orig, RedactionStrict)
|
||||
|
||||
assert.Equal(t, []string{"--set", "secret=1", "--set=x=y"}, orig, "input slice must not be mutated")
|
||||
assert.Equal(t, before, reflect.ValueOf(orig).Pointer(), "input backing array must be reused by the caller, not modified")
|
||||
}
|
||||
|
||||
func TestClassifyExec(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cmd string
|
||||
args []string
|
||||
wantName string
|
||||
wantAttrVals map[string][]string
|
||||
ctxMarked bool
|
||||
}{
|
||||
{
|
||||
name: "helm invocation",
|
||||
cmd: "/usr/local/bin/helm",
|
||||
args: []string{"upgrade", "--install", "envoy", "./chart"},
|
||||
wantName: "helm.exec",
|
||||
wantAttrVals: map[string][]string{
|
||||
"exec.command": {"helm"},
|
||||
"helm.subcommand": {"upgrade"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "helm with leading flags: subcommand is first positional",
|
||||
cmd: "helm",
|
||||
args: []string{"--kube-context", "ctx", "repo", "update"},
|
||||
wantName: "helm.exec",
|
||||
wantAttrVals: map[string][]string{
|
||||
"helm.subcommand": {"repo"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-helm binary",
|
||||
cmd: "./scripts/migrate.sh",
|
||||
args: []string{"--verbose"},
|
||||
wantName: "os.exec",
|
||||
wantAttrVals: map[string][]string{
|
||||
"exec.command": {"migrate.sh"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wrapper helm binary classified via marker, not name",
|
||||
cmd: "/opt/bin/custom-wrapper",
|
||||
args: []string{"upgrade", "demo", "./chart"},
|
||||
wantName: "helm.exec",
|
||||
wantAttrVals: map[string][]string{
|
||||
"exec.command": {"custom-wrapper"},
|
||||
"helm.subcommand": {"upgrade"},
|
||||
},
|
||||
ctxMarked: true,
|
||||
},
|
||||
{
|
||||
name: "secret args are redacted and flagged",
|
||||
cmd: "helm",
|
||||
args: []string{"upgrade", "--set", "pw=1", "--set=pw2=2"},
|
||||
wantName: "helm.exec",
|
||||
wantAttrVals: map[string][]string{
|
||||
"exec.args": {"upgrade", "--set", redactedArg, "--set=" + redactedArg},
|
||||
"exec.redacted": {"true"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "credentials embedded in positional URLs are masked",
|
||||
cmd: "helm",
|
||||
args: []string{"repo", "add", "name", "https://user:token@charts.example.com/repo"},
|
||||
wantName: "helm.exec",
|
||||
wantAttrVals: map[string][]string{
|
||||
"exec.args": {"repo", "add", "name", "https://xxxxx@charts.example.com/repo"},
|
||||
"exec.redacted": {"true"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "plain URLs are untouched",
|
||||
cmd: "helm",
|
||||
args: []string{"upgrade", "demo", "https://charts.example.com/repo/chart"},
|
||||
wantName: "helm.exec",
|
||||
wantAttrVals: map[string][]string{
|
||||
"exec.args": {"upgrade", "demo", "https://charts.example.com/repo/chart"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
if tt.ctxMarked {
|
||||
ctx = context.WithValue(ctx, helmExecMarker{}, true)
|
||||
}
|
||||
name, attrs := classifyExec(ctx, tt.cmd, tt.args)
|
||||
|
||||
assert.Equal(t, tt.wantName, name)
|
||||
got := map[string][]string{}
|
||||
for _, kv := range attrs {
|
||||
switch kv.Value.Type() {
|
||||
case attribute.STRING:
|
||||
got[string(kv.Key)] = []string{kv.Value.AsString()}
|
||||
case attribute.STRINGSLICE:
|
||||
got[string(kv.Key)] = kv.Value.AsStringSlice()
|
||||
case attribute.BOOL:
|
||||
got[string(kv.Key)] = []string{fmt.Sprintf("%t", kv.Value.AsBool())}
|
||||
}
|
||||
}
|
||||
for key, want := range tt.wantAttrVals {
|
||||
assert.Equal(t, want, got[key], "attribute %s", key)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package helmexec
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// redactedRefValue is the placeholder substituted for credential-bearing
|
||||
// userinfo and query-parameter values in telemetry attributes.
|
||||
const redactedRefValue = "xxxxx"
|
||||
|
||||
// sensitiveQueryKeys reports whether a query-parameter key is considered
|
||||
// credential-bearing, mirroring the heuristic used by pkg/remote for cache
|
||||
// keys (token/password/secret/key/signature substrings).
|
||||
func sensitiveQueryKey(key string) bool {
|
||||
lk := strings.ToLower(key)
|
||||
return strings.Contains(lk, "token") || strings.Contains(lk, "password") ||
|
||||
strings.Contains(lk, "secret") || strings.Contains(lk, "key") ||
|
||||
strings.Contains(lk, "signature")
|
||||
}
|
||||
|
||||
// RedactedRef sanitizes a remote reference (state-file path, chart reference,
|
||||
// repository URL) for telemetry export: go-getter forced-form prefixes
|
||||
// (git::, s3::, …) are preserved, URL userinfo is masked, and
|
||||
// credential-bearing query parameters have their values replaced. Non-URL
|
||||
// strings are returned unchanged. It is deliberately at least as strict as
|
||||
// the log-time RedactedURL.
|
||||
// maxForcedFormPrefix bounds the length of a recognized go-getter forced-form
|
||||
// prefix ("git::", "s3::", "hg::" …); longer "::"-containing prefixes are
|
||||
// treated as part of an opaque reference instead.
|
||||
const maxForcedFormPrefix = 16
|
||||
|
||||
func RedactedRef(ref string) string {
|
||||
force := ""
|
||||
rest := ref
|
||||
// A go-getter forced form looks like "git::https://…": a short
|
||||
// alphanumeric prefix followed by "::" at the start of the reference.
|
||||
if i := strings.Index(rest, "::"); i > 0 && i <= maxForcedFormPrefix && isAlphanumericPrefix(rest[:i]) {
|
||||
force = rest[:i+len("::")]
|
||||
rest = rest[i+len("::"):]
|
||||
}
|
||||
|
||||
u, err := url.Parse(rest)
|
||||
if err != nil {
|
||||
if strings.Contains(rest, "://") {
|
||||
// URL-like but malformed (e.g. a bad percent escape): fail
|
||||
// closed — export a fully redacted value rather than risk
|
||||
// leaking userinfo or query credentials.
|
||||
return force + redactedRefValue
|
||||
}
|
||||
return ref
|
||||
}
|
||||
if u.Scheme == "" || u.Host == "" {
|
||||
// Not a URL (e.g. "./charts/demo"); leave it alone.
|
||||
return ref
|
||||
}
|
||||
|
||||
// Mask the whole userinfo: usernames are as likely to carry tokens as
|
||||
// passwords are (e.g. "https://x-access-token@host").
|
||||
if u.User != nil {
|
||||
u.User = url.User(redactedRefValue)
|
||||
}
|
||||
|
||||
if u.RawQuery != "" {
|
||||
q, err := url.ParseQuery(u.RawQuery)
|
||||
if err != nil {
|
||||
// Malformed query: fail closed by dropping it entirely.
|
||||
u.RawQuery = ""
|
||||
} else {
|
||||
for key, values := range q {
|
||||
if sensitiveQueryKey(key) {
|
||||
for i := range values {
|
||||
values[i] = redactedRefValue
|
||||
}
|
||||
}
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
}
|
||||
}
|
||||
|
||||
return force + u.String()
|
||||
}
|
||||
|
||||
func isAlphanumericPrefix(s string) bool {
|
||||
for _, r := range s {
|
||||
if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return len(s) > 0
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package helmexec
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRedactedRef(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ref string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "plain chart path unchanged",
|
||||
ref: "./charts/demo",
|
||||
want: "./charts/demo",
|
||||
},
|
||||
{
|
||||
name: "https userinfo fully masked (username may carry the token)",
|
||||
ref: "https://user:token@charts.example.com/repo",
|
||||
want: "https://xxxxx@charts.example.com/repo",
|
||||
},
|
||||
{
|
||||
name: "go-getter forced form preserved and sanitized",
|
||||
ref: "git::https://x-access-token@github.com/org/repo.git//helmfile?ref=v1",
|
||||
want: "git::https://xxxxx@github.com/org/repo.git//helmfile?ref=v1",
|
||||
},
|
||||
{
|
||||
name: "s3 query credentials masked",
|
||||
ref: "s3::https://s3.amazonaws.com/bucket/helmfile?aws_access_key_id=AKIA&aws_secret_access_key=zzz®ion=us-east-1",
|
||||
want: "s3::https://s3.amazonaws.com/bucket/helmfile?aws_access_key_id=xxxxx&aws_secret_access_key=xxxxx®ion=us-east-1",
|
||||
},
|
||||
{
|
||||
name: "generic token query param masked",
|
||||
ref: "https://example.com/file?token=abc&path=ok",
|
||||
want: "https://example.com/file?path=ok&token=xxxxx",
|
||||
},
|
||||
{
|
||||
name: "non-credential query untouched",
|
||||
ref: "https://example.com/chart?ref=main&verify=true",
|
||||
want: "https://example.com/chart?ref=main&verify=true",
|
||||
},
|
||||
{
|
||||
name: "URL without scheme or host returned unchanged",
|
||||
ref: "git@github.com:org/repo.git",
|
||||
want: "git@github.com:org/repo.git",
|
||||
},
|
||||
{
|
||||
name: "malformed URL-like ref fails fully redacted",
|
||||
ref: "https://user:token@host/%zz",
|
||||
want: redactedRefValue,
|
||||
},
|
||||
{
|
||||
name: "malformed query is dropped entirely",
|
||||
ref: "https://host/file?token=secret%zz&ok=1",
|
||||
want: "https://host/file",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, RedactedRef(tt.ref))
|
||||
})
|
||||
}
|
||||
}
|
||||
+31
-8
@@ -7,11 +7,13 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
@@ -34,28 +36,51 @@ type ShellRunner struct {
|
||||
Ctx context.Context
|
||||
}
|
||||
|
||||
// Execute a shell command
|
||||
// Execute a shell command. The whole execution is wrapped in one span
|
||||
// (helm.exec/os.exec, see span.go); span overhead is nil when telemetry is
|
||||
// disabled.
|
||||
func (shell ShellRunner) Execute(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
|
||||
ctx, span, isHelm := startExecSpan(shell.Ctx, cmd, args)
|
||||
defer span.End()
|
||||
|
||||
start := time.Now()
|
||||
out, err := shell.run(ctx, cmd, args, env, enableLiveOutput)
|
||||
finishExecSpan(span, isHelm, args, start, err)
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (shell ShellRunner) run(ctx context.Context, cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
|
||||
preparedCmd := exec.Command(cmd, args...)
|
||||
preparedCmd.Dir = shell.Dir
|
||||
preparedCmd.Env = mergeEnv(os.Environ(), env)
|
||||
|
||||
if !enableLiveOutput {
|
||||
return Output(shell.Ctx, preparedCmd, shell.StripArgsValuesOnExitError, &logWriterGenerator{
|
||||
return Output(ctx, preparedCmd, shell.StripArgsValuesOnExitError, &logWriterGenerator{
|
||||
log: shell.Logger,
|
||||
})
|
||||
} else {
|
||||
return LiveOutput(shell.Ctx, preparedCmd, shell.StripArgsValuesOnExitError, os.Stdout)
|
||||
return LiveOutput(ctx, preparedCmd, shell.StripArgsValuesOnExitError, os.Stdout)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute a shell command
|
||||
// Execute a shell command with the given stdin; wrapped in an exec span like
|
||||
// Execute.
|
||||
func (shell ShellRunner) ExecuteStdIn(cmd string, args []string, env map[string]string, stdin io.Reader) ([]byte, error) {
|
||||
ctx, span, isHelm := startExecSpan(shell.Ctx, cmd, args)
|
||||
defer span.End()
|
||||
|
||||
start := time.Now()
|
||||
out, err := shell.runStdIn(ctx, cmd, args, env, stdin)
|
||||
finishExecSpan(span, isHelm, args, start, err)
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (shell ShellRunner) runStdIn(ctx context.Context, cmd string, args []string, env map[string]string, stdin io.Reader) ([]byte, error) {
|
||||
preparedCmd := exec.Command(cmd, args...)
|
||||
preparedCmd.Dir = shell.Dir
|
||||
preparedCmd.Env = mergeEnv(os.Environ(), env)
|
||||
preparedCmd.Stdin = stdin
|
||||
return Output(shell.Ctx, preparedCmd, shell.StripArgsValuesOnExitError, &logWriterGenerator{
|
||||
return Output(ctx, preparedCmd, shell.StripArgsValuesOnExitError, &logWriterGenerator{
|
||||
log: shell.Logger,
|
||||
})
|
||||
}
|
||||
@@ -187,9 +212,7 @@ func LiveOutput(ctx context.Context, c *exec.Cmd, stripArgsValuesOnExitError boo
|
||||
|
||||
func mergeEnv(orig []string, new map[string]string) []string {
|
||||
wanted := env2map(orig)
|
||||
for k, v := range new {
|
||||
wanted[k] = v
|
||||
}
|
||||
maps.Copy(wanted, new)
|
||||
return map2env(wanted)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
package helmexec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/telemetry"
|
||||
)
|
||||
|
||||
// startExecSpan starts one span per external process started by helmfile
|
||||
// (helm invocations, hooks, helmfile plugins). It is effectively free when
|
||||
// telemetry is disabled: telemetry.Tracer then returns the OTel no-op tracer.
|
||||
// The returned context derives from ctx (or Background when nil) and may be
|
||||
// used for the subprocess itself without changing cancellation semantics.
|
||||
// markHelmExec returns ctx (or Background when nil) stamped with the
|
||||
// helm-invocation marker used for span classification.
|
||||
func markHelmExec(ctx context.Context) context.Context {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
return context.WithValue(ctx, helmExecMarker{}, true)
|
||||
}
|
||||
|
||||
// withRunnerCtx returns a runner (value or pointer ShellRunner form) whose
|
||||
// context is transformed by f; non-ShellRunner runners pass through
|
||||
// unchanged. ShellRunner has value receivers, so both forms satisfy Runner.
|
||||
func withRunnerCtx(runner Runner, f func(context.Context) context.Context) Runner {
|
||||
switch shell := runner.(type) {
|
||||
case *ShellRunner:
|
||||
clone := *shell
|
||||
clone.Ctx = f(clone.Ctx)
|
||||
return &clone
|
||||
case ShellRunner:
|
||||
clone := shell
|
||||
clone.Ctx = f(clone.Ctx)
|
||||
return clone
|
||||
default:
|
||||
return runner
|
||||
}
|
||||
}
|
||||
|
||||
// markHelmRunner returns a runner whose context carries the helm-invocation
|
||||
// marker, so span classification and the helm duration metric do not rely on
|
||||
// the executable basename.
|
||||
func markHelmRunner(runner Runner) Runner {
|
||||
return withRunnerCtx(runner, markHelmExec)
|
||||
}
|
||||
|
||||
// spanAttachedContext returns a context that keeps runnerCtx's cancellation
|
||||
// chain but carries the span from spanCtx, so the subprocess span nests under
|
||||
// the caller's span while the subprocess itself stays governed by the
|
||||
// runner's own context (e.g. the kubedog safety valve). A nil runnerCtx falls
|
||||
// back to spanCtx.
|
||||
func spanAttachedContext(runnerCtx, spanCtx context.Context) context.Context {
|
||||
if runnerCtx == nil {
|
||||
return spanCtx
|
||||
}
|
||||
return trace.ContextWithSpan(runnerCtx, trace.SpanFromContext(spanCtx))
|
||||
}
|
||||
|
||||
// helmExecMarker marks contexts of invocations made through the execer
|
||||
// funnel, so span classification is authoritative even for wrapper binaries
|
||||
// whose name does not start with "helm".
|
||||
type helmExecMarker struct{}
|
||||
|
||||
func startExecSpan(ctx context.Context, cmd string, args []string) (context.Context, trace.Span, bool) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
name, attrs := classifyExec(ctx, cmd, args)
|
||||
ctx, span := telemetry.Tracer(telemetry.ScopeHelm).Start(ctx, name, trace.WithAttributes(attrs...))
|
||||
return ctx, span, name == helmExecSpanName
|
||||
}
|
||||
|
||||
// classifyExec builds the span name and attributes for one subprocess.
|
||||
// Secret-bearing arguments are always redacted with the strict profile: span
|
||||
// visibility must be at least as redacted as error messages (see redact.go).
|
||||
// helmExecSpanName is the single source of truth for what counts as a helm
|
||||
// invocation: the span name drives both classification and the helm duration
|
||||
// metric gate.
|
||||
const helmExecSpanName = "helm.exec"
|
||||
|
||||
func classifyExec(ctx context.Context, cmd string, args []string) (string, []attribute.KeyValue) {
|
||||
base := filepath.Base(cmd)
|
||||
isHelm := isHelmBinary(base)
|
||||
if marked, ok := ctx.Value(helmExecMarker{}).(bool); ok {
|
||||
isHelm = marked
|
||||
}
|
||||
attrs := []attribute.KeyValue{
|
||||
attribute.String("exec.command", base),
|
||||
}
|
||||
|
||||
redacted := RedactArgs(args, RedactionStrict)
|
||||
for i, a := range redacted {
|
||||
// Positional arguments can be chart/repository URLs with embedded
|
||||
// credentials (AddRepo, RegistryLogin, OCI and go-getter refs).
|
||||
redacted[i] = RedactedRef(a)
|
||||
}
|
||||
attrs = append(attrs, attribute.StringSlice("exec.args", redacted))
|
||||
if !equalArgs(args, redacted) {
|
||||
attrs = append(attrs, attribute.Bool("exec.redacted", true))
|
||||
}
|
||||
|
||||
if isHelm {
|
||||
if sub := helmSubcommand(args); sub != "" {
|
||||
attrs = append(attrs, attribute.String("helm.subcommand", sub))
|
||||
}
|
||||
return helmExecSpanName, attrs
|
||||
}
|
||||
return "os.exec", attrs
|
||||
}
|
||||
|
||||
// isHelmBinary is the FALLBACK classifier: a base name starting with "helm"
|
||||
// ("helm", "helm3", "helm-dev") for processes started outside the execer
|
||||
// funnels (e.g. the version probe in helmexec.New). Invocations through the
|
||||
// funnels are classified authoritatively by the helmExecMarker, so wrapper
|
||||
// --helm-binary names classify correctly too.
|
||||
func isHelmBinary(base string) bool {
|
||||
return strings.HasPrefix(base, "helm")
|
||||
}
|
||||
|
||||
// helmSubcommand returns the best-effort helm subcommand ("upgrade",
|
||||
// "repo", ...) from raw args. Bare flags are assumed to consume the next
|
||||
// argument as their value (true for the global --kube-context/--kubeconfig
|
||||
// helmfile prepends); a rare bare boolean flag before the subcommand can
|
||||
// misattribute the value — cosmetic only.
|
||||
func helmSubcommand(args []string) string {
|
||||
skipValue := false
|
||||
for _, a := range args {
|
||||
if skipValue {
|
||||
skipValue = false
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(a, "-") {
|
||||
skipValue = !strings.Contains(a, "=")
|
||||
continue
|
||||
}
|
||||
return a
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// finishExecSpan records a finished process's outcome on its span and, for
|
||||
// helm invocations (as classified by startExecSpan, wrapper binaries
|
||||
// included), the helmfile.helm.exec.duration metric.
|
||||
func finishExecSpan(span trace.Span, isHelm bool, args []string, start time.Time, err error) {
|
||||
if isHelm {
|
||||
telemetry.RecordHelmExecDuration(time.Since(start).Seconds(), helmSubcommand(args), err == nil)
|
||||
}
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if exitErr, ok := errors.AsType[ExitError](err); ok {
|
||||
span.SetAttributes(attribute.Int("exec.exit_code", exitErr.ExitStatus()))
|
||||
}
|
||||
// The raw error may embed command arguments and subprocess output; keep
|
||||
// the span description generic (the exit code is an attribute).
|
||||
span.SetStatus(codes.Error, "command failed")
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package helmexec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.opentelemetry.io/otel/trace/noop"
|
||||
commonpb "go.opentelemetry.io/proto/otlp/common/v1"
|
||||
v1 "go.opentelemetry.io/proto/otlp/trace/v1"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/telemetry"
|
||||
"github.com/helmfile/helmfile/pkg/telemetry/otlptest"
|
||||
)
|
||||
|
||||
// TestShellRunnerExecSpansExported drives ShellRunner.Execute through a real
|
||||
// OTLP export and asserts names, attributes, error status, and parent linkage
|
||||
// to the command span (i.e. the runner spans nest, never orphan).
|
||||
func TestShellRunnerExecSpansExported(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("uses the unix true/false binaries")
|
||||
}
|
||||
|
||||
rec := otlptest.NewRecorder(t)
|
||||
otlptest.SetupTelemetry(t, rec, "helmfile test")
|
||||
|
||||
shell := ShellRunner{
|
||||
Logger: NewLogger(io.Discard, "warn"),
|
||||
Ctx: telemetry.CommandContext(),
|
||||
}
|
||||
|
||||
out, err := shell.Execute("true", nil, nil, false)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, out)
|
||||
|
||||
// A failing command must surface its exit code and error status.
|
||||
_, err = shell.Execute("false", nil, nil, false)
|
||||
var exitErr ExitError
|
||||
require.ErrorAs(t, err, &exitErr)
|
||||
|
||||
// Secret-bearing arguments must be redacted on the span.
|
||||
_, err = shell.Execute("true", []string{"--set", "secret=1", "--set=also=2"}, nil, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
otlptest.ShutdownTelemetry(t)
|
||||
|
||||
spans := rec.Spans(t)
|
||||
root := otlptest.FindSpanWhere(t, spans, func(s *v1.Span) bool { return s.Name == "helmfile test" }, "command span")
|
||||
|
||||
success := otlptest.FindSpanWhere(t, spans, func(s *v1.Span) bool {
|
||||
cmd, ok := otlptest.AttrString(s, "exec.command")
|
||||
return s.Name == "os.exec" && ok && cmd == "true" && !otlptest.HasAttr(s, "exec.redacted")
|
||||
}, "success os.exec span")
|
||||
if success.Status != nil {
|
||||
assert.Equal(t, v1.Status_STATUS_CODE_UNSET, success.Status.Code, "success span must not be marked error")
|
||||
}
|
||||
|
||||
failure := otlptest.FindSpanWhere(t, spans, func(s *v1.Span) bool {
|
||||
cmd, ok := otlptest.AttrString(s, "exec.command")
|
||||
return ok && cmd == "false"
|
||||
}, "failure os.exec span")
|
||||
require.NotNil(t, failure.Status)
|
||||
assert.Equal(t, v1.Status_STATUS_CODE_ERROR, failure.Status.Code)
|
||||
assert.EqualValues(t, 1, spanAttr(t, failure, "exec.exit_code").Value.GetIntValue())
|
||||
|
||||
redactedSpan := otlptest.FindSpanWhere(t, spans, func(s *v1.Span) bool { return otlptest.HasAttr(s, "exec.redacted") }, "redacted os.exec span")
|
||||
assert.Equal(t, []string{"--set", redactedArg, "--set=" + redactedArg}, spanAttrStrings(t, redactedSpan, "exec.args"))
|
||||
|
||||
// Every os.exec span must be a child of the command span, proving the
|
||||
// runner-span nesting (no orphan traces).
|
||||
for _, s := range spans {
|
||||
if s.Name == "os.exec" {
|
||||
assert.Equal(t, root.TraceId, s.TraceId, "os.exec span must join the command trace")
|
||||
assert.Equal(t, root.SpanId, s.ParentSpanId, "os.exec span must nest under the command span")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func spanAttr(t *testing.T, span *v1.Span, key string) *commonpb.KeyValue {
|
||||
t.Helper()
|
||||
for _, kv := range span.Attributes {
|
||||
if kv.Key == key {
|
||||
return kv
|
||||
}
|
||||
}
|
||||
t.Fatalf("attribute %q not found on span %q", key, span.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func spanAttrStrings(t *testing.T, span *v1.Span, key string) []string {
|
||||
t.Helper()
|
||||
values := spanAttr(t, span, key).Value.GetArrayValue().GetValues()
|
||||
out := make([]string, 0, len(values))
|
||||
for _, v := range values {
|
||||
out = append(out, v.GetStringValue())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TestSpanAttachedContext pins the per-call context merge that lets helm
|
||||
// subprocess spans nest under per-release spans WITHOUT overriding the
|
||||
// runner's own cancellation context (the kubedog safety valve).
|
||||
func TestSpanAttachedContext(t *testing.T) {
|
||||
runnerCtx := t.Context()
|
||||
|
||||
spanCtx, span := noop.NewTracerProvider().Tracer("test").Start(context.Background(), "release")
|
||||
|
||||
merged := spanAttachedContext(runnerCtx, spanCtx)
|
||||
|
||||
// The merged context still cancels with the runner's context.
|
||||
assert.Equal(t, runnerCtx.Done(), merged.Done(), "cancellation authority must stay with the runner context")
|
||||
|
||||
// ...and carries the caller's span for nesting.
|
||||
assert.Equal(t, span, trace.SpanFromContext(merged), "the caller's span must be attached")
|
||||
|
||||
// A typed nil runner context (distinct from the nil literal, per
|
||||
// staticcheck) falls back to the span context.
|
||||
var nilRunnerCtx context.Context
|
||||
assert.Equal(t, spanCtx, spanAttachedContext(nilRunnerCtx, spanCtx))
|
||||
}
|
||||
|
||||
// TestMarkHelmRunnerCoversBothFunnels pins that the shared marker helper —
|
||||
// used by BOTH execWithRunner and execStdIn — stamps value and pointer
|
||||
// runners alike, so wrapper binaries stay classified as helm operations on
|
||||
// stdin-based invocations (registry login, repo add) too.
|
||||
func TestMarkHelmRunnerCoversBothFunnels(t *testing.T) {
|
||||
ptr := &ShellRunner{}
|
||||
if v, ok := markHelmRunner(ptr).(*ShellRunner).Ctx.Value(helmExecMarker{}).(bool); !ok || !v {
|
||||
t.Error("pointer runner must carry the helm marker")
|
||||
}
|
||||
|
||||
val := ShellRunner{}
|
||||
markedValue, ok := markHelmRunner(val).(ShellRunner)
|
||||
if !ok {
|
||||
t.Fatal("value runner must stay a value runner")
|
||||
}
|
||||
if v, ok := markedValue.Ctx.Value(helmExecMarker{}).(bool); !ok || !v {
|
||||
t.Error("value runner must carry the helm marker")
|
||||
}
|
||||
|
||||
fake := &mockRunner{}
|
||||
assert.Same(t, fake, markHelmRunner(fake), "non-ShellRunner runners pass through unchanged")
|
||||
}
|
||||
|
||||
// TestValueRunnerClassification pins that ShellRunner values (which satisfy
|
||||
// Runner via value receivers) receive the helm marker and span attachment
|
||||
// exactly like pointer runners.
|
||||
func TestValueRunnerClassification(t *testing.T) {
|
||||
spanCtx, span := noop.NewTracerProvider().Tracer("test").Start(context.Background(), "release")
|
||||
|
||||
// marker stamping
|
||||
valueRunner := ShellRunner{}
|
||||
marked := markHelmExec(valueRunner.Ctx)
|
||||
if v, ok := marked.Value(helmExecMarker{}).(bool); !ok || !v {
|
||||
t.Fatal("value runner ctx must carry the helm marker")
|
||||
}
|
||||
|
||||
// span attachment via the same path execWithContext uses for values
|
||||
merged := spanAttachedContext(valueRunner.Ctx, spanCtx)
|
||||
assert.Equal(t, span, trace.SpanFromContext(merged))
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package kubedog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/werf/kubedog/pkg/informer"
|
||||
kdutil "github.com/werf/kubedog/pkg/trackers/dyntracker/util"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
type podLogCutoff struct {
|
||||
uid types.UID
|
||||
time time.Time
|
||||
}
|
||||
|
||||
// podLogFilter remembers the first Ready transition independently of kubedog's
|
||||
// workload readiness. It only filters output; pod tracking and log capture continue.
|
||||
type podLogFilter struct {
|
||||
mu sync.RWMutex
|
||||
cutoffs map[string]podLogCutoff
|
||||
}
|
||||
|
||||
func newPodLogFilter() *podLogFilter {
|
||||
return &podLogFilter{cutoffs: make(map[string]podLogCutoff)}
|
||||
}
|
||||
|
||||
// newStartupLogFilter returns a readiness-tracking log filter when log
|
||||
// streaming is limited to startup output, or nil otherwise.
|
||||
func newStartupLogFilter(options *TrackOptions, factory *kdutil.Concurrent[*informer.InformerFactory], targets []trackTarget) (*podLogFilter, error) {
|
||||
if options == nil || !options.Logs || !options.LogsUntilReady {
|
||||
return nil, nil
|
||||
}
|
||||
filter := newPodLogFilter()
|
||||
if err := filter.watch(factory, targets); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
// cutoffTrackedKind reports whether pods of the target kind are subject to
|
||||
// readiness-based log cutoff. Jobs are excluded: they can be Ready while
|
||||
// still executing.
|
||||
func cutoffTrackedKind(kind string) bool {
|
||||
switch kind {
|
||||
case "deploy", "sts", "ds":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// watchNamespacePods attaches the readiness handler to the shared pod
|
||||
// informer for namespace and starts it.
|
||||
func (f *podLogFilter) watchNamespacePods(factory *kdutil.Concurrent[*informer.InformerFactory], gvr schema.GroupVersionResource, namespace string) error {
|
||||
var podInformer *kdutil.Concurrent[*informer.Informer]
|
||||
err := factory.RWTransactionErr(func(factory *informer.InformerFactory) error {
|
||||
informer, err := factory.ForNamespace(gvr, namespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create pod log informer: %w", err)
|
||||
}
|
||||
podInformer = informer
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return podInformer.RWTransactionErr(func(inf *informer.Informer) error {
|
||||
_, err := inf.AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: f.update,
|
||||
UpdateFunc: func(_, obj any) {
|
||||
f.update(obj)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("watch pod readiness for logs: %w", err)
|
||||
}
|
||||
inf.Run()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (f *podLogFilter) watch(factory *kdutil.Concurrent[*informer.InformerFactory], targets []trackTarget) error {
|
||||
podGVR := schema.GroupVersionResource{Version: "v1", Resource: "pods"}
|
||||
namespaces := make(map[string]struct{})
|
||||
for _, target := range targets {
|
||||
if !cutoffTrackedKind(target.kind) {
|
||||
continue
|
||||
}
|
||||
if _, watched := namespaces[target.namespace]; watched {
|
||||
continue
|
||||
}
|
||||
if err := f.watchNamespacePods(factory, podGVR, target.namespace); err != nil {
|
||||
return err
|
||||
}
|
||||
namespaces[target.namespace] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *podLogFilter) update(obj any) {
|
||||
pod, ok := obj.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
id := kdutil.ResourceID(pod.GetName(), pod.GetNamespace(), watchdogPodGVK)
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
// Keep the first Ready transition recorded for this pod instance. A
|
||||
// replacement pod reusing the name starts fresh; delete on a missing key
|
||||
// is a no-op.
|
||||
if cutoff, found := f.cutoffs[id]; found && cutoff.uid == pod.GetUID() {
|
||||
return
|
||||
}
|
||||
delete(f.cutoffs, id)
|
||||
// Job pods can be Ready while still executing; keep their complete output.
|
||||
for _, owner := range pod.GetOwnerReferences() {
|
||||
if owner.Kind == "Job" {
|
||||
return
|
||||
}
|
||||
}
|
||||
conditions, _, err := unstructured.NestedSlice(pod.Object, "status", "conditions")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, raw := range conditions {
|
||||
condition, ok := raw.(map[string]any)
|
||||
if !ok || condition["type"] != "Ready" || condition["status"] != "True" {
|
||||
continue
|
||||
}
|
||||
timestamp, _ := condition["lastTransitionTime"].(string)
|
||||
readyTime, err := time.Parse(time.RFC3339Nano, timestamp)
|
||||
if err != nil {
|
||||
readyTime = time.Now()
|
||||
} else if readyTime.Nanosecond() == 0 {
|
||||
// Kubernetes rounds Ready timestamps to seconds. Preserve startup
|
||||
// lines within that second too, even when delivered after readiness.
|
||||
readyTime = readyTime.Add(time.Second - time.Nanosecond)
|
||||
}
|
||||
f.cutoffs[id] = podLogCutoff{uid: pod.GetUID(), time: readyTime}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (f *podLogFilter) cutoff(name, namespace string, gvk schema.GroupVersionKind) time.Time {
|
||||
if f == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
f.mu.RLock()
|
||||
defer f.mu.RUnlock()
|
||||
return f.cutoffs[kdutil.ResourceID(name, namespace, gvk)].time
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package kubedog
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/werf/kubedog/pkg/informer"
|
||||
kdutil "github.com/werf/kubedog/pkg/trackers/dyntracker/util"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/dynamic/fake"
|
||||
clienttesting "k8s.io/client-go/testing"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
func newLogFilterPod(name, namespace, uid, readyStatus string, readyTime time.Time) *unstructured.Unstructured {
|
||||
pod := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": map[string]any{"name": name, "namespace": namespace, "uid": uid},
|
||||
"status": map[string]any{"phase": "Running"},
|
||||
}}
|
||||
if readyStatus != "" {
|
||||
condition := map[string]any{"type": "Ready", "status": readyStatus}
|
||||
if !readyTime.IsZero() {
|
||||
condition["lastTransitionTime"] = readyTime.Format(time.RFC3339Nano)
|
||||
}
|
||||
pod.Object["status"].(map[string]any)["conditions"] = []any{condition}
|
||||
}
|
||||
return pod
|
||||
}
|
||||
|
||||
func TestPodLogFilter_ReadyCondition(t *testing.T) {
|
||||
readyTime := time.Date(2026, 10, 1, 12, 0, 0, 123, time.UTC)
|
||||
for _, status := range []string{"", "False", "Unknown", "True"} {
|
||||
t.Run("Ready="+status, func(t *testing.T) {
|
||||
filter := newPodLogFilter()
|
||||
filter.update(newLogFilterPod("app", "ns", "uid", status, readyTime))
|
||||
cutoff := filter.cutoff("app", "ns", watchdogPodGVK)
|
||||
if status == "True" {
|
||||
assert.Equal(t, readyTime, cutoff)
|
||||
} else {
|
||||
assert.True(t, cutoff.IsZero(), "Running alone does not mean that a pod is ready")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPodLogFilter_FirstReadyTransitionIsRetained(t *testing.T) {
|
||||
filter := newPodLogFilter()
|
||||
firstReady := time.Date(2026, 10, 1, 12, 0, 0, 123, time.UTC)
|
||||
filter.update(newLogFilterPod("app", "ns", "uid", "True", firstReady))
|
||||
filter.update(newLogFilterPod("app", "ns", "uid", "False", firstReady.Add(time.Minute)))
|
||||
filter.update(newLogFilterPod("app", "ns", "uid", "True", firstReady.Add(2*time.Minute)))
|
||||
assert.Equal(t, firstReady, filter.cutoff("app", "ns", watchdogPodGVK))
|
||||
}
|
||||
|
||||
func TestPodLogFilter_JobPodsAreExempt(t *testing.T) {
|
||||
filter := newPodLogFilter()
|
||||
pod := newLogFilterPod("job-pod", "ns", "uid", "True", time.Now())
|
||||
pod.SetOwnerReferences([]metav1.OwnerReference{{APIVersion: "batch/v1", Kind: "Job", Name: "job"}})
|
||||
filter.update(pod)
|
||||
assert.True(t, filter.cutoff("job-pod", "ns", watchdogPodGVK).IsZero())
|
||||
}
|
||||
|
||||
func TestPodLogFilter_ReplacementPodStartsLoggingAgain(t *testing.T) {
|
||||
filter := newPodLogFilter()
|
||||
firstReady := time.Date(2026, 10, 1, 12, 0, 0, 123, time.UTC)
|
||||
filter.update(newLogFilterPod("app", "ns", "old-uid", "True", firstReady))
|
||||
filter.update(newLogFilterPod("app", "ns", "new-uid", "False", time.Time{}))
|
||||
assert.True(t, filter.cutoff("app", "ns", watchdogPodGVK).IsZero())
|
||||
|
||||
nextReady := firstReady.Add(time.Minute)
|
||||
filter.update(newLogFilterPod("app", "ns", "new-uid", "True", nextReady))
|
||||
assert.Equal(t, nextReady, filter.cutoff("app", "ns", watchdogPodGVK))
|
||||
}
|
||||
|
||||
func TestPodLogFilter_NamespacesAreIndependent(t *testing.T) {
|
||||
filter := newPodLogFilter()
|
||||
readyTime := time.Date(2026, 10, 1, 12, 0, 0, 123, time.UTC)
|
||||
filter.update(newLogFilterPod("app", "ready-ns", "uid", "True", readyTime))
|
||||
filter.update(newLogFilterPod("app", "starting-ns", "uid", "False", time.Time{}))
|
||||
assert.Equal(t, readyTime, filter.cutoff("app", "ready-ns", watchdogPodGVK))
|
||||
assert.True(t, filter.cutoff("app", "starting-ns", watchdogPodGVK).IsZero())
|
||||
}
|
||||
|
||||
func TestPodLogFilter_PreservesReadinessSecond(t *testing.T) {
|
||||
filter := newPodLogFilter()
|
||||
readyTime := time.Date(2026, 10, 1, 12, 0, 0, 0, time.UTC)
|
||||
filter.update(newLogFilterPod("app", "ns", "uid", "True", readyTime))
|
||||
assert.Equal(t, readyTime.Add(time.Second-time.Nanosecond), filter.cutoff("app", "ns", watchdogPodGVK))
|
||||
}
|
||||
|
||||
func TestPodLogFilter_MissingOrInvalidTransitionTime(t *testing.T) {
|
||||
for _, timestamp := range []string{"", "invalid"} {
|
||||
t.Run(timestamp, func(t *testing.T) {
|
||||
filter := newPodLogFilter()
|
||||
pod := newLogFilterPod("app", "ns", "uid", "True", time.Time{})
|
||||
conditions := pod.Object["status"].(map[string]any)["conditions"].([]any)
|
||||
conditions[0].(map[string]any)["lastTransitionTime"] = timestamp
|
||||
before := time.Now()
|
||||
filter.update(pod)
|
||||
after := time.Now()
|
||||
cutoff := filter.cutoff("app", "ns", watchdogPodGVK)
|
||||
assert.False(t, cutoff.Before(before))
|
||||
assert.False(t, cutoff.After(after))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPodLogFilter_WatchesExistingPodsAndUpdatesUsingSharedInformer(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
podGVR := schema.GroupVersionResource{Version: "v1", Resource: "pods"}
|
||||
readyTime := time.Date(2026, 10, 1, 12, 0, 0, 123, time.UTC)
|
||||
readyPod := newLogFilterPod("ready", "ns", "ready-uid", "True", readyTime)
|
||||
startingPod := newLogFilterPod("starting", "ns", "starting-uid", "False", time.Time{})
|
||||
client := fake.NewSimpleDynamicClientWithCustomListKinds(
|
||||
runtime.NewScheme(), map[schema.GroupVersionResource]string{podGVR: "PodList"}, readyPod, startingPod,
|
||||
)
|
||||
var watches atomic.Int32
|
||||
client.PrependWatchReactor("pods", func(clienttesting.Action) (bool, watch.Interface, error) {
|
||||
watches.Add(1)
|
||||
return false, nil, nil
|
||||
})
|
||||
factory := informer.NewConcurrentInformerFactory(ctx.Done(), make(chan error, 1), client,
|
||||
informer.ConcurrentInformerFactoryOptions{})
|
||||
|
||||
// Start another consumer first, as kubedog does during resource tracking.
|
||||
var shared *kdutil.Concurrent[*informer.Informer]
|
||||
var err error
|
||||
factory.RWTransaction(func(factory *informer.InformerFactory) {
|
||||
shared, err = factory.ForNamespace(podGVR, "ns")
|
||||
})
|
||||
require.NoError(t, err)
|
||||
shared.RWTransaction(func(inf *informer.Informer) {
|
||||
_, err = inf.AddEventHandler(cache.ResourceEventHandlerFuncs{})
|
||||
if err == nil {
|
||||
inf.Run()
|
||||
}
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
filter := newPodLogFilter()
|
||||
require.NoError(t, filter.watch(factory, []trackTarget{
|
||||
{kind: "deploy", namespace: "ns"},
|
||||
{kind: "sts", namespace: "ns"},
|
||||
{kind: "job", namespace: "jobs"},
|
||||
{kind: "pvc", namespace: "storage"},
|
||||
{kind: "canary", namespace: "canaries"},
|
||||
}))
|
||||
require.Eventually(t, func() bool {
|
||||
return filter.cutoff("ready", "ns", watchdogPodGVK).Equal(readyTime)
|
||||
}, 5*time.Second, 10*time.Millisecond, "initial add must retain the pod's historical readiness time")
|
||||
assert.True(t, filter.cutoff("starting", "ns", watchdogPodGVK).IsZero())
|
||||
|
||||
nextReady := readyTime.Add(time.Minute)
|
||||
startingPod = newLogFilterPod("starting", "ns", "starting-uid", "True", nextReady)
|
||||
_, err = client.Resource(podGVR).Namespace("ns").Update(ctx, startingPod, metav1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Eventually(t, func() bool {
|
||||
return filter.cutoff("starting", "ns", watchdogPodGVK).Equal(nextReady)
|
||||
}, 5*time.Second, 10*time.Millisecond, "updates must stop logs independently of the other pod")
|
||||
assert.EqualValues(t, 1, watches.Load(), "consumers and repeated targets must share one pod watch")
|
||||
}
|
||||
|
||||
func TestCutoffTrackedKind(t *testing.T) {
|
||||
for kind, want := range map[string]bool{
|
||||
"deploy": true,
|
||||
"sts": true,
|
||||
"ds": true,
|
||||
"job": false,
|
||||
"pvc": false,
|
||||
"canary": false,
|
||||
} {
|
||||
t.Run(kind, func(t *testing.T) {
|
||||
assert.Equal(t, want, cutoffTrackedKind(kind))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewStartupLogFilter(t *testing.T) {
|
||||
// Every combination below must yield a nil filter without touching the
|
||||
// informer factory: startup-only cutoffs apply solely to full streaming.
|
||||
nilFilterCases := []struct {
|
||||
name string
|
||||
options *TrackOptions
|
||||
}{
|
||||
{name: "nil options", options: nil},
|
||||
{name: "no log streaming", options: NewTrackOptions().WithLogsUntilReady(true)},
|
||||
{name: "streaming without cutoff", options: NewTrackOptions().WithLogs(true)},
|
||||
{name: "failed-only mode", options: NewTrackOptions().WithFailedLogsOnly(true).WithLogsUntilReady(true)},
|
||||
}
|
||||
for _, tt := range nilFilterCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
filter, err := newStartupLogFilter(tt.options, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, filter)
|
||||
})
|
||||
}
|
||||
}
|
||||
+30
-8
@@ -26,18 +26,27 @@ const (
|
||||
TrackModeKubedog TrackMode = "kubedog"
|
||||
)
|
||||
|
||||
// defaultLogsInterval is how often the printer flushes captured logs when
|
||||
// TrackOptions.LogsInterval is not set (zero).
|
||||
const defaultLogsInterval = 10 * time.Second
|
||||
|
||||
type TrackOptions struct {
|
||||
Timeout time.Duration
|
||||
// Logs enables emitting logs for every pod kubedog observes.
|
||||
Logs bool
|
||||
// LogsUntilReady limits Logs to startup output through each pod's first
|
||||
// Ready transition. Job logs are unaffected. Has no effect unless Logs is true.
|
||||
LogsUntilReady bool
|
||||
// FailedLogsOnly enables capturing logs in the background and emitting
|
||||
// them only for pods that enter a failed state (CrashLoopBackOff, Error,
|
||||
// ImagePullBackOff, etc.). Has no effect when Logs is true.
|
||||
FailedLogsOnly bool
|
||||
LogsSince time.Duration
|
||||
Filter *resource.FilterConfig
|
||||
QPS float32
|
||||
Burst int
|
||||
// LogsInterval controls how often captured logs are printed.
|
||||
LogsInterval time.Duration
|
||||
LogsSince time.Duration
|
||||
Filter *resource.FilterConfig
|
||||
QPS float32
|
||||
Burst int
|
||||
// Baselines holds the pre-change state of each resource keyed by
|
||||
// "Kind/Namespace/Name". When set, the tracker delays attaching kubedog
|
||||
// to a resource until its UID changes or its generation increments past
|
||||
@@ -51,10 +60,11 @@ type TrackOptions struct {
|
||||
|
||||
func NewTrackOptions() *TrackOptions {
|
||||
return &TrackOptions{
|
||||
Timeout: 5 * time.Minute,
|
||||
LogsSince: 10 * time.Minute,
|
||||
QPS: 100,
|
||||
Burst: 200,
|
||||
Timeout: 5 * time.Minute,
|
||||
LogsInterval: defaultLogsInterval,
|
||||
LogsSince: 10 * time.Minute,
|
||||
QPS: 100,
|
||||
Burst: 200,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +78,18 @@ func (o *TrackOptions) WithLogs(logs bool) *TrackOptions {
|
||||
return o
|
||||
}
|
||||
|
||||
// WithLogsInterval sets how often captured logs are printed.
|
||||
func (o *TrackOptions) WithLogsInterval(interval time.Duration) *TrackOptions {
|
||||
o.LogsInterval = interval
|
||||
return o
|
||||
}
|
||||
|
||||
// WithLogsUntilReady limits pod log output to startup when log streaming is enabled.
|
||||
func (o *TrackOptions) WithLogsUntilReady(v bool) *TrackOptions {
|
||||
o.LogsUntilReady = v
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *TrackOptions) WithFilterConfig(config *resource.FilterConfig) *TrackOptions {
|
||||
o.Filter = config
|
||||
return o
|
||||
|
||||
+20
-15
@@ -3,6 +3,7 @@ package kubedog
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -18,7 +19,6 @@ import (
|
||||
|
||||
const (
|
||||
progressInterval = 10 * time.Second
|
||||
logsInterval = 10 * time.Second
|
||||
heartbeatInterval = 2 * time.Minute
|
||||
)
|
||||
|
||||
@@ -60,7 +60,7 @@ func HeaderDividerStyled(title string, useColor bool) string {
|
||||
// trailing padding on left-aligned last columns.
|
||||
func TableVisualWidth(tableStr string) int {
|
||||
max := 0
|
||||
for _, line := range strings.Split(tableStr, "\n") {
|
||||
for line := range strings.SplitSeq(tableStr, "\n") {
|
||||
line = strings.TrimRight(line, " ")
|
||||
w := runewidth.StringWidth(line)
|
||||
if w > max {
|
||||
@@ -136,9 +136,7 @@ func (g *gateStatuses) snapshot() map[string]string {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
out := make(map[string]string, len(g.m))
|
||||
for k, v := range g.m {
|
||||
out[k] = v
|
||||
}
|
||||
maps.Copy(out, g.m)
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -189,9 +187,11 @@ type progressPrinter struct {
|
||||
logStore *kdutil.Concurrent[*logstore.LogStore]
|
||||
skipLogs bool
|
||||
failedLogsOnly bool
|
||||
podLogFilter *podLogFilter
|
||||
gates *gateStatuses
|
||||
skipped *skippedKeys
|
||||
useColor bool
|
||||
logsInterval time.Duration
|
||||
startTime time.Time
|
||||
// lastEmit tracks the most recent time we printed *anything* to the
|
||||
// logger (progress block or log stream). The heartbeat ticker uses it to
|
||||
@@ -207,6 +207,9 @@ type progressPrinter struct {
|
||||
lastLogSource string
|
||||
}
|
||||
|
||||
// newProgressPrinter builds the printer that renders progress blocks, log
|
||||
// streams, and heartbeats. logsInterval sets the log-flush cadence; a zero
|
||||
// value falls back to defaultLogsInterval.
|
||||
func newProgressPrinter(
|
||||
logger *zap.SugaredLogger,
|
||||
releaseName string,
|
||||
@@ -217,7 +220,11 @@ func newProgressPrinter(
|
||||
gates *gateStatuses,
|
||||
skipped *skippedKeys,
|
||||
useColor bool,
|
||||
logsInterval time.Duration,
|
||||
) *progressPrinter {
|
||||
if logsInterval <= 0 {
|
||||
logsInterval = defaultLogsInterval
|
||||
}
|
||||
return &progressPrinter{
|
||||
logger: logger,
|
||||
releaseName: releaseName,
|
||||
@@ -228,6 +235,7 @@ func newProgressPrinter(
|
||||
gates: gates,
|
||||
skipped: skipped,
|
||||
useColor: useColor,
|
||||
logsInterval: logsInterval,
|
||||
startTime: time.Now(),
|
||||
lastEmit: time.Now(),
|
||||
lastStatus: make(map[string]string),
|
||||
@@ -238,7 +246,7 @@ func newProgressPrinter(
|
||||
func (p *progressPrinter) run(ctx context.Context, done <-chan struct{}) {
|
||||
progressTicker := time.NewTicker(progressInterval)
|
||||
defer progressTicker.Stop()
|
||||
logsTicker := time.NewTicker(logsInterval)
|
||||
logsTicker := time.NewTicker(p.logsInterval)
|
||||
defer logsTicker.Stop()
|
||||
heartbeatTicker := time.NewTicker(heartbeatInterval)
|
||||
defer heartbeatTicker.Stop()
|
||||
@@ -249,9 +257,6 @@ func (p *progressPrinter) run(ctx context.Context, done <-chan struct{}) {
|
||||
select {
|
||||
case <-progressTicker.C:
|
||||
p.flushProgress()
|
||||
if !p.skipLogs {
|
||||
p.flushLogs()
|
||||
}
|
||||
case <-logsTicker.C:
|
||||
if !p.skipLogs {
|
||||
p.flushLogs()
|
||||
@@ -259,10 +264,6 @@ func (p *progressPrinter) run(ctx context.Context, done <-chan struct{}) {
|
||||
case <-heartbeatTicker.C:
|
||||
p.flushHeartbeat()
|
||||
case <-done:
|
||||
p.flushProgress()
|
||||
if !p.skipLogs {
|
||||
p.flushLogs()
|
||||
}
|
||||
return
|
||||
case <-ctx.Done():
|
||||
return
|
||||
@@ -327,8 +328,8 @@ func (p *progressPrinter) statusColor(status, parentKind string) string {
|
||||
}
|
||||
|
||||
head := status
|
||||
if idx := strings.Index(status, " "); idx >= 0 {
|
||||
head = status[:idx]
|
||||
if before, _, ok := strings.Cut(status, " "); ok {
|
||||
head = before
|
||||
}
|
||||
switch head {
|
||||
case "ready":
|
||||
@@ -726,6 +727,7 @@ func (p *progressPrinter) flushLogs() {
|
||||
return
|
||||
}
|
||||
}
|
||||
cutoff := p.podLogFilter.cutoff(rl.Name(), rl.Namespace(), rl.GroupVersionKind())
|
||||
for source, lines := range rl.LogLines() {
|
||||
cursorKey := resourceKey + "|" + source
|
||||
start := p.lastCounts[cursorKey]
|
||||
@@ -733,6 +735,9 @@ func (p *progressPrinter) flushLogs() {
|
||||
continue
|
||||
}
|
||||
for _, ll := range lines[start:] {
|
||||
if !cutoff.IsZero() && ll.Time.After(cutoff) {
|
||||
continue
|
||||
}
|
||||
pending = append(pending, entry{
|
||||
resourceKey: resourceKey,
|
||||
displayLabel: displayLabel,
|
||||
|
||||
+161
-23
@@ -15,6 +15,7 @@ import (
|
||||
kdutil "github.com/werf/kubedog/pkg/trackers/dyntracker/util"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
@@ -211,7 +212,7 @@ func TestFlushProgress_HidesStaleReadyPodWithoutAttr(t *testing.T) {
|
||||
taskStore.RWTransaction(func(s *statestore.TaskStore) { s.AddReadinessTaskState(taskC) })
|
||||
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, true /*skipLogs*/, false, newGateStatuses(), newSkippedKeys(), false)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, true /*skipLogs*/, false, newGateStatuses(), newSkippedKeys(), false, 0)
|
||||
p.flushProgress()
|
||||
|
||||
out := capturedOutput(buf, mu)
|
||||
@@ -238,7 +239,7 @@ func TestFlushProgress_PreReadyPhaseAttributeOverridesReady(t *testing.T) {
|
||||
})
|
||||
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false, 0)
|
||||
p.flushProgress()
|
||||
|
||||
out := capturedOutput(buf, mu)
|
||||
@@ -266,7 +267,7 @@ func TestFlushProgress_KeepsNamespacesWhenReleaseSpansMultiple(t *testing.T) {
|
||||
})
|
||||
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "vray", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false)
|
||||
p := newProgressPrinter(logger, "vray", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false, 0)
|
||||
p.flushProgress()
|
||||
out := capturedOutput(buf, mu)
|
||||
|
||||
@@ -292,7 +293,7 @@ func TestFlushProgress_HidesEmptyNameChildPlaceholder(t *testing.T) {
|
||||
})
|
||||
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false, 0)
|
||||
p.flushProgress()
|
||||
|
||||
out := capturedOutput(buf, mu)
|
||||
@@ -314,7 +315,7 @@ func TestFlushProgress_HeaderUsesReleaseName(t *testing.T) {
|
||||
})
|
||||
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "vray", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false)
|
||||
p := newProgressPrinter(logger, "vray", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false, 0)
|
||||
p.flushProgress()
|
||||
out := capturedOutput(buf, mu)
|
||||
assert.Contains(t, out, "Release 'vray' progress (",
|
||||
@@ -334,7 +335,7 @@ func TestFlushProgress_HeaderFallsBackWithoutReleaseName(t *testing.T) {
|
||||
})
|
||||
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false, 0)
|
||||
p.flushProgress()
|
||||
out := capturedOutput(buf, mu)
|
||||
assert.Contains(t, out, "kubedog progress (")
|
||||
@@ -356,7 +357,7 @@ func TestFlushProgress_NestingAndAlignment(t *testing.T) {
|
||||
})
|
||||
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false, 0)
|
||||
p.flushProgress()
|
||||
|
||||
out := capturedOutput(buf, mu)
|
||||
@@ -392,7 +393,7 @@ func TestFlushProgress_RespectsGateAndSkip(t *testing.T) {
|
||||
skips.add(kdutil.ResourceID("skipped", "ns", deploymentGVK))
|
||||
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, true, false, gates, skips, false)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, true, false, gates, skips, false, 0)
|
||||
p.flushProgress()
|
||||
|
||||
out := capturedOutput(buf, mu)
|
||||
@@ -420,7 +421,7 @@ func TestFlushLogs_FailedOnlyMode_GatesUnchanged(t *testing.T) {
|
||||
addPodLogs(t, logStore, "init-bad", "bad line 1", "panic: boom")
|
||||
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, false /*skipLogs*/, true /*failedLogsOnly*/, newGateStatuses(), newSkippedKeys(), false)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, false /*skipLogs*/, true /*failedLogsOnly*/, newGateStatuses(), newSkippedKeys(), false, 0)
|
||||
p.flushLogs()
|
||||
|
||||
out := capturedOutput(buf, mu)
|
||||
@@ -445,7 +446,7 @@ func TestFlushLogs_FailedOnlyMode_DoesNotAdvanceCursorForSuccess(t *testing.T) {
|
||||
addPodLogs(t, logStore, "flaky-pod", "early line A", "early line B")
|
||||
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, false, true, newGateStatuses(), newSkippedKeys(), false)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, false, true, newGateStatuses(), newSkippedKeys(), false, 0)
|
||||
|
||||
p.flushLogs()
|
||||
assert.NotContains(t, capturedOutput(buf, mu), "early line",
|
||||
@@ -474,7 +475,7 @@ func TestFlushLogs_AllPodsMode_EmitsEverything(t *testing.T) {
|
||||
addPodLogs(t, logStore, "init-bad", "bad line 1")
|
||||
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, false, false /*failedLogsOnly=off*/, newGateStatuses(), newSkippedKeys(), false)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, false, false /*failedLogsOnly=off*/, newGateStatuses(), newSkippedKeys(), false, 0)
|
||||
p.flushLogs()
|
||||
|
||||
out := capturedOutput(buf, mu)
|
||||
@@ -493,7 +494,7 @@ func TestFlushLogs_HeaderDedupedAcrossFlushes(t *testing.T) {
|
||||
})
|
||||
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, false, false, newGateStatuses(), newSkippedKeys(), false)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, false, false, newGateStatuses(), newSkippedKeys(), false, 0)
|
||||
|
||||
addPodLogs(t, logStore, "app-pod", "line 1", "line 2")
|
||||
p.flushLogs()
|
||||
@@ -511,6 +512,94 @@ func TestFlushLogs_HeaderDedupedAcrossFlushes(t *testing.T) {
|
||||
assert.Contains(t, out, "line 4")
|
||||
}
|
||||
|
||||
func TestFlushLogs_LogsUntilReady(t *testing.T) {
|
||||
for _, kind := range []string{"Deployment", "StatefulSet", "DaemonSet"} {
|
||||
t.Run(kind, func(t *testing.T) {
|
||||
taskStore := kdutil.NewConcurrent(statestore.NewTaskStore())
|
||||
logStore := kdutil.NewConcurrent(logstore.NewLogStore())
|
||||
gvk := schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: kind}
|
||||
ts := statestore.NewReadinessTaskState("app", "ns", gvk, statestore.ReadinessTaskStateOptions{})
|
||||
// The real dyntracker leaves individual pod states unknown until
|
||||
// the entire workload is ready, even when a replica is already Ready.
|
||||
addPodChild(t, ts, "app-ready", "ns", statestore.ResourceStatusUnknown, "Running")
|
||||
addPodChild(t, ts, "app-starting", "ns", statestore.ResourceStatusUnknown, "Running")
|
||||
taskStore.RWTransaction(func(s *statestore.TaskStore) {
|
||||
s.AddReadinessTaskState(kdutil.NewConcurrent(ts))
|
||||
})
|
||||
|
||||
readyTime := time.Unix(100, 500)
|
||||
filter := newPodLogFilter()
|
||||
filter.update(newLogFilterPod("app-ready", "ns", "ready-uid", "True", readyTime))
|
||||
filter.update(newLogFilterPod("app-starting", "ns", "starting-uid", "False", readyTime))
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, false, false, newGateStatuses(), newSkippedKeys(), false, 0)
|
||||
p.podLogFilter = filter
|
||||
|
||||
readyLogs := kdutil.NewConcurrent(logstore.NewResourceLogs("app-ready", "ns", podGVK))
|
||||
startingLogs := kdutil.NewConcurrent(logstore.NewResourceLogs("app-starting", "ns", podGVK))
|
||||
logStore.RWTransaction(func(s *logstore.LogStore) {
|
||||
s.AddResourceLogs(readyLogs)
|
||||
s.AddResourceLogs(startingLogs)
|
||||
})
|
||||
readyLogs.RWTransaction(func(rl *logstore.ResourceLogs) {
|
||||
rl.AddLogLine("successful startup", "container/main", readyTime.Add(-time.Second))
|
||||
rl.AddLogLine("at readiness", "container/main", readyTime)
|
||||
rl.AddLogLine("HTTP request", "container/main", readyTime.Add(time.Second))
|
||||
})
|
||||
startingLogs.RWTransaction(func(rl *logstore.ResourceLogs) {
|
||||
rl.AddLogLine("still starting", "container/main", readyTime.Add(time.Second))
|
||||
})
|
||||
p.flushLogs()
|
||||
|
||||
// Late pre-readiness chunks from another container still flush;
|
||||
// repeated traffic from the ready replica stays silent.
|
||||
readyLogs.RWTransaction(func(rl *logstore.ResourceLogs) {
|
||||
rl.AddLogLine("late startup chunk", "container/sidecar", readyTime.Add(-time.Millisecond))
|
||||
rl.AddLogLine("background job", "container/sidecar", readyTime.Add(time.Second))
|
||||
rl.AddLogLine("another HTTP request", "container/main", readyTime.Add(2*time.Second))
|
||||
})
|
||||
p.flushLogs()
|
||||
out := capturedOutput(buf, mu)
|
||||
assert.Contains(t, out, "successful startup")
|
||||
assert.Contains(t, out, "at readiness")
|
||||
assert.Contains(t, out, "late startup chunk")
|
||||
assert.Contains(t, out, "still starting")
|
||||
assert.NotContains(t, out, "HTTP request")
|
||||
assert.NotContains(t, out, "background job")
|
||||
assert.Equal(t, 4, p.lastCounts["Pod/ns/app-ready|container/main"])
|
||||
assert.Equal(t, 2, p.lastCounts["Pod/ns/app-ready|container/sidecar"])
|
||||
p.flushLogs()
|
||||
assert.Equal(t, out, capturedOutput(buf, mu), "startup lines must not be duplicated")
|
||||
|
||||
// Filtering logs must not mutate readiness or hide a later failure.
|
||||
assert.NotEqual(t, statestore.ReadinessTaskStatusReady, ts.Status())
|
||||
flipPodPhase(t, ts, "app-ready", "ns", "CrashLoopBackOff")
|
||||
p.flushProgress()
|
||||
assert.Contains(t, capturedOutput(buf, mu), "CrashLoopBackOff")
|
||||
assert.Contains(t, p.collectFailedPodIDs(), kdutil.ResourceID("app-ready", "ns", podGVK))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlushLogs_LogsUntilReady_JobLogs(t *testing.T) {
|
||||
taskStore := kdutil.NewConcurrent(statestore.NewTaskStore())
|
||||
logStore := kdutil.NewConcurrent(logstore.NewLogStore())
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, false, false, newGateStatuses(), newSkippedKeys(), false, 0)
|
||||
p.podLogFilter = newPodLogFilter()
|
||||
pod := newLogFilterPod("job-pod", "ns", "job-uid", "True", time.Unix(0, 0))
|
||||
pod.SetOwnerReferences([]metav1.OwnerReference{{Kind: "Job", Name: "job"}})
|
||||
p.podLogFilter.update(pod)
|
||||
addPodLogs(t, logStore, "job-pod", "job started", "job still running")
|
||||
p.flushLogs()
|
||||
addPodLogs(t, logStore, "job-pod", "job completed")
|
||||
p.flushLogs()
|
||||
out := capturedOutput(buf, mu)
|
||||
assert.Contains(t, out, "job started")
|
||||
assert.Contains(t, out, "job still running")
|
||||
assert.Contains(t, out, "job completed")
|
||||
}
|
||||
|
||||
func TestHeaderDivider_FormatsBothBorders(t *testing.T) {
|
||||
assert.Equal(t, "========== title: ==========", HeaderDivider("title:"))
|
||||
}
|
||||
@@ -625,7 +714,7 @@ func TestFlushHeartbeat_EmitsWhenIdleWithInFlight(t *testing.T) {
|
||||
})
|
||||
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "vray", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false)
|
||||
p := newProgressPrinter(logger, "vray", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false, 0)
|
||||
// Simulate the silent gap: pretend we last printed long enough ago that
|
||||
// the heartbeat ticker would now fire.
|
||||
p.lastEmit = time.Now().Add(-3 * heartbeatInterval)
|
||||
@@ -664,7 +753,7 @@ func TestFlushHeartbeat_LabelsGatedTasksAsWaiting(t *testing.T) {
|
||||
gates.set(BaselineKey("job", "ns", "queued-job"), "waiting for update (uid=abc gen=1)")
|
||||
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "vray", taskStore, logStore, true, false, gates, newSkippedKeys(), false)
|
||||
p := newProgressPrinter(logger, "vray", taskStore, logStore, true, false, gates, newSkippedKeys(), false, 0)
|
||||
p.lastEmit = time.Now().Add(-3 * heartbeatInterval)
|
||||
|
||||
p.flushHeartbeat()
|
||||
@@ -705,7 +794,7 @@ func TestFlushHeartbeat_ProgressingItemsListedBeforeWaiting(t *testing.T) {
|
||||
gates.set(BaselineKey("job", "ns", "bbb-queued"), "waiting for update (uid=b gen=1)")
|
||||
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "vray", taskStore, logStore, true, false, gates, newSkippedKeys(), false)
|
||||
p := newProgressPrinter(logger, "vray", taskStore, logStore, true, false, gates, newSkippedKeys(), false, 0)
|
||||
p.lastEmit = time.Now().Add(-3 * heartbeatInterval)
|
||||
|
||||
p.flushHeartbeat()
|
||||
@@ -738,7 +827,7 @@ func TestFlushHeartbeat_AllWaitingShowsCountOnly(t *testing.T) {
|
||||
}
|
||||
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "vray", taskStore, logStore, true, false, gates, newSkippedKeys(), false)
|
||||
p := newProgressPrinter(logger, "vray", taskStore, logStore, true, false, gates, newSkippedKeys(), false, 0)
|
||||
p.lastEmit = time.Now().Add(-3 * heartbeatInterval)
|
||||
|
||||
p.flushHeartbeat()
|
||||
@@ -768,7 +857,7 @@ func TestFlushHeartbeat_DoesNotUpdateLastEmit(t *testing.T) {
|
||||
})
|
||||
|
||||
logger, _, _ := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "vray", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false)
|
||||
p := newProgressPrinter(logger, "vray", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false, 0)
|
||||
pinned := time.Now().Add(-3 * heartbeatInterval)
|
||||
p.lastEmit = pinned
|
||||
|
||||
@@ -789,7 +878,7 @@ func TestFlushHeartbeat_SuppressedWhenRecentEmit(t *testing.T) {
|
||||
})
|
||||
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "vray", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false)
|
||||
p := newProgressPrinter(logger, "vray", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false, 0)
|
||||
// lastEmit is current — we just printed something, so the heartbeat
|
||||
// should stay quiet.
|
||||
p.lastEmit = time.Now()
|
||||
@@ -816,7 +905,7 @@ func TestFlushHeartbeat_SuppressedWhenAllReady(t *testing.T) {
|
||||
})
|
||||
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "vray", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false)
|
||||
p := newProgressPrinter(logger, "vray", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false, 0)
|
||||
p.lastEmit = time.Now().Add(-3 * heartbeatInterval)
|
||||
|
||||
p.flushHeartbeat()
|
||||
@@ -841,7 +930,7 @@ func TestFlushHeartbeat_CapsResourceListAndShowsOverflow(t *testing.T) {
|
||||
}
|
||||
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "vray", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false)
|
||||
p := newProgressPrinter(logger, "vray", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false, 0)
|
||||
p.lastEmit = time.Now().Add(-3 * heartbeatInterval)
|
||||
|
||||
p.flushHeartbeat()
|
||||
@@ -855,12 +944,12 @@ func TestFlushHeartbeat_CapsResourceListAndShowsOverflow(t *testing.T) {
|
||||
"progressing items beyond the cap must be summarized by overflow count, not listed")
|
||||
}
|
||||
|
||||
func TestProgressPrinter_FullRun_CancelsAndDrainsOnContextDone(t *testing.T) {
|
||||
// Smoke test that the run loop terminates cleanly on context cancel.
|
||||
func TestProgressPrinter_FullRun_StopsOnContextDone(t *testing.T) {
|
||||
// The tracker performs the final drain after this loop has stopped.
|
||||
taskStore := kdutil.NewConcurrent(statestore.NewTaskStore())
|
||||
logStore := kdutil.NewConcurrent(logstore.NewLogStore())
|
||||
logger, _, _ := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, true, false, newGateStatuses(), newSkippedKeys(), false, 0)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
@@ -877,6 +966,55 @@ func TestProgressPrinter_FullRun_CancelsAndDrainsOnContextDone(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProgressPrinter_RunFlushesLogsAtConfiguredInterval(t *testing.T) {
|
||||
taskStore := kdutil.NewConcurrent(statestore.NewTaskStore())
|
||||
logStore := kdutil.NewConcurrent(logstore.NewLogStore())
|
||||
ts := statestore.NewReadinessTaskState("app", "ns", deploymentGVK, statestore.ReadinessTaskStateOptions{})
|
||||
taskStore.RWTransaction(func(s *statestore.TaskStore) {
|
||||
s.AddReadinessTaskState(kdutil.NewConcurrent(ts))
|
||||
})
|
||||
|
||||
logger, buf, mu := newBufferedLogger(t)
|
||||
p := newProgressPrinter(logger, "", taskStore, logStore, false, false, newGateStatuses(), newSkippedKeys(), false, 20*time.Millisecond)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
returned := make(chan struct{})
|
||||
go func() {
|
||||
p.run(ctx, done)
|
||||
close(returned)
|
||||
}()
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
select {
|
||||
case <-returned:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Error("printer.run did not return after ctx cancel")
|
||||
}
|
||||
})
|
||||
|
||||
addPodLogs(t, logStore, "app-pod", "interval-line-one")
|
||||
require.Eventually(t, func() bool {
|
||||
return strings.Contains(capturedOutput(buf, mu), "interval-line-one")
|
||||
}, 2*time.Second, 5*time.Millisecond, "first line should appear before tracking finishes")
|
||||
|
||||
// Seeing the second line proves that another logs tick has occurred.
|
||||
addPodLogs(t, logStore, "app-pod", "interval-line-two")
|
||||
require.Eventually(t, func() bool {
|
||||
return strings.Contains(capturedOutput(buf, mu), "interval-line-two")
|
||||
}, 2*time.Second, 5*time.Millisecond)
|
||||
|
||||
out := capturedOutput(buf, mu)
|
||||
assert.Equal(t, 1, strings.Count(out, "interval-line-one"), "a later tick must not repeat earlier lines")
|
||||
assert.Equal(t, 1, strings.Count(out, "interval-line-two"))
|
||||
assert.Equal(t, 1, strings.Count(out, "kubedog progress ("), "log ticks must not print extra progress blocks")
|
||||
select {
|
||||
case <-returned:
|
||||
t.Fatal("printer.run returned before tracking finished")
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// addPodLogs appends log lines to a pod's ResourceLogs entry in the log
|
||||
// store, creating the entry if needed. Uses an incrementing timestamp so
|
||||
// the printer's chronological sort is deterministic.
|
||||
|
||||
+17
-8
@@ -429,15 +429,11 @@ func (t *Tracker) TrackResources(ctx context.Context, resources []*resource.Reso
|
||||
}()
|
||||
|
||||
captureLogsFromTime := time.Now().Add(-t.trackOptions.LogsSince)
|
||||
// Capture logs whenever either flag wants them. The kubedog tracker
|
||||
// records logs into the logStore unconditionally; the failed-only mode
|
||||
// is enforced at print time by the printer.
|
||||
// Capture logs for either output mode. Failed-only filtering happens in
|
||||
// the printer, after kubedog has populated the log store.
|
||||
wantLogsCapture := t.trackOptions.Logs || t.trackOptions.FailedLogsOnly
|
||||
ignoreLogs := !wantLogsCapture
|
||||
// failedLogsOnly takes effect only when full streaming isn't already on.
|
||||
failedLogsOnly := t.trackOptions.FailedLogsOnly && !t.trackOptions.Logs
|
||||
// skipLogsInPrinter mutes the printer entirely if neither mode is on.
|
||||
skipLogsInPrinter := !wantLogsCapture
|
||||
|
||||
type trackerEntry struct {
|
||||
target trackTarget
|
||||
@@ -456,7 +452,13 @@ func (t *Tracker) TrackResources(ctx context.Context, resources []*resource.Reso
|
||||
}
|
||||
|
||||
gateStatuses := newGateStatuses()
|
||||
printer := newProgressPrinter(t.logger, t.releaseName, taskStore, logStore, skipLogsInPrinter, failedLogsOnly, gateStatuses, t.skipped, t.trackOptions.Color)
|
||||
printer := newProgressPrinter(t.logger, t.releaseName, taskStore, logStore, ignoreLogs,
|
||||
failedLogsOnly, gateStatuses, t.skipped, t.trackOptions.Color, t.trackOptions.LogsInterval)
|
||||
podLogFilter, err := newStartupLogFilter(t.trackOptions, informerFactory, targets)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printer.podLogFilter = podLogFilter
|
||||
printerDone := make(chan struct{})
|
||||
|
||||
// Spawn a parallel failure watchdog. It catches pods that genuinely
|
||||
@@ -553,7 +555,7 @@ func (t *Tracker) TrackResources(ctx context.Context, resources []*resource.Reso
|
||||
if t.releaseName != "" {
|
||||
releaseLabel = fmt.Sprintf("release '%s'", t.releaseName)
|
||||
}
|
||||
t.logger.Warnf("kubedog tracker stopped for %s: %v — helmfile will continue waiting for helm to finish; no more progress/log output until then",
|
||||
t.logger.Warnf("kubedog tracker stopped for %s: %v — helmfile will continue waiting for helm to finish; no more periodic progress/log output until then",
|
||||
releaseLabel, err)
|
||||
cancel()
|
||||
case <-trackersDone:
|
||||
@@ -564,6 +566,13 @@ func (t *Tracker) TrackResources(ctx context.Context, resources []*resource.Reso
|
||||
cancel()
|
||||
<-trackersDone
|
||||
<-printerDone
|
||||
// Flush after all producers and the periodic printer have stopped. This
|
||||
// guarantees the last lines are printed even if cancellation wins the
|
||||
// printer's select over trackersDone.
|
||||
printer.flushProgress()
|
||||
if !ignoreLogs {
|
||||
printer.flushLogs()
|
||||
}
|
||||
|
||||
// Drain remaining tracker errors so they're surfaced in logs.
|
||||
close(errCh)
|
||||
|
||||
@@ -32,7 +32,9 @@ func TestNewTrackOptions(t *testing.T) {
|
||||
assert.NotNil(t, opts)
|
||||
assert.Equal(t, 5*time.Minute, opts.Timeout)
|
||||
assert.Equal(t, false, opts.Logs)
|
||||
assert.False(t, opts.LogsUntilReady)
|
||||
assert.Equal(t, 10*time.Minute, opts.LogsSince)
|
||||
assert.Equal(t, 10*time.Second, opts.LogsInterval)
|
||||
}
|
||||
|
||||
func TestTrackOptions_WithTimeout(t *testing.T) {
|
||||
@@ -49,6 +51,19 @@ func TestTrackOptions_WithLogs(t *testing.T) {
|
||||
assert.True(t, opts.Logs)
|
||||
}
|
||||
|
||||
func TestTrackOptions_WithLogsInterval(t *testing.T) {
|
||||
opts := NewTrackOptions()
|
||||
opts = opts.WithLogsInterval(3 * time.Second)
|
||||
|
||||
assert.Equal(t, 3*time.Second, opts.LogsInterval)
|
||||
}
|
||||
|
||||
func TestTrackOptions_WithLogsUntilReady(t *testing.T) {
|
||||
opts := NewTrackOptions().WithLogs(true).WithLogsUntilReady(true)
|
||||
assert.True(t, opts.LogsUntilReady)
|
||||
assert.True(t, opts.Logs)
|
||||
}
|
||||
|
||||
func TestTrackOptions_Chaining(t *testing.T) {
|
||||
opts := NewTrackOptions()
|
||||
opts = opts.
|
||||
@@ -375,7 +390,8 @@ func TestTrackOptions_Chaining_AllSetters(t *testing.T) {
|
||||
WithQPS(50).
|
||||
WithBurst(80).
|
||||
WithColor(true).
|
||||
WithBaselines(baselines)
|
||||
WithBaselines(baselines).
|
||||
WithLogsInterval(30 * time.Second)
|
||||
|
||||
assert.Equal(t, 2*time.Minute, opts.Timeout)
|
||||
assert.True(t, opts.Logs)
|
||||
@@ -385,6 +401,7 @@ func TestTrackOptions_Chaining_AllSetters(t *testing.T) {
|
||||
assert.Equal(t, 80, opts.Burst)
|
||||
assert.True(t, opts.Color)
|
||||
assert.Equal(t, baselines, opts.Baselines)
|
||||
assert.Equal(t, 30*time.Second, opts.LogsInterval)
|
||||
}
|
||||
|
||||
// makeObj builds an *unstructured.Unstructured with the given path/value
|
||||
|
||||
@@ -3,6 +3,7 @@ package kubedog
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/werf/kubedog/pkg/trackers/dyntracker/statestore"
|
||||
@@ -167,10 +168,8 @@ func (t *Tracker) scanForMissedFailures(ctx context.Context, taskStore *kdutil.C
|
||||
// "RS UIDs owned by this Deployment" so we only LIST RSes when we actually
|
||||
// need to disambiguate.
|
||||
func podOwnedBy(pod *unstructured.Unstructured, workloadUID types.UID, workloadKind string, deploymentRSUIDs func() map[string]struct{}) bool {
|
||||
for _, uid := range podOwnerUIDs(pod) {
|
||||
if uid == workloadUID {
|
||||
return true
|
||||
}
|
||||
if slices.Contains(podOwnerUIDs(pod), workloadUID) {
|
||||
return true
|
||||
}
|
||||
if workloadKind != "deploy" {
|
||||
return false
|
||||
@@ -206,11 +205,8 @@ func (t *Tracker) rsUIDsOwnedBy(ctx context.Context, namespace string, workloadU
|
||||
out := map[string]struct{}{}
|
||||
for i := range rsList.Items {
|
||||
rs := &rsList.Items[i]
|
||||
for _, uid := range podOwnerUIDs(rs) {
|
||||
if uid == workloadUID {
|
||||
out[string(rs.GetUID())] = struct{}{}
|
||||
break
|
||||
}
|
||||
if slices.Contains(podOwnerUIDs(rs), workloadUID) {
|
||||
out[string(rs.GetUID())] = struct{}{}
|
||||
}
|
||||
}
|
||||
return out
|
||||
|
||||
+9
-13
@@ -2,6 +2,7 @@ package maputil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -263,16 +264,14 @@ func typedVal(val string, st bool) any {
|
||||
}
|
||||
|
||||
// MergeMaps merges two maps with special handling for nested maps and arrays.
|
||||
func MergeMaps(a, b map[string]interface{}, opts ...MergeOptions) map[string]interface{} {
|
||||
func MergeMaps(a, b map[string]any, opts ...MergeOptions) map[string]any {
|
||||
arrayStrategy := ArrayMergeStrategySparse
|
||||
if len(opts) > 0 {
|
||||
arrayStrategy = opts[0].ArrayStrategy
|
||||
}
|
||||
|
||||
out := make(map[string]interface{}, len(a))
|
||||
for k, v := range a {
|
||||
out[k] = v
|
||||
}
|
||||
out := make(map[string]any, len(a))
|
||||
maps.Copy(out, a)
|
||||
for k, v := range b {
|
||||
if v == nil {
|
||||
// If key doesn't exist in base, add nil (issue #1154).
|
||||
@@ -282,9 +281,9 @@ func MergeMaps(a, b map[string]interface{}, opts ...MergeOptions) map[string]int
|
||||
}
|
||||
continue
|
||||
}
|
||||
if v, ok := v.(map[string]interface{}); ok {
|
||||
if v, ok := v.(map[string]any); ok {
|
||||
if bv, ok := out[k]; ok {
|
||||
if bv, ok := bv.(map[string]interface{}); ok {
|
||||
if bv, ok := bv.(map[string]any); ok {
|
||||
out[k] = MergeMaps(bv, v, opts...)
|
||||
continue
|
||||
}
|
||||
@@ -365,15 +364,12 @@ func mergeSlices(base, override []any, strategy ArrayMergeStrategy) []any {
|
||||
}
|
||||
|
||||
// Merge element-by-element (for ArrayMergeStrategyMerge or sparse arrays)
|
||||
maxLen := len(base)
|
||||
if len(override) > maxLen {
|
||||
maxLen = len(override)
|
||||
}
|
||||
maxLen := max(len(override), len(base))
|
||||
|
||||
result := make([]interface{}, maxLen)
|
||||
result := make([]any, maxLen)
|
||||
copy(result, base)
|
||||
|
||||
for i := 0; i < len(override); i++ {
|
||||
for i := range override {
|
||||
overrideVal := override[i]
|
||||
if overrideVal == nil {
|
||||
continue
|
||||
|
||||
+36
-36
@@ -195,27 +195,27 @@ func typedValueTest(t *testing.T, input string, expectedWhenNoStr any) {
|
||||
}
|
||||
|
||||
func TestMapUtil_MergeMaps(t *testing.T) {
|
||||
map1 := map[string]interface{}{
|
||||
map1 := map[string]any{
|
||||
"debug": true,
|
||||
}
|
||||
map2 := map[string]interface{}{
|
||||
map2 := map[string]any{
|
||||
"logLevel": "info",
|
||||
"replicaCount": 3,
|
||||
}
|
||||
map3 := map[string]interface{}{
|
||||
map3 := map[string]any{
|
||||
"logLevel": "info",
|
||||
"replicaCount": map[string]any{
|
||||
"app1": 3,
|
||||
"awesome": 4,
|
||||
},
|
||||
}
|
||||
map4 := map[string]interface{}{
|
||||
map4 := map[string]any{
|
||||
"logLevel": "info",
|
||||
"replicaCount": map[string]any{
|
||||
"app1": 3,
|
||||
},
|
||||
}
|
||||
map5 := map[string]interface{}{
|
||||
map5 := map[string]any{
|
||||
"logLevel": "error",
|
||||
"replicaCount": nil,
|
||||
}
|
||||
@@ -239,7 +239,7 @@ func TestMapUtil_MergeMaps(t *testing.T) {
|
||||
}
|
||||
|
||||
testMap = MergeMaps(map1, map3)
|
||||
expectedMap := map[string]interface{}{
|
||||
expectedMap := map[string]any{
|
||||
"debug": true,
|
||||
"logLevel": "info",
|
||||
"replicaCount": map[string]any{
|
||||
@@ -253,7 +253,7 @@ func TestMapUtil_MergeMaps(t *testing.T) {
|
||||
}
|
||||
|
||||
testMap = MergeMaps(map3, map5)
|
||||
expectedMap = map[string]interface{}{
|
||||
expectedMap = map[string]any{
|
||||
"logLevel": "error",
|
||||
"replicaCount": map[string]any{
|
||||
"app1": 3,
|
||||
@@ -478,7 +478,7 @@ func TestMapUtil_Issue2281_EmptyMapScenario(t *testing.T) {
|
||||
func TestMapUtil_Issue2281_MergeArrays(t *testing.T) {
|
||||
t.Run("merging sparse arrays should preserve elements from base that aren't in override", func(t *testing.T) {
|
||||
// Base values from helmfile
|
||||
base := map[string]interface{}{
|
||||
base := map[string]any{
|
||||
"top": map[string]any{
|
||||
"array": []any{"thing1", "thing2"},
|
||||
},
|
||||
@@ -486,7 +486,7 @@ func TestMapUtil_Issue2281_MergeArrays(t *testing.T) {
|
||||
|
||||
// Override values from --state-values-set top.array[1]=cmdlinething1
|
||||
// This creates a sparse array with nil at index 0
|
||||
override := map[string]interface{}{
|
||||
override := map[string]any{
|
||||
"top": map[string]any{
|
||||
"array": []any{nil, "cmdlinething1"},
|
||||
},
|
||||
@@ -506,7 +506,7 @@ func TestMapUtil_Issue2281_MergeArrays(t *testing.T) {
|
||||
|
||||
t.Run("complete arrays without nils should replace entirely (layer behavior)", func(t *testing.T) {
|
||||
// Base values from helmfile
|
||||
base := map[string]interface{}{
|
||||
base := map[string]any{
|
||||
"top": map[string]any{
|
||||
"array": []any{"thing1", "thing2", "thing3"},
|
||||
},
|
||||
@@ -514,7 +514,7 @@ func TestMapUtil_Issue2281_MergeArrays(t *testing.T) {
|
||||
|
||||
// Override values from environment YAML (complete array, no nils)
|
||||
// This should REPLACE the base array entirely
|
||||
override := map[string]interface{}{
|
||||
override := map[string]any{
|
||||
"top": map[string]any{
|
||||
"array": []any{"override1"},
|
||||
},
|
||||
@@ -533,7 +533,7 @@ func TestMapUtil_Issue2281_MergeArrays(t *testing.T) {
|
||||
|
||||
t.Run("merging complex arrays should preserve non-overridden elements and fields", func(t *testing.T) {
|
||||
// Base values from helmfile
|
||||
base := map[string]interface{}{
|
||||
base := map[string]any{
|
||||
"top": map[string]any{
|
||||
"complexArray": []any{
|
||||
map[string]any{
|
||||
@@ -549,7 +549,7 @@ func TestMapUtil_Issue2281_MergeArrays(t *testing.T) {
|
||||
}
|
||||
|
||||
// Override values from --state-values-set top.complexArray[1].anotherThing=cmdline
|
||||
override := map[string]interface{}{
|
||||
override := map[string]any{
|
||||
"top": map[string]any{
|
||||
"complexArray": []any{
|
||||
nil,
|
||||
@@ -588,7 +588,7 @@ func TestMapUtil_Issue2281_MergeArrays(t *testing.T) {
|
||||
|
||||
t.Run("complete issue #2281 scenario with MergeMaps - sparse arrays", func(t *testing.T) {
|
||||
// Base values from helmfile
|
||||
base := map[string]interface{}{
|
||||
base := map[string]any{
|
||||
"top": map[string]any{
|
||||
"array": []any{"thing1", "thing2"},
|
||||
"complexArray": []any{
|
||||
@@ -607,7 +607,7 @@ func TestMapUtil_Issue2281_MergeArrays(t *testing.T) {
|
||||
// Override values from:
|
||||
// --state-values-set top.array[1]=cmdlinething1 (creates sparse array with nil at 0)
|
||||
// --state-values-set top.complexArray[1].anotherThing=cmdline
|
||||
override := map[string]interface{}{
|
||||
override := map[string]any{
|
||||
"top": map[string]any{
|
||||
"array": []any{nil, "cmdlinething1"}, // Sparse array - nil at index 0
|
||||
"complexArray": []any{
|
||||
@@ -651,10 +651,10 @@ func TestMapUtil_Issue2281_MergeArrays(t *testing.T) {
|
||||
func TestMergeMaps_ArrayStrategies(t *testing.T) {
|
||||
t.Run("ArrayMergeStrategyReplace should replace arrays entirely - fixes #2353", func(t *testing.T) {
|
||||
// This simulates layer value overriding where outer layer array should replace inner
|
||||
base := map[string]interface{}{
|
||||
base := map[string]any{
|
||||
"array": []any{"inner1", "inner2", "inner3"},
|
||||
}
|
||||
override := map[string]interface{}{
|
||||
override := map[string]any{
|
||||
"array": []any{"outer1", "outer2"},
|
||||
}
|
||||
|
||||
@@ -671,10 +671,10 @@ func TestMergeMaps_ArrayStrategies(t *testing.T) {
|
||||
|
||||
t.Run("ArrayMergeStrategySparse (default) should merge element-by-element - preserves #2281 fix", func(t *testing.T) {
|
||||
// This simulates --state-values-set which creates sparse arrays
|
||||
base := map[string]interface{}{
|
||||
base := map[string]any{
|
||||
"array": []any{"base1", "base2", "base3"},
|
||||
}
|
||||
override := map[string]interface{}{
|
||||
override := map[string]any{
|
||||
"array": []any{nil, "override2"}, // Has nil = sparse array from CLI
|
||||
}
|
||||
|
||||
@@ -691,10 +691,10 @@ func TestMergeMaps_ArrayStrategies(t *testing.T) {
|
||||
|
||||
t.Run("Auto-detect: complete array (no nils) replaces base entirely", func(t *testing.T) {
|
||||
// Array without nils is detected as "complete" (layer value) and replaces entirely
|
||||
base := map[string]interface{}{
|
||||
base := map[string]any{
|
||||
"array": []any{"base1", "base2", "base3"},
|
||||
}
|
||||
override := map[string]interface{}{
|
||||
override := map[string]any{
|
||||
"array": []any{"override1"}, // Single element, no nils
|
||||
}
|
||||
|
||||
@@ -711,10 +711,10 @@ func TestMergeMaps_ArrayStrategies(t *testing.T) {
|
||||
|
||||
t.Run("Auto-detect: sparse array (with nils) preserves base at nil indices", func(t *testing.T) {
|
||||
// Array with nils is detected as "sparse" (CLI value) and merges element-by-element
|
||||
base := map[string]interface{}{
|
||||
base := map[string]any{
|
||||
"array": []any{"base1", "base2", "base3"},
|
||||
}
|
||||
override := map[string]interface{}{
|
||||
override := map[string]any{
|
||||
"array": []any{nil, nil, "override3"}, // Has nils at indices 0, 1
|
||||
}
|
||||
|
||||
@@ -730,13 +730,13 @@ func TestMergeMaps_ArrayStrategies(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("nested maps in sparse arrays should merge recursively", func(t *testing.T) {
|
||||
base := map[string]interface{}{
|
||||
base := map[string]any{
|
||||
"complexArray": []any{
|
||||
map[string]any{"field1": "a", "field2": "b"},
|
||||
map[string]any{"field1": "c", "field2": "d"},
|
||||
},
|
||||
}
|
||||
override := map[string]interface{}{
|
||||
override := map[string]any{
|
||||
"complexArray": []any{
|
||||
nil, // Skip index 0
|
||||
map[string]any{"field2": "override"}, // Only override field2 at index 1
|
||||
@@ -761,14 +761,14 @@ func TestMergeMaps_ArrayStrategies(t *testing.T) {
|
||||
|
||||
t.Run("Replace strategy: array of maps replaced entirely - layer scenario #2353", func(t *testing.T) {
|
||||
// This is the key scenario from #2353: outer layer defining complete array of objects
|
||||
base := map[string]interface{}{
|
||||
base := map[string]any{
|
||||
"releases": []any{
|
||||
map[string]any{"name": "inner-release-1", "chart": "inner-chart-1"},
|
||||
map[string]any{"name": "inner-release-2", "chart": "inner-chart-2"},
|
||||
map[string]any{"name": "inner-release-3", "chart": "inner-chart-3"},
|
||||
},
|
||||
}
|
||||
override := map[string]interface{}{
|
||||
override := map[string]any{
|
||||
"releases": []any{
|
||||
map[string]any{"name": "outer-release-1", "chart": "outer-chart-1"},
|
||||
map[string]any{"name": "outer-release-2", "chart": "outer-chart-2"},
|
||||
@@ -796,10 +796,10 @@ func TestMergeMaps_ArrayStrategies(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Replace strategy: empty override array replaces base", func(t *testing.T) {
|
||||
base := map[string]interface{}{
|
||||
base := map[string]any{
|
||||
"array": []any{"a", "b", "c"},
|
||||
}
|
||||
override := map[string]interface{}{
|
||||
override := map[string]any{
|
||||
"array": []any{}, // Empty array
|
||||
}
|
||||
|
||||
@@ -814,10 +814,10 @@ func TestMergeMaps_ArrayStrategies(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Auto-detect: empty override replaces base (no nils means complete)", func(t *testing.T) {
|
||||
base := map[string]interface{}{
|
||||
base := map[string]any{
|
||||
"array": []any{"a", "b", "c"},
|
||||
}
|
||||
override := map[string]interface{}{
|
||||
override := map[string]any{
|
||||
"array": []any{}, // Empty array - has no nils, detected as complete
|
||||
}
|
||||
|
||||
@@ -833,14 +833,14 @@ func TestMergeMaps_ArrayStrategies(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("strategies propagate to nested maps", func(t *testing.T) {
|
||||
base := map[string]interface{}{
|
||||
base := map[string]any{
|
||||
"outer": map[string]any{
|
||||
"inner": []any{"a", "b", "c"},
|
||||
},
|
||||
}
|
||||
|
||||
// With Replace strategy - explicit replacement
|
||||
overrideComplete := map[string]interface{}{
|
||||
overrideComplete := map[string]any{
|
||||
"outer": map[string]any{
|
||||
"inner": []any{"x", "y"}, // Complete array (no nils)
|
||||
},
|
||||
@@ -856,7 +856,7 @@ func TestMergeMaps_ArrayStrategies(t *testing.T) {
|
||||
}
|
||||
|
||||
// With auto-detection and sparse array (has nils)
|
||||
overrideSparse := map[string]interface{}{
|
||||
overrideSparse := map[string]any{
|
||||
"outer": map[string]any{
|
||||
"inner": []any{"x", "y", nil}, // Sparse array - has nil at index 2
|
||||
},
|
||||
@@ -875,10 +875,10 @@ func TestMergeMaps_ArrayStrategies(t *testing.T) {
|
||||
t.Run("ArrayMergeStrategyMerge always merges element-by-element (CLI index 0 case)", func(t *testing.T) {
|
||||
// This tests the CLI scenario: --state-values-set array[0]=value
|
||||
// Creates array ["value"] with NO nils, but should still merge
|
||||
base := map[string]interface{}{
|
||||
base := map[string]any{
|
||||
"array": []any{"base0", "base1", "base2"},
|
||||
}
|
||||
override := map[string]interface{}{
|
||||
override := map[string]any{
|
||||
"array": []any{"override0"}, // Single element, no nils - from CLI index 0
|
||||
}
|
||||
|
||||
|
||||
+111
-2
@@ -1,9 +1,12 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -18,9 +21,101 @@ const (
|
||||
valsCacheSize = 512
|
||||
)
|
||||
|
||||
var instance *vals.Runtime
|
||||
var instance vals.Evaluator
|
||||
var mu sync.Mutex
|
||||
|
||||
// ErrValsDisabled is returned by the evaluator when HELMFILE_DISABLE_VALS_STRICT is set
|
||||
// and a `ref+` expression is encountered.
|
||||
var ErrValsDisabled = errors.New("vals is disabled via HELMFILE_DISABLE_VALS_STRICT environment variable")
|
||||
|
||||
// refPlusRegexp mirrors the reference syntax understood by the vals library
|
||||
// (`ref+<provider>://...` and `secretref+<provider>://...`), so that disabled-vals
|
||||
// modes only report values that vals itself would have tried to resolve.
|
||||
var refPlusRegexp = regexp.MustCompile(`(secret)?ref\+[^\s+:]*://`)
|
||||
|
||||
// passthroughEvaluator passes values through unchanged (for external vals)
|
||||
type passthroughEvaluator struct{}
|
||||
|
||||
func (p *passthroughEvaluator) Eval(m map[string]any) (map[string]any, error) {
|
||||
return normalizeMap(m), nil
|
||||
}
|
||||
|
||||
// strictEvaluator passes through values but errors if ref+ is detected
|
||||
type strictEvaluator struct{}
|
||||
|
||||
func (s *strictEvaluator) Eval(m map[string]any) (map[string]any, error) {
|
||||
if containsRefPlus(m) {
|
||||
return nil, ErrValsDisabled
|
||||
}
|
||||
return normalizeMap(m), nil
|
||||
}
|
||||
|
||||
// normalizeMap converts []string values to []any to match vals.Eval behavior.
|
||||
func normalizeMap(m map[string]any) map[string]any {
|
||||
out := make(map[string]any, len(m))
|
||||
for k, v := range m {
|
||||
out[k] = normalizeValue(v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// normalizeValue recursively converts []string to []any and map[any]any to
|
||||
// map[string]any, matching the type normalization performed by vals.Eval.
|
||||
func normalizeValue(v any) any {
|
||||
switch typed := v.(type) {
|
||||
case map[string]any:
|
||||
return normalizeMap(typed)
|
||||
case map[any]any:
|
||||
strmap := make(map[string]any, len(typed))
|
||||
for k, v := range typed {
|
||||
strmap[fmt.Sprintf("%v", k)] = normalizeValue(v)
|
||||
}
|
||||
return strmap
|
||||
case []any:
|
||||
a := make([]any, len(typed))
|
||||
for i, e := range typed {
|
||||
a[i] = normalizeValue(e)
|
||||
}
|
||||
return a
|
||||
case []string:
|
||||
a := make([]any, len(typed))
|
||||
for i, s := range typed {
|
||||
a[i] = s
|
||||
}
|
||||
return a
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func containsRefPlus(v any) bool {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return refPlusRegexp.MatchString(val)
|
||||
case map[string]any:
|
||||
for _, v := range val {
|
||||
if containsRefPlus(v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case map[any]any:
|
||||
for _, v := range val {
|
||||
if containsRefPlus(v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case []any:
|
||||
if slices.ContainsFunc(val, containsRefPlus) {
|
||||
return true
|
||||
}
|
||||
case []string:
|
||||
if slices.ContainsFunc(val, refPlusRegexp.MatchString) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func buildValsOptions() (vals.Options, error) {
|
||||
// Configure AWS SDK logging via HELMFILE_AWS_SDK_LOG_LEVEL environment variable
|
||||
// Default: "off" to prevent sensitive information (tokens, auth headers) from being exposed
|
||||
@@ -78,7 +173,7 @@ func buildValsOptions() (vals.Options, error) {
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func ValsInstance() (*vals.Runtime, error) {
|
||||
func ValsInstance() (vals.Evaluator, error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
@@ -86,6 +181,20 @@ func ValsInstance() (*vals.Runtime, error) {
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// HELMFILE_DISABLE_VALS_STRICT: error on ref+ usage
|
||||
strict, _ := strconv.ParseBool(os.Getenv(envvar.DisableValsStrict))
|
||||
if strict {
|
||||
instance = &strictEvaluator{}
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// HELMFILE_DISABLE_VALS: pass-through for external vals
|
||||
disabled, _ := strconv.ParseBool(os.Getenv(envvar.DisableVals))
|
||||
if disabled {
|
||||
instance = &passthroughEvaluator{}
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
opts, err := buildValsOptions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+332
-11
@@ -2,6 +2,7 @@ package plugins
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/helmfile/vals"
|
||||
@@ -11,20 +12,347 @@ import (
|
||||
"github.com/helmfile/helmfile/pkg/envvar"
|
||||
)
|
||||
|
||||
func TestValsInstance(t *testing.T) {
|
||||
i, err := ValsInstance()
|
||||
// resetInstance resets the singleton for testing
|
||||
func resetInstance() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
instance = nil
|
||||
}
|
||||
|
||||
// setenvForTest sets the environment variable to value, or unsets it when value is empty,
|
||||
// restoring the original value when the test finishes.
|
||||
func setenvForTest(t *testing.T, key, value string) {
|
||||
t.Helper()
|
||||
if value == "" {
|
||||
orig, had := os.LookupEnv(key)
|
||||
os.Unsetenv(key)
|
||||
t.Cleanup(func() {
|
||||
if had {
|
||||
os.Setenv(key, orig)
|
||||
} else {
|
||||
os.Unsetenv(key)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
t.Setenv(key, value)
|
||||
}
|
||||
|
||||
func TestValsInstance(t *testing.T) {
|
||||
resetInstance()
|
||||
defer resetInstance()
|
||||
|
||||
setenvForTest(t, envvar.DisableVals, "")
|
||||
setenvForTest(t, envvar.DisableValsStrict, "")
|
||||
|
||||
i, err := ValsInstance()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
i2, _ := ValsInstance()
|
||||
|
||||
if i != i2 {
|
||||
t.Error("Instances should be equal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableVals(t *testing.T) {
|
||||
resetInstance()
|
||||
defer resetInstance()
|
||||
|
||||
setenvForTest(t, envvar.DisableVals, "true")
|
||||
setenvForTest(t, envvar.DisableValsStrict, "")
|
||||
|
||||
evaluator, err := ValsInstance()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Should pass through values unchanged
|
||||
input := map[string]any{"key": "ref+echo://secret"}
|
||||
output, err := evaluator.Eval(input)
|
||||
if err != nil {
|
||||
t.Fatalf("passthrough should not error: %v", err)
|
||||
}
|
||||
|
||||
if output["key"] != "ref+echo://secret" {
|
||||
t.Errorf("expected ref+ to pass through unchanged, got %v", output["key"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableValsStrict(t *testing.T) {
|
||||
resetInstance()
|
||||
defer resetInstance()
|
||||
|
||||
setenvForTest(t, envvar.DisableVals, "")
|
||||
setenvForTest(t, envvar.DisableValsStrict, "true")
|
||||
|
||||
evaluator, err := ValsInstance()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Should error on ref+
|
||||
input := map[string]any{"key": "ref+echo://secret"}
|
||||
_, err = evaluator.Eval(input)
|
||||
if err == nil {
|
||||
t.Fatal("strict mode should error on ref+")
|
||||
}
|
||||
if err != ErrValsDisabled {
|
||||
t.Errorf("expected ErrValsDisabled, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableValsStrictTakesPrecedence(t *testing.T) {
|
||||
resetInstance()
|
||||
defer resetInstance()
|
||||
|
||||
setenvForTest(t, envvar.DisableVals, "true")
|
||||
setenvForTest(t, envvar.DisableValsStrict, "true")
|
||||
|
||||
evaluator, err := ValsInstance()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Strict mode should win over pass-through mode
|
||||
input := map[string]any{"key": "ref+echo://secret"}
|
||||
_, err = evaluator.Eval(input)
|
||||
if err != ErrValsDisabled {
|
||||
t.Errorf("expected ErrValsDisabled when both env vars are set, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableValsStrictAllowsNonRef(t *testing.T) {
|
||||
resetInstance()
|
||||
defer resetInstance()
|
||||
|
||||
setenvForTest(t, envvar.DisableVals, "")
|
||||
setenvForTest(t, envvar.DisableValsStrict, "true")
|
||||
|
||||
evaluator, err := ValsInstance()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Should pass through non-ref+ values, including strings that merely
|
||||
// contain "ref+" without a provider scheme (not a vals reference)
|
||||
input := map[string]any{
|
||||
"key": "normal-value",
|
||||
"literal": "some ref+ text without scheme",
|
||||
"secretRef2": "ref+2",
|
||||
}
|
||||
output, err := evaluator.Eval(input)
|
||||
if err != nil {
|
||||
t.Fatalf("strict mode should allow non-ref+ values: %v", err)
|
||||
}
|
||||
if output["key"] != "normal-value" {
|
||||
t.Errorf("expected value to pass through, got %v", output["key"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableValsStrictDetectsSecretRef(t *testing.T) {
|
||||
resetInstance()
|
||||
defer resetInstance()
|
||||
|
||||
setenvForTest(t, envvar.DisableVals, "")
|
||||
setenvForTest(t, envvar.DisableValsStrict, "true")
|
||||
|
||||
evaluator, err := ValsInstance()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// secretref+ is also a vals reference and must be rejected
|
||||
input := map[string]any{"key": "secretref+vault://secret/data/x#y"}
|
||||
if _, err := evaluator.Eval(input); err != ErrValsDisabled {
|
||||
t.Errorf("expected ErrValsDisabled for secretref+ expression, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableValsStrictNestedRef(t *testing.T) {
|
||||
resetInstance()
|
||||
defer resetInstance()
|
||||
|
||||
setenvForTest(t, envvar.DisableVals, "")
|
||||
setenvForTest(t, envvar.DisableValsStrict, "true")
|
||||
|
||||
evaluator, err := ValsInstance()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Should detect nested ref+ in map[string]any
|
||||
input := map[string]any{
|
||||
"outer": map[string]any{
|
||||
"inner": "ref+vault://secret",
|
||||
},
|
||||
}
|
||||
_, err = evaluator.Eval(input)
|
||||
if err == nil {
|
||||
t.Fatal("strict mode should detect nested ref+")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableValsStrictMapAnyAny(t *testing.T) {
|
||||
resetInstance()
|
||||
defer resetInstance()
|
||||
|
||||
setenvForTest(t, envvar.DisableVals, "")
|
||||
setenvForTest(t, envvar.DisableValsStrict, "true")
|
||||
|
||||
evaluator, err := ValsInstance()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Should detect ref+ inside map[any]any (yaml.v2 nested maps)
|
||||
input := map[string]any{
|
||||
"outer": map[any]any{
|
||||
"inner": "ref+vault://secret",
|
||||
},
|
||||
}
|
||||
_, err = evaluator.Eval(input)
|
||||
if err == nil {
|
||||
t.Fatal("strict mode should detect ref+ in map[any]any")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableValsStrictArrayRef(t *testing.T) {
|
||||
resetInstance()
|
||||
defer resetInstance()
|
||||
|
||||
setenvForTest(t, envvar.DisableVals, "")
|
||||
setenvForTest(t, envvar.DisableValsStrict, "true")
|
||||
|
||||
evaluator, err := ValsInstance()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Should detect ref+ in []any arrays
|
||||
input := map[string]any{
|
||||
"values": []any{"normal", "ref+awssecrets://db/password"},
|
||||
}
|
||||
_, err = evaluator.Eval(input)
|
||||
if err == nil {
|
||||
t.Fatal("strict mode should detect ref+ in arrays")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableValsStrictStringSlice(t *testing.T) {
|
||||
resetInstance()
|
||||
defer resetInstance()
|
||||
|
||||
setenvForTest(t, envvar.DisableVals, "")
|
||||
setenvForTest(t, envvar.DisableValsStrict, "true")
|
||||
|
||||
evaluator, err := ValsInstance()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Should detect ref+ in []string arrays (matches renderValsSecrets usage)
|
||||
input := map[string]any{
|
||||
"values": []string{"normal", "ref+awssecrets://db/password"},
|
||||
}
|
||||
_, err = evaluator.Eval(input)
|
||||
if err == nil {
|
||||
t.Fatal("strict mode should detect ref+ in []string arrays")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableValsPassThroughStringSlice(t *testing.T) {
|
||||
resetInstance()
|
||||
defer resetInstance()
|
||||
|
||||
setenvForTest(t, envvar.DisableVals, "true")
|
||||
setenvForTest(t, envvar.DisableValsStrict, "")
|
||||
|
||||
evaluator, err := ValsInstance()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
input := map[string]any{
|
||||
"values": []string{"normal", "ref+awssecrets://db/password"},
|
||||
}
|
||||
out, err := evaluator.Eval(input)
|
||||
if err != nil {
|
||||
t.Fatalf("pass-through should not error: %v", err)
|
||||
}
|
||||
|
||||
values, ok := out["values"].([]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected out[\"values\"] to be []any, got %T", out["values"])
|
||||
}
|
||||
if values[0] != "normal" || values[1] != "ref+awssecrets://db/password" {
|
||||
t.Errorf("unexpected values: %v", values)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableValsPassThroughNestedTypes(t *testing.T) {
|
||||
resetInstance()
|
||||
defer resetInstance()
|
||||
|
||||
setenvForTest(t, envvar.DisableVals, "true")
|
||||
setenvForTest(t, envvar.DisableValsStrict, "")
|
||||
|
||||
evaluator, err := ValsInstance()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Nested map[any]any and []string should be normalized to map[string]any
|
||||
// and []any respectively, matching vals.Eval behavior
|
||||
input := map[string]any{
|
||||
"outer": map[any]any{
|
||||
"list": []string{"normal", "ref+echo://secret"},
|
||||
},
|
||||
}
|
||||
out, err := evaluator.Eval(input)
|
||||
if err != nil {
|
||||
t.Fatalf("pass-through should not error: %v", err)
|
||||
}
|
||||
|
||||
outer, ok := out["outer"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected out[\"outer\"] to be map[string]any, got %T", out["outer"])
|
||||
}
|
||||
list, ok := outer["list"].([]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected outer[\"list\"] to be []any, got %T", outer["list"])
|
||||
}
|
||||
if list[0] != "normal" || list[1] != "ref+echo://secret" {
|
||||
t.Errorf("unexpected values: %v", list)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalValsProcessing(t *testing.T) {
|
||||
resetInstance()
|
||||
defer resetInstance()
|
||||
|
||||
// Ensure both are unset
|
||||
setenvForTest(t, envvar.DisableVals, "")
|
||||
setenvForTest(t, envvar.DisableValsStrict, "")
|
||||
|
||||
evaluator, err := ValsInstance()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// ref+echo should expand to the value after ://
|
||||
input := map[string]any{"key": "ref+echo://myvalue"}
|
||||
output, err := evaluator.Eval(input)
|
||||
if err != nil {
|
||||
t.Fatalf("normal vals should process ref+echo: %v", err)
|
||||
}
|
||||
|
||||
if output["key"] != "myvalue" {
|
||||
t.Errorf("expected 'myvalue', got %v", output["key"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildValsOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -129,14 +457,6 @@ func TestBuildValsOptions(t *testing.T) {
|
||||
expectedFailOnMissingKey: false,
|
||||
expectedLogOutputDiscarded: true,
|
||||
},
|
||||
{
|
||||
name: "aws log level Off mixed case",
|
||||
awsLogLevel: "Off",
|
||||
failOnMissingKey: "",
|
||||
expectedLogLevel: "off",
|
||||
expectedFailOnMissingKey: false,
|
||||
expectedLogOutputDiscarded: true,
|
||||
},
|
||||
{
|
||||
name: "both options set",
|
||||
awsLogLevel: "standard",
|
||||
@@ -172,6 +492,7 @@ func TestBuildValsOptions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestAWSSDKLogLevelConfiguration tests the AWS SDK log level configuration logic
|
||||
func TestAWSSDKLogLevelConfiguration(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -32,7 +32,7 @@ func forbidEnvironmentsWithReleases(filePath string, content []byte) (bool, erro
|
||||
return true, fmt.Errorf("no top-level config keys are found in %s", filePath)
|
||||
}
|
||||
result := []string{}
|
||||
resultKeys := map[string]interface{}{}
|
||||
resultKeys := map[string]any{}
|
||||
for _, k := range topKeys {
|
||||
if slices.Contains([]string{"environments", "releases", "---"}, k) {
|
||||
if _, ok := resultKeys[k]; !ok {
|
||||
@@ -79,9 +79,9 @@ func isTopOrderKey(key string) bool {
|
||||
// TopKeys returns the top-level config keys.
|
||||
func TopKeys(helmfileContent []byte, hasSeparator bool) []string {
|
||||
var topKeys []string
|
||||
clines := bytes.Split(helmfileContent, []byte("\n"))
|
||||
clines := bytes.SplitSeq(helmfileContent, []byte("\n"))
|
||||
|
||||
for _, line := range clines {
|
||||
for line := range clines {
|
||||
lineStr := strings.TrimRightFunc(string(line), unicode.IsSpace)
|
||||
if lineStr == "" {
|
||||
continue // Skip empty lines
|
||||
@@ -91,7 +91,7 @@ func TopKeys(helmfileContent []byte, hasSeparator bool) []string {
|
||||
}
|
||||
|
||||
if topConfigKeysRegex.MatchString(lineStr) {
|
||||
topKey := strings.SplitN(lineStr, ":", 2)[0]
|
||||
topKey, _, _ := strings.Cut(lineStr, ":")
|
||||
topKeys = append(topKeys, topKey)
|
||||
}
|
||||
}
|
||||
|
||||
+76
-8
@@ -100,6 +100,13 @@ func (e InvalidURLError) Error() string {
|
||||
|
||||
type Source struct {
|
||||
Getter, Scheme, User, Host, Dir, File, RawQuery string
|
||||
|
||||
// HasSelector is true when the source URL contained an explicit "@<file>"
|
||||
// selector marking where the repository/dir root ends and the file
|
||||
// selector begins. Without it, Dir and File are inferred from the last
|
||||
// path segment (see Parse), which loses the ability to reconstruct a
|
||||
// valid getter source when File is itself a pattern.
|
||||
HasSelector bool
|
||||
}
|
||||
|
||||
func IsRemote(goGetterSrc string) bool {
|
||||
@@ -112,6 +119,37 @@ func IsRemote(goGetterSrc string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// hasGlobMeta reports whether p contains a filepath.Match metacharacter. The
|
||||
// set matches filepath.Match's, which also backs local values-file globbing
|
||||
// (Storage.ExpandPaths), so remote and local glob syntax stay identical.
|
||||
//
|
||||
// In practice a raw "?" can never reach p when p is Source.File: url.Parse
|
||||
// splits the query string at the first unescaped "?", so it never survives
|
||||
// into u.Path. A "?" wildcard only reaches here percent-encoded ("%3F") in
|
||||
// the original URL, which url.Parse decodes back into a literal "?" in
|
||||
// u.Path/u.File. "?" is kept in this set anyway, for symmetry with
|
||||
// filepath.Match's metacharacters and to handle that percent-encoded case.
|
||||
func hasGlobMeta(p string) bool {
|
||||
return strings.ContainsAny(p, `*?[`)
|
||||
}
|
||||
|
||||
// HasGlobPattern reports whether the "@<file>" selector of a go-getter style
|
||||
// reference is a glob pattern, e.g.
|
||||
//
|
||||
// git::https://github.com/org/repo.git@path/to/dir/*.yaml?ref=main
|
||||
//
|
||||
// Only the file selector is examined. The rest of a reference legitimately
|
||||
// contains glob metacharacters that are not patterns: "?" begins the query
|
||||
// string ("?ref=main") unless percent-encoded, and "[" appears in IPv6 hosts
|
||||
// and in placeholder values such as github.com/[$GITHUB_ORG]/repo.git.
|
||||
func HasGlobPattern(goGetterSrc string) bool {
|
||||
u, err := Parse(goGetterSrc)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return hasGlobMeta(u.File)
|
||||
}
|
||||
|
||||
func Parse(goGetterSrc string) (*Source, error) {
|
||||
items := strings.Split(goGetterSrc, "::")
|
||||
var getter string
|
||||
@@ -143,7 +181,8 @@ func Parse(goGetterSrc string) (*Source, error) {
|
||||
}
|
||||
|
||||
pathComponents := strings.Split(u.Path, "@")
|
||||
if len(pathComponents) != 2 {
|
||||
hasSelector := len(pathComponents) == 2
|
||||
if !hasSelector {
|
||||
dir := filepath.Dir(u.Path)
|
||||
if len(dir) > 0 {
|
||||
dir = dir[1:]
|
||||
@@ -152,13 +191,14 @@ func Parse(goGetterSrc string) (*Source, error) {
|
||||
}
|
||||
|
||||
return &Source{
|
||||
Getter: getter,
|
||||
User: u.User.String(),
|
||||
Scheme: u.Scheme,
|
||||
Host: u.Host,
|
||||
Dir: pathComponents[0],
|
||||
File: pathComponents[1],
|
||||
RawQuery: u.RawQuery,
|
||||
Getter: getter,
|
||||
User: u.User.String(),
|
||||
Scheme: u.Scheme,
|
||||
Host: u.Host,
|
||||
Dir: pathComponents[0],
|
||||
File: pathComponents[1],
|
||||
RawQuery: u.RawQuery,
|
||||
HasSelector: hasSelector,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -213,6 +253,34 @@ func (r *Remote) Fetch(path string, cacheDirOpt ...string) (string, error) {
|
||||
return "", fmt.Errorf("remote sources are disabled due to 'HELMFILE_DISABLE_INSECURE_FEATURES'")
|
||||
}
|
||||
|
||||
// A wildcard in the "@<file>" selector is expanded by the caller (e.g.
|
||||
// Storage.resolveFile) against the directory downloaded below. Reject the
|
||||
// shapes where that can never work, instead of letting it fail later with a
|
||||
// confusing "file not found" naming a path that contains a literal "*".
|
||||
//
|
||||
// Only "*" is treated as an unambiguous wildcard signal here: "?" can never
|
||||
// reach u.File (url.Parse splits the query string at the first "?"), and
|
||||
// "[" legitimately appears in existing literal file names, so neither
|
||||
// should make Fetch reject an otherwise-valid single-file reference.
|
||||
if strings.Contains(u.File, "*") {
|
||||
switch {
|
||||
case u.Getter == "normal":
|
||||
// Plain http(s):// and s3:// URLs (ParseNormal) always download a
|
||||
// single object.
|
||||
return "", fmt.Errorf("wildcards are not supported for %s:// sources, which fetch a single file: %s (use a directory-capable getter such as git::)", u.Scheme, path)
|
||||
case u.Getter == "s3" && decompressorForFile(filepath.Base(u.Dir)) == nil:
|
||||
// Forced s3:: also fetches a single object, unless it's an archive:
|
||||
// S3Getter decompresses those into the cache dir, so a selector can
|
||||
// still match a file inside.
|
||||
return "", fmt.Errorf("wildcards are not supported for s3:: sources unless the object is an archive: %s", path)
|
||||
case !u.HasSelector:
|
||||
// Without an explicit "@" selector, Dir/File are inferred from the
|
||||
// last path segment (see Parse), which can't tell a repository root
|
||||
// from a wildcard file name.
|
||||
return "", fmt.Errorf(`wildcards require an explicit "@" selector marking the repository root, e.g. git::https://host/org/repo.git@dir/*.yaml?ref=main: %s`, path)
|
||||
}
|
||||
}
|
||||
|
||||
srcDir := fmt.Sprintf("%s://%s/%s", u.Scheme, u.Host, u.Dir)
|
||||
file := u.File
|
||||
|
||||
|
||||
+247
-15
@@ -760,16 +760,65 @@ func TestIsRemote(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasGlobPattern(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "wildcard file selector",
|
||||
input: "git::https://github.com/org/repo.git@path/to/dir/*.yaml?ref=main",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "literal file selector with ref query param",
|
||||
input: "git::https://github.com/org/repo.git@path/to/values.yaml?ref=main",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "placeholder bracket in dir, not file",
|
||||
input: `git::https://github.com/[$GITHUB_ORG]/repository-name.git@/values.dev.yaml?ref=main`,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "ipv6 host, not file",
|
||||
input: "git::https://[::1]/org/repo.git@values.yaml?ref=main",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "character class in file selector",
|
||||
input: "git::https://github.com/org/repo.git@dir/values-[abc].yaml?ref=main",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "relative local path",
|
||||
input: "relative/path/*.yaml",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testcases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := HasGlobPattern(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("HasGlobPattern(%q) = %v, want %v", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
input string
|
||||
getter string
|
||||
scheme string
|
||||
dir string
|
||||
file string
|
||||
query string
|
||||
err string
|
||||
name string
|
||||
input string
|
||||
getter string
|
||||
scheme string
|
||||
dir string
|
||||
file string
|
||||
query string
|
||||
hasSelector bool
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "miss scheme",
|
||||
@@ -782,13 +831,24 @@ func TestParse(t *testing.T) {
|
||||
err: "parse url: local absolute path is not a remote URL: /absolute/path/to/file.yaml",
|
||||
},
|
||||
{
|
||||
name: "git scheme",
|
||||
input: "git::https://github.com/stakater/Forecastle.git@deployments/kubernetes/chart/forecastle?ref=v1.0.54",
|
||||
getter: "git",
|
||||
scheme: "https",
|
||||
dir: "/stakater/Forecastle.git",
|
||||
file: "deployments/kubernetes/chart/forecastle",
|
||||
query: "ref=v1.0.54",
|
||||
name: "git scheme",
|
||||
input: "git::https://github.com/stakater/Forecastle.git@deployments/kubernetes/chart/forecastle?ref=v1.0.54",
|
||||
getter: "git",
|
||||
scheme: "https",
|
||||
dir: "/stakater/Forecastle.git",
|
||||
file: "deployments/kubernetes/chart/forecastle",
|
||||
query: "ref=v1.0.54",
|
||||
hasSelector: true,
|
||||
},
|
||||
{
|
||||
name: "git scheme with wildcard file selector",
|
||||
input: "git::https://github.com/org/repo.git@path/to/dir/*.yaml?ref=main",
|
||||
getter: "git",
|
||||
scheme: "https",
|
||||
dir: "/org/repo.git",
|
||||
file: "path/to/dir/*.yaml",
|
||||
query: "ref=main",
|
||||
hasSelector: true,
|
||||
},
|
||||
{
|
||||
name: "s3 scheme",
|
||||
@@ -878,12 +938,14 @@ func TestParse(t *testing.T) {
|
||||
}
|
||||
|
||||
var getter, scheme, dir, file, query string
|
||||
var hasSelector bool
|
||||
if src != nil {
|
||||
getter = src.Getter
|
||||
scheme = src.Scheme
|
||||
dir = src.Dir
|
||||
file = src.File
|
||||
query = src.RawQuery
|
||||
hasSelector = src.HasSelector
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tt.getter, getter); diff != "" {
|
||||
@@ -905,6 +967,10 @@ func TestParse(t *testing.T) {
|
||||
if diff := cmp.Diff(tt.query, query); diff != "" {
|
||||
t.Fatalf("Unexpected query:\n%s", diff)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tt.hasSelector, hasSelector); diff != "" {
|
||||
t.Fatalf("Unexpected hasSelector:\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -987,6 +1053,172 @@ func TestRemote_Fetch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemote_Fetch_Wildcard(t *testing.T) {
|
||||
cleanfs := map[string]string{
|
||||
CacheDir(): "",
|
||||
}
|
||||
cachefs := map[string]string{
|
||||
filepath.Join(CacheDir(), "https_github_com_helmfile_helmfile_git.ref=v0.151.0/examples/values/a.yaml"): "foo: bar",
|
||||
}
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
files map[string]string
|
||||
expectCacheHit bool
|
||||
}{
|
||||
{name: "not expectCacheHit", files: cleanfs, expectCacheHit: false},
|
||||
{name: "expectCacheHit", files: cachefs, expectCacheHit: true},
|
||||
}
|
||||
|
||||
for _, tt := range testcases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
testfs := testhelper.NewTestFs(tt.files)
|
||||
|
||||
hit := true
|
||||
|
||||
get := func(wd, src, dst string) error {
|
||||
if wd != CacheDir() {
|
||||
return fmt.Errorf("unexpected wd: %s", wd)
|
||||
}
|
||||
// The wildcard selector is stripped from the getter source: the
|
||||
// whole directory is downloaded and the pattern is applied
|
||||
// against it afterwards.
|
||||
if src != "git::https://github.com/helmfile/helmfile.git?ref=v0.151.0" {
|
||||
return fmt.Errorf("unexpected src: %s", src)
|
||||
}
|
||||
|
||||
hit = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
getter := &testGetter{
|
||||
get: get,
|
||||
}
|
||||
remote := &Remote{
|
||||
Logger: helmexec.NewLogger(io.Discard, "debug"),
|
||||
Home: CacheDir(),
|
||||
Getter: getter,
|
||||
fs: testfs.ToFileSystem(),
|
||||
}
|
||||
|
||||
url := "git::https://github.com/helmfile/helmfile.git@examples/values/*.yaml?ref=v0.151.0"
|
||||
file, err := remote.Fetch(url)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// The wildcard selector is joined onto the cache dir untouched: it is
|
||||
// the caller's (Storage.resolveFile's) job to glob it.
|
||||
expectedFile := filepath.Join(CacheDir(), "https_github_com_helmfile_helmfile_git.ref=v0.151.0/examples/values/*.yaml")
|
||||
if file != expectedFile {
|
||||
t.Errorf("unexpected file located: %s vs expected: %s", file, expectedFile)
|
||||
}
|
||||
|
||||
// A wildcard selector must share the cache entry with a literal
|
||||
// reference to the same repo/ref: the cache key never depends on File.
|
||||
if tt.expectCacheHit && !hit {
|
||||
t.Errorf("unexpected result: unexpected cache miss")
|
||||
}
|
||||
if !tt.expectCacheHit && hit {
|
||||
t.Errorf("unexpected result: unexpected cache hit")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemote_Fetch_WildcardUnsupported(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
url string
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "plain https single file",
|
||||
url: "https://example.com/dir/*.yaml",
|
||||
errContains: "wildcards are not supported for https:// sources",
|
||||
},
|
||||
{
|
||||
name: "plain s3 single file",
|
||||
url: "s3://bucket/dir/*.yaml",
|
||||
errContains: "wildcards are not supported for s3:// sources",
|
||||
},
|
||||
{
|
||||
name: "forced s3 getter, non-archive object",
|
||||
url: "s3::https://bucket.s3.us-east-2.amazonaws.com/dir/*.yaml",
|
||||
errContains: "wildcards are not supported for s3:: sources",
|
||||
},
|
||||
{
|
||||
name: "no explicit selector",
|
||||
url: "git::https://git.company.org/helmfiles/global/*.yaml?ref=master",
|
||||
errContains: `wildcards require an explicit "@" selector`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testcases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
called := false
|
||||
get := func(wd, src, dst string) error {
|
||||
called = true
|
||||
return nil
|
||||
}
|
||||
getter := &testGetter{get: get}
|
||||
|
||||
testfs := testhelper.NewTestFs(map[string]string{CacheDir(): ""})
|
||||
remote := &Remote{
|
||||
Logger: helmexec.NewLogger(io.Discard, "debug"),
|
||||
Home: CacheDir(),
|
||||
Getter: getter,
|
||||
S3Getter: getter,
|
||||
HttpGetter: getter,
|
||||
fs: testfs.ToFileSystem(),
|
||||
}
|
||||
|
||||
_, err := remote.Fetch(tt.url)
|
||||
if err == nil {
|
||||
t.Fatalf("expected an error, got none")
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.errContains) {
|
||||
t.Errorf("error %q does not contain %q", err.Error(), tt.errContains)
|
||||
}
|
||||
if called {
|
||||
t.Errorf("getter should not have been invoked")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRemote_Fetch_WildcardAllowedS3Archive locks in the one s3:: shape a
|
||||
// wildcard selector IS allowed for: a forced s3:: getter pointing at an
|
||||
// archive. S3Getter decompresses archives into the cache dir (see
|
||||
// decompressorForFile), so a selector can still match files inside it, unlike
|
||||
// a single non-archive s3:: object (covered by TestRemote_Fetch_WildcardUnsupported).
|
||||
func TestRemote_Fetch_WildcardAllowedS3Archive(t *testing.T) {
|
||||
testfs := testhelper.NewTestFs(map[string]string{CacheDir(): ""})
|
||||
|
||||
called := false
|
||||
get := func(wd, src, dst string) error {
|
||||
called = true
|
||||
return nil
|
||||
}
|
||||
getter := &testGetter{get: get}
|
||||
|
||||
remote := &Remote{
|
||||
Logger: helmexec.NewLogger(io.Discard, "debug"),
|
||||
Home: CacheDir(),
|
||||
S3Getter: getter,
|
||||
fs: testfs.ToFileSystem(),
|
||||
}
|
||||
|
||||
url := "s3::https://bucket.s3.us-east-2.amazonaws.com/dir/app.tar.gz@*.yaml"
|
||||
if _, err := remote.Fetch(url); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !called {
|
||||
t.Errorf("expected S3Getter to be invoked for an archive wildcard selector")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAWSSDKLogLevelInit verifies that the init() function reads HELMFILE_AWS_SDK_LOG_LEVEL correctly
|
||||
func TestAWSSDKLogLevelInit(t *testing.T) {
|
||||
tests := []struct {
|
||||
|
||||
@@ -549,7 +549,7 @@ dependencies:
|
||||
errCh := make(chan error, numGoroutines)
|
||||
ready := make(chan struct{})
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
for range numGoroutines {
|
||||
wg.Add(1)
|
||||
readyWg.Add(1)
|
||||
go func() {
|
||||
|
||||
@@ -260,8 +260,8 @@ func chartDependenciesAlias(namespace, releaseName string) string {
|
||||
// the chart path when helm processes OCI references during dependency update.
|
||||
// See issue #954.
|
||||
func ociDependencyChartName(chart string) string {
|
||||
if idx := strings.LastIndex(chart, "/"); idx >= 0 {
|
||||
return chart[idx+1:]
|
||||
if _, name, found := strings.CutLast(chart, "/"); found {
|
||||
return name
|
||||
}
|
||||
return chart
|
||||
}
|
||||
@@ -273,10 +273,11 @@ func ociDependencyChartName(chart string) string {
|
||||
// chart="path_with_underscores/example", baseURL="oci://registry.example.com"
|
||||
// → "oci://registry.example.com/path_with_underscores"
|
||||
func ociDependencyRepoURL(chart, ociBaseURL string) string {
|
||||
if idx := strings.LastIndex(chart, "/"); idx >= 0 {
|
||||
return strings.TrimSuffix(ociBaseURL, "/") + "/" + chart[:idx]
|
||||
pathPrefix, _, found := strings.CutLast(chart, "/")
|
||||
if !found {
|
||||
return ociBaseURL
|
||||
}
|
||||
return ociBaseURL
|
||||
return strings.TrimSuffix(ociBaseURL, "/") + "/" + pathPrefix
|
||||
}
|
||||
|
||||
func getUnresolvedDependenciess(st *HelmState) (string, *UnresolvedDependencies) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user