Compare commits

...
25 Commits
Author SHA1 Message Date
Spicy Shark dfce9cf570 fix: honor release-level syncReleaseLabels false (#2839)
* fix: honor release-level syncReleaseLabels false

An explicit release syncReleaseLabels false now prevents helmDefaults from
adding --labels. Unset releases still inherit the default, and the
command-line flag still takes precedence.

Signed-off-by: spicysharkco <321699413+spicysharkco@users.noreply.github.com>

* test: set HelmDefaults through the promoted field

Applies the go fix modernizer rewrite the lint job checks for.

Signed-off-by: spicysharkco <321699413+spicysharkco@users.noreply.github.com>

---------

Signed-off-by: spicysharkco <321699413+spicysharkco@users.noreply.github.com>
2026-10-21 18:04:32 +08:00
Spicy Shark 0d5311ba4d fix: honor release-level takeOwnership false over helmDefaults (#2838)
* fix: honor release-level takeOwnership false over helmDefaults

An explicit release takeOwnership false now prevents helmDefaults from
adding --take-ownership to upgrades and helmfile diff. Unset releases
still inherit the default, and the command-line flag still takes
precedence.

Signed-off-by: spicysharkco <321699413+spicysharkco@users.noreply.github.com>

* test: set HelmDefaults through the promoted field

Applies the go fix modernizer rewrite the lint job checks for.

Signed-off-by: spicysharkco <321699413+spicysharkco@users.noreply.github.com>

---------

Signed-off-by: spicysharkco <321699413+spicysharkco@users.noreply.github.com>
2026-10-21 18:04:03 +08:00
Spicy Shark 54c5944fce fix: honor release-level skipSchemaValidation false (#2837)
An explicit release value of false now keeps helmDefaults from adding
--skip-schema-validation. The command-line flag still applies to every
release.

Signed-off-by: spicysharkco <321699413+spicysharkco@users.noreply.github.com>
2026-10-21 09:18:50 +08:00
dependabot[bot] d5a354144a build(deps): bump github.com/gookit/color from 1.6.2 to 1.6.3 (#2836)
Bumps [github.com/gookit/color](https://github.com/gookit/color) from 1.6.2 to 1.6.3.
- [Release notes](https://github.com/gookit/color/releases)
- [Commits](https://github.com/gookit/color/compare/v1.6.2...v1.6.3)

---
updated-dependencies:
- dependency-name: github.com/gookit/color
  dependency-version: 1.6.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-20 08:37:02 +08:00
dependabot[bot] 5d4db7ab89 build(deps): bump github.com/go-test/deep from 1.1.1 to 1.1.2 (#2835)
Bumps [github.com/go-test/deep](https://github.com/go-test/deep) from 1.1.1 to 1.1.2.
- [Release notes](https://github.com/go-test/deep/releases)
- [Changelog](https://github.com/go-test/deep/blob/main/CHANGES.md)
- [Commits](https://github.com/go-test/deep/compare/v1.1.1...v1.1.2)

---
updated-dependencies:
- dependency-name: github.com/go-test/deep
  dependency-version: 1.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-20 08:36:20 +08:00
yxxhero 254b2aa617 fix: propagate --oci-plain-http to chartify for all chart-preparing commands (#2826)
* fix: propagate --oci-plain-http to chartify for all chart-preparing commands

The global --oci-plain-http flag was only wired into ChartPrepareOptions
for the template command. For apply, sync, diff, lint, unittest, fetch
(and destroy/test, which share the same chartify path), the flag never
reached chartify, so its internal 'helm dependency up' against OCI chart
dependencies was issued without --plain-http and failed against
plain-HTTP OCI registries (the per-repository plainHttp field only
covers the helm pull of the release chart, not chartify's dependency
update).

Add HelmOCIPlainHTTP: a.HelmOCIPlainHTTP to the ChartPrepareOptions of
these commands, mirroring Template(), and add a regression test that
fetches a release with an adhoc OCI dependency from a plain-HTTP local
registry.

Fixes #2824

Signed-off-by: yxxhero <yunxuan8603@gmail.com>
Signed-off-by: yxxhero <aiopsclub@163.com>

* test: use lint (not fetch) to exercise chartify OCI plain-http path

The pull/fetch command skips chartify (except for local dirs without
Chart.yaml) and runs helmexec.BuildDeps instead, which already handles
--plain-http at the exec layer. lint goes through chartify like
template/apply/sync/diff do, and needs no cluster, so it reproduces
issue #2824: before the fix, chartify's 'helm dependency up' on the
adhoc OCI dependency fails against the plain-HTTP registry on Helm 4.

Signed-off-by: yxxhero <yunxuan8603@gmail.com>
Signed-off-by: yxxhero <aiopsclub@163.com>

* test: align lint snapshot goldens with actual helm output

Fix the Helm 3 golden with the exact output captured from CI (helm lint
prints the chartified chart path and extra blank lines). The Helm 4
golden follows the same shape plus the Pulled/Digest lines emitted by
helm dependency up.

Signed-off-by: yxxhero <yunxuan8603@gmail.com>
Signed-off-by: yxxhero <aiopsclub@163.com>

* test: add fetch regression case exercising chartify OCI plain-http path

helmfile fetch only enters chartification when the chart is a local
directory without Chart.yaml (NeedsChartifyForLocalDir) — a packaged or
regular local chart goes through helm dependency build instead, which
already handles --plain-http at the helmexec layer. The new case uses a
manifest-only chart directory plus an adhoc OCI dependency, so chartify's
own 'helm dependency up' must receive the propagated --oci-plain-http on
Helm 4, directly protecting the Fetch() wiring added in this PR.

To reference the case-local fixture, the snapshot harness now rewrites
'./chart/' in inputs to the chart directory shipped next to the case
(mirroring the existing '../../charts/' rewrite). The fixture cannot
live in the shared testdata/charts directory because every chart there
is helm-packaged and pushed to the test registry.

Addresses the review finding that only the Lint assignment was covered.

Signed-off-by: yxxhero <aiopsclub@163.com>

* test: align fetch goldens with actual helm dep build output

For a chartified manifest-only chart, chartify's ReplaceWithRendered
strips the Chart.yaml dependencies field and removes charts/ after
rendering the adhoc dependency into templates/. The follow-up
`helm dependency build` therefore finds no declared dependencies and
returns silently (helm3/helm4 Manager short-circuit), so the captured
output is just the "Building dependency" log line — confirmed by the
Helm 3 CI diff. Helm 4.3.0's Manager has the identical early-return
path, so its golden matches.

The regression signal remains the command's exit code: without the
propagated --oci-plain-http, chartify's `helm dependency up` fails on
Helm 4 and the whole fetch errors out.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <yunxuan8603@gmail.com>
Signed-off-by: yxxhero <aiopsclub@163.com>
2026-10-16 13:37:47 +08:00
yxxhero b2644c6ec2 chore: bump Helm to v3.22.1 and v4.3.1 (#2834)
- Update CI test matrix from v3.22.0/v4.3.0 to v3.22.1/v4.3.1
- Update Helm v4.3.1 SHA256 checksums in Dockerfiles
- Update go.mod dependencies (helm.sh/helm/v3 v3.22.1, helm.sh/helm/v4 v4.3.1)
- Update HelmRecommendedVersion to v4.3.1
- Pin controller-runtime to v0.24.1: v0.25.x needs client-go >= v0.36
  (toolscache.DoneChecker), conflicting with the k8s.io/api v0.35.4 pin
  required by kubedog's flagger dependency

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-10-16 13:37:34 +08:00
Daniel Pintoandyxxhero 8fc23a4d80 fix: skip recreatePods on Helm 4, which removed --recreate-pods (#2832)
* fix: skip recreatePods on Helm 4, which removed --recreate-pods

- Helm 4 removed `--recreate-pods` (helm/helm#31023) and fails with "unknown flag", so a release
  with `recreatePods: true` broke `sync` and `apply`. On Helm 4 the flag is now left out with a
  warning; Helm 3 still receives it.
- The upgrade flag tests gain Helm 4 cases, and the existing ones are pinned to Helm 3 so they no
  longer depend on the installed helm binary.
- A test checks that the warning names the release on Helm 4 and is not logged on Helm 3.
- The configuration docs note that recreatePods is ignored on Helm 4.

Signed-off-by: Cayan <1619617+Cayan@users.noreply.github.com>

* refactor: flatten recreatePods flag logic in flagsForUpgrade

Drop the nested if-in-if and the st.logger nil-guard: no other log call in
state.go nil-checks the logger (e.g. the --color Helm 4 skip). Compute
recreatePodsEnabled and branch via a tagless switch, matching the switch
style already used elsewhere in state.go: warn on Helm 4, append
--recreate-pods otherwise. The flagsForUpgrade table runner now uses the
shared discard logger so the Helm 4 recreate-pods cases log safely.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: Cayan <1619617+Cayan@users.noreply.github.com>
Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: yxxhero <aiopsclub@163.com>
2026-10-16 09:23:47 +08:00
dependabot[bot] 5c225fd200 build(deps): bump github.com/gookit/color from 1.6.1 to 1.6.2 (#2833)
Bumps [github.com/gookit/color](https://github.com/gookit/color) from 1.6.1 to 1.6.2.
- [Release notes](https://github.com/gookit/color/releases)
- [Commits](https://github.com/gookit/color/compare/v1.6.1...v1.6.2)

---
updated-dependencies:
- dependency-name: github.com/gookit/color
  dependency-version: 1.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-16 07:16:30 +08:00
dependabot[bot] b71d27e85b build(deps): bump github.com/aws/aws-sdk-go-v2/config from 1.33.6 to 1.33.9 (#2831)
build(deps): bump github.com/aws/aws-sdk-go-v2/config

Bumps [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) from 1.33.6 to 1.33.9.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.33.6...config/v1.33.9)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/config
  dependency-version: 1.33.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-15 08:03:01 +08:00
dependabot[bot] 735c484ff5 build(deps): bump golang.org/x/term from 0.46.0 to 0.47.0 (#2830)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.46.0 to 0.47.0.
- [Commits](https://github.com/golang/term/compare/v0.46.0...v0.47.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-version: 0.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-15 07:54:11 +08:00
dependabot[bot] 706ceda804 build(deps): bump github.com/mattn/go-runewidth from 0.0.30 to 0.0.31 (#2829)
Bumps [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth) from 0.0.30 to 0.0.31.
- [Commits](https://github.com/mattn/go-runewidth/compare/v0.0.30...v0.0.31)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-runewidth
  dependency-version: 0.0.31
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-15 07:54:05 +08:00
dependabot[bot] fa878f5ebb build(deps): bump github.com/aws/aws-sdk-go-v2/service/s3 from 1.114.0 to 1.114.3 (#2828)
build(deps): bump github.com/aws/aws-sdk-go-v2/service/s3

Bumps [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) from 1.114.0 to 1.114.3.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.114.0...service/s3/v1.114.3)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/s3
  dependency-version: 1.114.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-15 07:53:59 +08:00
dependabot[bot] eeb913472a build(deps): bump go.opentelemetry.io/contrib/exporters/autoexport from 0.71.0 to 0.72.0 (#2823)
build(deps): bump go.opentelemetry.io/contrib/exporters/autoexport

Bumps [go.opentelemetry.io/contrib/exporters/autoexport](https://github.com/open-telemetry/opentelemetry-go-contrib) from 0.71.0 to 0.72.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.71.0...zpages/v0.72.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/contrib/exporters/autoexport
  dependency-version: 0.72.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-11 11:34:04 +08:00
dependabot[bot] e088d125f6 build(deps): bump go.opentelemetry.io/proto/otlp from 1.11.0 to 1.11.1 (#2822)
Bumps [go.opentelemetry.io/proto/otlp](https://github.com/open-telemetry/opentelemetry-proto-go) from 1.11.0 to 1.11.1.
- [Release notes](https://github.com/open-telemetry/opentelemetry-proto-go/releases)
- [Commits](https://github.com/open-telemetry/opentelemetry-proto-go/compare/v1.11.0...v1.11.1)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/proto/otlp
  dependency-version: 1.11.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-11 11:31:30 +08:00
dependabot[bot] b7f71a0ce3 build(deps): bump github.com/hashicorp/go-getter/v2 from 2.2.4 to 2.2.5 (#2821)
Bumps [github.com/hashicorp/go-getter/v2](https://github.com/hashicorp/go-getter) from 2.2.4 to 2.2.5.
- [Release notes](https://github.com/hashicorp/go-getter/releases)
- [Changelog](https://github.com/hashicorp/go-getter/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/go-getter/compare/v2.2.4...v2.2.5)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/go-getter/v2
  dependency-version: 2.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-11 11:31:25 +08:00
pznamenskyandyxxhero 12fb6887c0 add trackLogsInterval option (#2825)
* add trackLogsInterval option

Signed-off-by: pznamensky <kompastver@gmail.com>

* test: add coverage for trackLogsInterval option

The trackLogsInterval feature shipped without tests. Add coverage for:

- config getter: zero when --track-logs-interval is not supplied, explicit
  value (including an explicit default) when supplied, and the nil-Cmd
  fallback for non-cobra callers (pkg/config/apply_test.go, sync_test.go)
- buildReleaseTracker resolution: explicit CLI interval overrides the
  release setting, release setting used when the flag is absent, 10s
  default preserved when both are unset (pkg/state/helmx_test.go)
- release-level minimum validation in ExecuteTemplates: below/zero/
  negative rejected, 1s and above accepted (pkg/state/state_exec_tmpl_test.go)

Also add the missing blank line after syncImpl.Cmd in cmd/sync.go for
consistency with cmd/apply.go.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: pznamensky <kompastver@gmail.com>
Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: yxxhero <aiopsclub@163.com>
2026-10-10 14:13:10 +08:00
dependabot[bot] 3216c02743 build(deps): bump golang.org/x/sync from 0.23.0 to 0.24.0 (#2827)
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.23.0 to 0.24.0.
- [Commits](https://github.com/golang/sync/compare/v0.23.0...v0.24.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  dependency-version: 0.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-10 11:56:34 +08:00
dependabot[bot] 892f2f9604 build(deps): bump gitpython from 3.1.59 to 3.1.62 in /docs (#2820)
Bumps [gitpython](https://github.com/gitpython-developers/GitPython) from 3.1.59 to 3.1.62.
- [Release notes](https://github.com/gitpython-developers/GitPython/releases)
- [Changelog](https://github.com/gitpython-developers/GitPython/blob/main/CHANGES)
- [Commits](https://github.com/gitpython-developers/GitPython/compare/3.1.59...3.1.62)

---
updated-dependencies:
- dependency-name: gitpython
  dependency-version: 3.1.62
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 09:12:30 +08:00
dependabot[bot] eeec9e17a2 build(deps): bump go.opentelemetry.io/otel/sdk/metric from 1.46.0 to 1.47.0 (#2815)
build(deps): bump go.opentelemetry.io/otel/sdk/metric

Bumps [go.opentelemetry.io/otel/sdk/metric](https://github.com/open-telemetry/opentelemetry-go) from 1.46.0 to 1.47.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.46.0...v1.47.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/sdk/metric
  dependency-version: 1.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 09:11:46 +08:00
dependabot[bot] f1c86aa9dd build(deps): bump github.com/sashabaranov/go-openai from 1.42.1 to 1.43.0 (#2819)
build(deps): bump github.com/sashabaranov/go-openai

Bumps [github.com/sashabaranov/go-openai](https://github.com/sashabaranov/go-openai) from 1.42.1 to 1.43.0.
- [Release notes](https://github.com/sashabaranov/go-openai/releases)
- [Commits](https://github.com/sashabaranov/go-openai/compare/v1.42.1...v1.43.0)

---
updated-dependencies:
- dependency-name: github.com/sashabaranov/go-openai
  dependency-version: 1.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 08:41:01 +08:00
dependabot[bot] 25cd13e148 build(deps): bump github.com/aws/aws-sdk-go-v2/service/s3 from 1.113.4 to 1.114.0 (#2818)
build(deps): bump github.com/aws/aws-sdk-go-v2/service/s3

Bumps [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) from 1.113.4 to 1.114.0.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.113.4...service/s3/v1.114.0)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/s3
  dependency-version: 1.114.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-06 08:40:57 +08:00
Dominik Schmidtandyxxhero 443cef0dc1 feat: dir= selector for path-based release filtering and traversal skip (#2609)
* feat: add dir= selector for path-based release filtering and traversal skip

Introduce a new selector key `dir=<path>` to the -l/--selector flag with two
effects: it filters releases by the directory of their defining helmfile
relative to the root helmfile (using directory-prefix matching, so
dir=apps/foo matches both apps/foo and apps/foo/sub), and it short-circuits
sub-helmfile traversal when paired with positive dir= constraints, so
non-matching branches in the helmfiles: tree are not parsed, templated, or
fetched.

The motivation is consuming aggregator-style upstreams that the operator
does not control (opendesk being the immediate example), where the user
cannot restructure the helmfile layout but still wants to act on a subset.

The dir label is auto-populated at filter time only; user-facing label
output is unchanged. The label key "dir" is reserved at state load.
Selectors that escape the root via .. or absolute paths, and the bare ".",
are rejected at parse time.

Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>

* refactor: use sync.Once for root dir resolution, extract skipForDirFilter

Add a unit test covering the per-entry skip decision: match descends,
sibling skipped, remote never short-circuited.

Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>

* cmd: shorten dir selector help text, link to docs

Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>

* docs: highlight selectors inheritance caveat, add bases example for dir label

Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>

* feat: warn instead of fail on user-defined dir label

Hard-failing on an existing dir label is a breaking change. Log a
deprecation warning for now and turn it into an error in a later release.

Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>

* refactor: apply go fix modernizers to dir selector code

Upstream CI now fails when go fix would rewrite code. Use maps.Copy in
injectLabel and drop the explicit embedded ReleaseSetSpec in test
literals.

Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>

* fix: tighten dir selector edge cases found in review

- Always shadow a user-defined dir label at match time, also when the
  release has no auto-populated value (remote or outside-root helmfiles).
  Previously the user value was matched there despite the warning.
- Skip sibling helmfiles entries that are directories, not only files.
- Warn about a dir label declared in a release template, and warn only
  once when it comes from a bases file.
- Share the root-relative path and at-or-below checks between the release
  filter and the traversal skip; a directory whose name merely starts with
  two dots is no longer treated as escaping the root.
- Log when the root directory cannot be resolved.
- Tests: require for setup steps, t.Chdir, shared app fixture.

Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>

* docs: sync dir selector CLI help, document anchor and traversal assumption

docs/cli.md still carried the long help text. Also spell out what the dir
value is relative to, that the traversal skip assumes nested helmfiles
stay below their parent's directory, and that user-defined dir labels are
ignored for releases without an auto-populated value as well.

Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>

* fix: complete dir selector values relative to the root helmfile

Completion listed directories relative to the working directory, while
dir values are relative to the root helmfile directory. Resolve the root
from -f or HELMFILE_FILE_PATH with the same logic the app uses, and offer
nothing for values the selector would reject (absolute paths, paths that
escape the root) or for a remote root helmfile.

Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>

* refactor(state): flatten nested conditionals in label filter parsing and matching

- LabelFilter.Match: extract labelMatches helper; positive/negative loops
  become single-level guards. Also removes the pre-existing empty
  then-branch (if !ok {} else if) in the negative-label loop and the
  redundant len()>0 guards around the loops
- positiveLabelsCompatibleWith: extract positiveLabelPairCompatible so the
  double loop holds one guard instead of a nested if chain
- ParseLabels: extract parseOneLabel (separator detection, dir= value
  validation, split) so the three-level regex/dir/error nesting becomes
  flat; drop the vestigial trailing 'var err error' return

No behavior change; state/app/cmd tests incl. -race and golangci-lint pass.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>
Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: yxxhero <aiopsclub@163.com>
2026-10-06 08:21:35 +08:00
pznamenskyandyxxhero c25d16a438 add trackLogsUntilReady flag (#2814)
* add trackLogsUntilReady flag

Signed-off-by: pznamensky <kompastver@gmail.com>

* refactor(kubedog): flatten nested conditionals in trackLogsUntilReady filter

- podLogFilter.update: replace nested if-found/if-uid block with a single
  combined guard plus a no-op delete
- podLogFilter.watch: extract cutoffTrackedKind predicate and
  watchNamespacePods helper; use RWTransactionErr (kubedog's own idiom)
  instead of capturing err in plain RWTransaction closures, removing the
  'if err == nil { inf.Run() }' nesting
- TrackResources: extract newStartupLogFilter guard-clause helper so the
  printer wiring stays flat
- add table tests for the new helpers

No behavior change; race tests and golangci-lint pass.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: pznamensky <kompastver@gmail.com>
Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: yxxhero <aiopsclub@163.com>
2026-10-04 08:49:05 +08:00
000a6fa660 fix: support escaped commas in --state-values-set (#2813)
* feat: support comma escape for values overwrites

Signed-off-by: zhengbayi <zhengbaiyi@sensetime.com>

* fix: harden state-values-set parsing and support \, in state-values-set-string

- Return a descriptive error instead of panicking when a --state-values-set
  assignment is missing '=', has an empty key, or ends with a trailing comma
- Support escaped commas in --state-values-set-string and preserve other
  backslashes (including trailing ones), aligning it with --state-values-set
  and Helm's --set/--set-string; quoted values keep working
- Allow empty values in --state-values-set-string for parity with
  --state-values-set
- Restructure parsing into guard-clause helpers with single-level
  conditionals; parse assignments via strings.Cut
- Extend table-driven tests: error cases, --state-values-set-string
  coverage, combined flags, and unit tests for the split/unescape helpers

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: zhengbayi <zhengbaiyi@sensetime.com>
Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: zhengbayi <zhengbaiyi@sensetime.com>
Co-authored-by: yxxhero <aiopsclub@163.com>
2026-10-03 15:31:04 +08:00
59 changed files with 3306 additions and 403 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
HELM_VERSION ?= v4.3.0
HELM_VERSION ?= v4.3.1
KUSTOMIZE_VERSION ?= v5.8.0
K8S_VERSION ?= v1.34.0
MINIKUBE_VERSION ?= v1.37.0
+8 -8
View File
@@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
helm-version: [v3.18.6, v3.22.0, v4.3.0]
helm-version: [v3.18.6, v3.22.1, v4.3.1]
steps:
- uses: actions/checkout@v7
with:
@@ -73,16 +73,16 @@ jobs:
env:
HELMFILE_HELM4: ${{ startsWith(matrix.helm-version, 'v4') && '1' || '0' }}
- name: Archive built binaries
if: matrix.helm-version == 'v4.3.0'
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.3.0'
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.3.0'
if: matrix.helm-version == 'v4.3.1'
run: ls -l helmfile diff-yamls dyff
integration_tests:
@@ -111,23 +111,23 @@ jobs:
plugin-secrets-version: 4.7.4
plugin-diff-version: 3.15.13
extra-helmfile-flags: '--enable-live-output'
- helm-version: v3.22.0
- helm-version: v3.22.1
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.4
plugin-diff-version: 3.15.13
extra-helmfile-flags: ''
- helm-version: v3.22.0
- helm-version: v3.22.1
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.4
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.3.0
- helm-version: v4.3.1
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.4
plugin-diff-version: 3.15.13
extra-helmfile-flags: ''
- helm-version: v4.3.0
- helm-version: v4.3.1
kustomize-version: v5.8.0
plugin-secrets-version: 4.7.4
plugin-diff-version: 3.15.13
+5
View File
@@ -5,6 +5,11 @@
- 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
+3 -3
View File
@@ -30,7 +30,7 @@ ENV HELM_CONFIG_HOME="${HELM_CONFIG_HOME}"
ARG HELM_DATA_HOME="${HOME}/.local/share/helm"
ENV HELM_DATA_HOME="${HELM_DATA_HOME}"
ARG HELM_VERSION="v4.3.0"
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="86584a54def73570558f66f5111cc53dfed56689637ae32c1201205d494f54fb" ;; \
"linux/arm64") HELM_SHA256="31c5794dd55c66a51e6b7d2e2ac7a114ae8b1de41ff1d9ba51748ac973b06a08" ;; \
"linux/amd64") HELM_SHA256="0403a88ae20913d11b10a2467b50087721fea7581736da6b9d3992f5357fe54b" ;; \
"linux/arm64") HELM_SHA256="8cf8f66ecbc252ac8f7f06cca68aba88508ddfde3a6a34feb1678ad1694b4f88" ;; \
esac && \
echo "${HELM_SHA256} ${HELM_FILENAME}" | sha256sum -c && \
echo Extracting ${HELM_FILENAME}... && \
+3 -3
View File
@@ -38,7 +38,7 @@ ENV HELM_CONFIG_HOME="${HELM_CONFIG_HOME}"
ARG HELM_DATA_HOME="${HOME}/.local/share/helm"
ENV HELM_DATA_HOME="${HELM_DATA_HOME}"
ARG HELM_VERSION="v4.3.0"
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="86584a54def73570558f66f5111cc53dfed56689637ae32c1201205d494f54fb" ;; \
"linux/arm64") HELM_SHA256="31c5794dd55c66a51e6b7d2e2ac7a114ae8b1de41ff1d9ba51748ac973b06a08" ;; \
"linux/amd64") HELM_SHA256="0403a88ae20913d11b10a2467b50087721fea7581736da6b9d3992f5357fe54b" ;; \
"linux/arm64") HELM_SHA256="8cf8f66ecbc252ac8f7f06cca68aba88508ddfde3a6a34feb1678ad1694b4f88" ;; \
esac && \
echo "${HELM_SHA256} ${HELM_FILENAME}" | sha256sum -c && \
echo Extracting ${HELM_FILENAME}... && \
+3 -3
View File
@@ -38,7 +38,7 @@ ENV HELM_CONFIG_HOME="${HELM_CONFIG_HOME}"
ARG HELM_DATA_HOME="${HOME}/.local/share/helm"
ENV HELM_DATA_HOME="${HELM_DATA_HOME}"
ARG HELM_VERSION="v4.3.0"
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="86584a54def73570558f66f5111cc53dfed56689637ae32c1201205d494f54fb" ;; \
"linux/arm64") HELM_SHA256="31c5794dd55c66a51e6b7d2e2ac7a114ae8b1de41ff1d9ba51748ac973b06a08" ;; \
"linux/amd64") HELM_SHA256="0403a88ae20913d11b10a2467b50087721fea7581736da6b9d3992f5357fe54b" ;; \
"linux/arm64") HELM_SHA256="8cf8f66ecbc252ac8f7f06cca68aba88508ddfde3a6a34feb1678ad1694b4f88" ;; \
esac && \
echo "${HELM_SHA256} ${HELM_FILENAME}" | sha256sum -c && \
echo Extracting ${HELM_FILENAME}... && \
+17
View File
@@ -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:
+3 -1
View File
@@ -16,6 +16,7 @@ func NewApplyCmd(globalCfg *config.GlobalImpl) *cobra.Command {
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,9 +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)")
"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"`)
+63 -3
View File
@@ -4,6 +4,8 @@ import (
stderrors "errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -16,8 +18,10 @@ 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"
)
@@ -97,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.
@@ -149,13 +157,64 @@ func commandSpanAttributes(cmdName string, g *config.GlobalImpl) []attribute.Key
}
}
// 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"`)
@@ -183,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.`)
+139
View File
@@ -1,9 +1,13 @@
package cmd
import (
"bytes"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -82,3 +86,138 @@ func TestRootCmdRegistersOtelTracingFlag(t *testing.T) {
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")
}
+4 -1
View File
@@ -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,9 +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)")
"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"`)
+29 -1
View File
@@ -34,11 +34,39 @@ helmfile apply --track-mode kubedog --track-timeout 300 --track-logs --track-log
- **`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`**: 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 log option enabled, `helmfile apply` and `helmfile sync` print collected logs every 10 seconds by default. Use `--track-logs-interval` to change this interval (for example, `1s` above). The minimum is `1s`. Deployment progress is checked for changes every 10 seconds.
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
+26 -1
View File
@@ -62,7 +62,8 @@ Flags:
-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).
@@ -76,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.
+6 -3
View File
@@ -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
@@ -254,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)
@@ -514,7 +515,9 @@ 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 | Print logs from tracked resources during deployment (every 10 seconds by default; see [`--track-logs-interval`](advanced-features.md#resource-tracking-with-kubedog)) |
| `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 |
+19
View File
@@ -215,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.
+1 -1
View File
@@ -2,7 +2,7 @@ Babel==2.17.0
click==8.1.2
ghp-import==2.0.2
gitdb==4.0.9
GitPython==3.1.59
GitPython==3.1.62
importlib-metadata==4.11.3
Jinja2==3.1.6
Markdown==3.8.1
+98 -93
View File
@@ -6,25 +6,25 @@ 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.33.6
github.com/aws/aws-sdk-go-v2/service/s3 v1.113.4
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/fatih/color v1.19.0
github.com/go-test/deep v1.1.1
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.4
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.30
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.42.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
@@ -34,33 +34,38 @@ 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.71.0
go.opentelemetry.io/otel v1.46.0
go.opentelemetry.io/otel/metric v1.46.0
go.opentelemetry.io/otel/sdk v1.46.0
go.opentelemetry.io/otel/sdk/metric v1.46.0
go.opentelemetry.io/otel/trace v1.46.0
go.opentelemetry.io/proto/otlp v1.11.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.23.0
golang.org/x/term v0.46.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.22.0
helm.sh/helm/v4 v4.3.0
k8s.io/apimachinery v0.37.0
k8s.io/client-go v0.37.0
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
// 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 (
@@ -102,7 +107,7 @@ require (
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
@@ -121,26 +126,26 @@ 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.47.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.20.6 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.1 // 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.5.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.4 // 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.1 // 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.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.51.1 // indirect
github.com/aws/smithy-go v1.28.1 // 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
@@ -184,26 +189,26 @@ require (
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v1.0.0 // indirect
github.com/go-openapi/errors v0.22.8 // indirect
github.com/go-openapi/jsonpointer 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.1 // indirect
github.com/go-openapi/swag v0.28.0 // indirect
github.com/go-openapi/swag/cmdutils v0.28.0 // indirect
github.com/go-openapi/swag/conv v0.29.1 // indirect
github.com/go-openapi/swag/fileutils v0.29.1 // indirect
github.com/go-openapi/swag/jsonutils v0.29.1 // indirect
github.com/go-openapi/swag/loading v0.29.1 // indirect
github.com/go-openapi/swag/mangling v0.29.1 // indirect
github.com/go-openapi/swag/netutils v0.28.0 // indirect
github.com/go-openapi/swag/pools v0.29.1 // indirect
github.com/go-openapi/swag/stringutils v0.29.1 // indirect
github.com/go-openapi/swag/typeutils v0.29.1 // indirect
github.com/go-openapi/swag/yamlutils v0.29.1 // 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
@@ -214,7 +219,7 @@ require (
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
@@ -224,7 +229,7 @@ require (
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.30.0 // 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
@@ -289,10 +294,10 @@ require (
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.2 // indirect
github.com/prometheus/common v0.70.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.21.1 // 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
@@ -301,7 +306,7 @@ require (
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.9.4 // 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
@@ -329,58 +334,58 @@ require (
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.71.0 // 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.70.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.22.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.22.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.46.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.46.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.68.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.22.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.46.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.46.0 // indirect
go.opentelemetry.io/otel/log v0.22.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.22.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.55.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/mod v0.39.0 // indirect
golang.org/x/net v0.58.0 // 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.48.0 // indirect
golang.org/x/text v0.41.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.49.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-20260825221802-da73d73af1c5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
google.golang.org/grpc v1.83.2 // 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.37.0 // indirect
k8s.io/apiextensions-apiserver v0.37.0 // indirect
k8s.io/apiserver v0.37.0 // indirect
k8s.io/cli-runtime v0.37.0 // indirect
k8s.io/component-base v0.37.0 // 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-20260721132016-d427ff9ee9ad // indirect
k8s.io/kubectl v0.37.0 // 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.4.2 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
+188 -190
View File
@@ -102,8 +102,8 @@ github.com/DelineaXPM/tss-sdk-go/v3 v3.1.0 h1:Z8Npn0lGxAjYMgR5Wu/Di8OCfD2e1A1pnV
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=
@@ -157,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.47.1 h1:uOIZnp4PK3ZhKI0dNrJrhTEsLxbpXHTAJlwoS1pvAtw=
github.com/aws/aws-sdk-go-v2 v1.47.1/go.mod h1:bttEH6JqnUL8LepvDVfdrds/fZ5bCIxzpe3abyUrhDU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 h1:GPRlPwz40I2B2VrBEASOA3Bi77NyeqejNLkifosX0rs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20/go.mod h1:g7PNzKcsOKWb4fkSRBA7BZVAS6Y8IcxzN+nRohhQ1Q8=
github.com/aws/aws-sdk-go-v2/config v1.33.6 h1:MBjkSTLczek/UgiK+EYPIoRTqE7gP8vtW3OFbFo7Nug=
github.com/aws/aws-sdk-go-v2/config v1.33.6/go.mod h1:grRAFzdAZJrwcbasJRg2MPvIrVjtlfXllHssN6+E1JE=
github.com/aws/aws-sdk-go-v2/credentials v1.20.6 h1:NpAFXCU7NzXNkdGK3zQTtsRJ+3v9tZQV0xcdRw8uBdw=
github.com/aws/aws-sdk-go-v2/credentials v1.20.6/go.mod h1:mcZCoiPnyMvP8VMNbygNX5lLqSlkYJIMPODylQMurOk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.1 h1:8gALAAmacnIXh+z6VkdDanv4/IkG5APdg4DZLDTmLog=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.1/go.mod h1:Z7IJhJU+poOdJjUR2wpyY21ossQ1XS/R3Lk9Msq5kM4=
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.5.4 h1:CLq4+8UHCI+ZZYl/EuJxXovaIVN2xeeT8JV+dsApQ5E=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.4/go.mod h1:Wv4q5sAM04xAMkoOedxLx2inVf6K5FdxYp+A61L+q/0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.4 h1:dD4MR81I7YkpEBRk6UP9rocC2QnT3qVuXwzlYTtfGEs=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.4/go.mod h1:EcXV1kAFd5XwSkDHlj94gnF3q5CkJyYiIJfH8N0VmrE=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.4 h1:7Wo47d/xn/7KttCSBd8EGYeZ7ULRFRkUHr6vkZPBzVQ=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.4/go.mod h1:tDB2IVC1xC3vX8o+6uRlzhTxP3g1b77CZXFX/oD2FnQ=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 h1:bAdDl/HkGCcGPoe25ToSHEw23VIxt6CT5fLcg111BKg=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19/go.mod h1:KaUzbLxv4CeSxh6ZCl9B4m7CuFenS8kUEaDs+f/DQr4=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.5 h1:/TYsZXdA8UTa+WCtCYSAJIr1vwl0+eho6TUgJGwFFO8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.5/go.mod h1:qPqp1Uwd/BqdhPufv6oem9j5J7HNsgc2V22dUiDPn+s=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.4 h1:29SvnfGhXjTl8ONxFwbj2rs6lbhiFXD2CgFQmbT/bXY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.4/go.mod h1:wm04I5DMuNVvZHFe/dHnUxincvNbbK7AiNBbYsQivek=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.4 h1:pPiWfgeNxqluKEph7hvU88kuGKBPOWzO+Dk9t2zqqNs=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.4/go.mod h1:YlwGoIUDG/3kBQbdNOVs/xKZ9J01G8e/6D1mRBj9uTk=
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.113.4 h1:n6kO3OlBvnDEksQpvBLbAldjHwGlu8kErvhHJkhlaRY=
github.com/aws/aws-sdk-go-v2/service/s3 v1.113.4/go.mod h1:9APRWGLFITKD+xzWSIyT9V7QV4bNlEuIieWlzXgGFlI=
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.1 h1:DzCCWLzcIRQ77F3DEUljud7bEjTgFOIKXP52NmVRyhU=
github.com/aws/aws-sdk-go-v2/service/signin v1.10.1/go.mod h1:xpo/geVldu8payT375WekctUzopG/hBU7miiqItMUlw=
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.1 h1:Umtl/0YZhng4xndfW3lKJrYYP7NLEjI6bGXVomwLcs0=
github.com/aws/aws-sdk-go-v2/service/sso v1.38.1/go.mod h1:rRD/dnm7q0HYE/I5TMaPgkWyyUGLcwuxHLABsLnQ3e0=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.1 h1:orIWdNiLgzrhu/11RcPPKO/SBzUUymbUQuZbSPImghg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.1/go.mod h1:skwM/xsbR/1ReUTesv9BhpJp1VjajR7DWQnuVLwiXsQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.51.1 h1:0HOqZXRvMytH6bFHVIc0oJX07sZjfhz0zXtjs6gdE8s=
github.com/aws/aws-sdk-go-v2/service/sts v1.51.1/go.mod h1:26zA0GhDrLo+yiLI2yXWxqB1PdsShfLikoI7GOEgugM=
github.com/aws/smithy-go v1.28.1 h1:R/nXH00c8qcfCzQVELtRw+eLQWtzv+VAIEFJ1/xxXlQ=
github.com/aws/smithy-go v1.28.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
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=
@@ -211,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=
@@ -263,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=
@@ -277,8 +275,8 @@ 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=
@@ -346,10 +344,10 @@ 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 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.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/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=
@@ -360,38 +358,38 @@ github.com/go-openapi/runtime/server-middleware v0.33.2 h1:BVFjAaW4Jh/kZ4QSsgDrG
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.1 h1:6yeRws56xbMR8c75R487iLfrREpYnmnBs4g554f5hdk=
github.com/go-openapi/strfmt v0.27.1/go.mod h1:7enjivZtYHpyqjJLetiAOh6McJziVnvIS/e7rX8/798=
github.com/go-openapi/swag v0.28.0 h1:xkgbOSKj6DZziNpyqRRAOt3GJGtgjgsd2RoyT30VWuw=
github.com/go-openapi/swag v0.28.0/go.mod h1:4qYnT3Cqr1p1VknOdPo70evN4rgQnAg6jwApHyxSGIg=
github.com/go-openapi/swag/cmdutils v0.28.0 h1:7TOeNtkYru1SG8Y34tDh9WBbLsMqGnptuxWiHREPZ4Q=
github.com/go-openapi/swag/cmdutils v0.28.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM=
github.com/go-openapi/swag/conv v0.29.1 h1:AC4Eh/5c/eUDOUCzzsRC9ghmFgOSBHeRMGIngY0ZUGA=
github.com/go-openapi/swag/conv v0.29.1/go.mod h1:S1X7/ZrBEZOC0Wc8AGxjbcGS92l3WEjA7aPtpl+RaqM=
github.com/go-openapi/swag/fileutils v0.29.1 h1:ZcPzMceVhU1WPbK6N1G6sNQKdd1CWJlf3cA08UHuoM0=
github.com/go-openapi/swag/fileutils v0.29.1/go.mod h1:/wofKYckbtRl2p3+EwQsosie5CT1B38+dQ+PS579BzI=
github.com/go-openapi/swag/jsonutils v0.29.1 h1:AFCxs0eQZ24/QyfhVHM2t49rMz7Vv3XCsZQI6yrNy+c=
github.com/go-openapi/swag/jsonutils v0.29.1/go.mod h1:u3+sCfJpttDpcmS5kpm0yxL6GK0eWgODsx8Yw8fcqNM=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.1 h1:BiiXE31Bx9SfpsMmOQj5KYpUhTZBpLVriVhJDuLuY2o=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.1/go.mod h1:julgTUKZ9/D0j6O7GKajmRs+812FWxQg/mMpGunWSjg=
github.com/go-openapi/swag/loading v0.29.1 h1:FCv5fG8UhTdDJa2R7w+5O9Ekpcbw7tt0nFWvmDKGBjc=
github.com/go-openapi/swag/loading v0.29.1/go.mod h1:N0ESuem4p2oedKal8EJhciqnJ9Q9Wmt83L1CRB3Fouw=
github.com/go-openapi/swag/mangling v0.29.1 h1:lHALtvYCdxVnRl4GrHmFPwfBTZYIObqdGNSKyu/8D6I=
github.com/go-openapi/swag/mangling v0.29.1/go.mod h1:SAop9pB7PUjQ/CGCNf/JmCKTRK+GDO+RqE9UHqC/N6s=
github.com/go-openapi/swag/netutils v0.28.0 h1:YXN6TALEi2pzts8/8GNm6T61HTAZsieukGZidap989k=
github.com/go-openapi/swag/netutils v0.28.0/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ=
github.com/go-openapi/swag/pools v0.29.1 h1:NRogYxdEW9SjRM4mkAOji9iefO4MRXq3p/ZJcoQbUKg=
github.com/go-openapi/swag/pools v0.29.1/go.mod h1:leDcaghjkRAhCuCRv9NfJU5f0mjoU3cT/XZObhMk3pc=
github.com/go-openapi/swag/stringutils v0.29.1 h1:1ykunK7iJQk1uOO7+oUH1ukbsK85fFCOiCFMOVSY+F0=
github.com/go-openapi/swag/stringutils v0.29.1/go.mod h1:7fSqZ+z8Qc0tOfAAK0jVa5qFGrnIlRi6n7NeGGrr1vc=
github.com/go-openapi/swag/typeutils v0.29.1 h1:Nzv9nhnlLCRBPQqfOX+7lB6Guju370or8StT+lIOf6M=
github.com/go-openapi/swag/typeutils v0.29.1/go.mod h1:hxpgDZJVBkBsi/d3MIUosafoFdE5exaQRmVp0zwu3YE=
github.com/go-openapi/swag/yamlutils v0.29.1 h1:69w3tsBajm7MR/fejLy7HD/3J68Ys1SeeZMEzZ3w2sk=
github.com/go-openapi/swag/yamlutils v0.29.1/go.mod h1:rgsp3vT/QdWzKwn43CigDwjOGIenPyTZMKnxEM8jZOA=
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.7.0 h1:bycOreEj6wfBvijg3YFogZ/sFjTCDmQnwSodSzHa3X8=
github.com/go-openapi/testify/v2 v2.7.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
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=
@@ -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=
@@ -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=
@@ -476,8 +474,8 @@ github.com/googleapis/gax-go/v2 v2.24.0 h1:myMaPYyF9MecEmvQqMqomIwn9t/4KCZN9qnws
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.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY=
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.4 h1:ra70o0kGsM9/T2PksKr55MrmLClR1ouLcWxexuLvdTQ=
github.com/hashicorp/go-getter/v2 v2.2.4/go.mod h1:aZFhc+xmYUqWNxvBhlVi5ZJinWvSjelAFIeCDE5uIkM=
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=
@@ -601,8 +599,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
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.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk=
github.com/mattn/go-runewidth v0.0.30/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=
@@ -688,14 +686,14 @@ github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjz
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.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY=
github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc=
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.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
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=
@@ -718,8 +716,8 @@ 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.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.42.1 h1:9nK2UgDVVSIyoEUNDeWqu3Ttj8EqCO6FT8HK0Cv8VEo=
github.com/sashabaranov/go-openai v1.42.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
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=
@@ -727,8 +725,8 @@ 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=
@@ -830,58 +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.71.0 h1:9qgxsFLskbDMXl8WMqThoF6w8yGJgCumn9qRc67OmnI=
go.opentelemetry.io/contrib/bridges/prometheus v0.71.0/go.mod h1:2rCjF4F2siiTeLCzJsaGZ3CK0XIoimCSKXEBPdv+Je0=
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.71.0 h1:VCsJbp0YLyPtx2tu5Vgv2a2/qLoaMCj8hT2uZ34+Mx0=
go.opentelemetry.io/contrib/exporters/autoexport v0.71.0/go.mod h1:qxZqn7e10f6ajmMCkg/47rMS7qQYfaOl2nj/4aytHUQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0 h1:oECp5f+hN7nkwjU/8BxQ/q23bGPb8FIrD839owX222E=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0/go.mod h1:DqEFwLumhzMBDQv9PcWbyoDxHI/4lAk6CM4nJBH39sc=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 h1:LMuyCAyfalSjDyjdC65nK6N0zoTT63+E/u95X0JovZI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0/go.mod h1:085m8qbm4hgc8rZWGDEa4vmyyo2c3nPxUslYUKUIU04=
go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.22.0 h1:Bu39F5tzJct+f2IZbB8989fwyTps3c8e7EsUQsz+vs8=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.22.0/go.mod h1:dJUwod88EsFgYCqrDHaSPzhiY9pBUpt0d85/qSfua7k=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.22.0 h1:lYk7RmxdLK865qLwibroNGldHa1U7SWKYYvNjlK7PIo=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.22.0/go.mod h1:6GvlND0H0xdUJanOtIAn0xfwLkauh1tmsYEEVSMDdqY=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0 h1:qkDYCAFiZXLcs1L4aY+tP2wguQ4kURANqHOQMA2et2s=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0/go.mod h1:tkipS4DRzmpAmvg+Gw4++O1IdDq6TVDnvnYU6cmbQVs=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.46.0 h1:AP23h/mFgb/lc7tdck1Kfn9qxsM8TAeNPCU5C3pzaps=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.46.0/go.mod h1:K4EqCe1b4kGk5WR690ntg9LaBfsPoV32FwthbyoptuA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 h1:OFnwLJr+pF3iHrlGSzbxyuo6/6HyBlnlN1CWEJmBVcw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0/go.mod h1:716wFneO0ov19A2beH5hjfh9AK5z/VWNAtDijp1Y0/g=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 h1:w53CDeOA/Kurp7yRsegSr6pbbr759dOvJ+yNmWM6Hxs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0/go.mod h1:BOmGMCbAtvcJiSJ+hLuhgPLdDbimnraSl8irz3iY8sY=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.46.0 h1:KrC1YrQeSt46ITMWAbgQx1M1eV1/1TKzttrBzymPmss=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.46.0/go.mod h1:zDSEzoEqsOrgBeGvH66KRgxh90VonFyJqBHA0Pk3+rM=
go.opentelemetry.io/otel/exporters/prometheus v0.68.0 h1:QOf2IftqQwITVRJpnn0M7M9ZCbgWfxz4P7i9C9yc2N4=
go.opentelemetry.io/otel/exporters/prometheus v0.68.0/go.mod h1:bgSvqu2TWGXiz7yr5UTMfObH8oqxJWHTnubQ3ef9BO4=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.22.0 h1:kvMAiLEudKmk+CSG+iYbU8vTUGNNDaf/V09OO5lrTwI=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.22.0/go.mod h1:L9Dlksri+MdT1cb2gIiA1cJJYW3Y92ipvDjNxYEyaDI=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.46.0 h1:PR9eAf7o0dQs3hshZNZpE9aW2dXWX/KdDf6pJilVD3U=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.46.0/go.mod h1:2Z4KyNdH1uuzivdinyfGsxzNNT/Rl45pwtVwfYVI0xk=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.46.0 h1:KdRxPiAoMptR3vfWzvjjvutTsSiwbC2uG0496rzZNfo=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.46.0/go.mod h1:K/qSA+3G7Eovxi4K09wzrAgkWRnosS0DAOZeEpve7sM=
go.opentelemetry.io/otel/log v0.22.0 h1:5DBNnfvaJ6CVdkJ+Jle8Tzs50aSSv49TXGj9XRsEYw0=
go.opentelemetry.io/otel/log v0.22.0/go.mod h1:gzOt/R67vF2GniAqWu8Qv0SXy89f71muHcrkz76PCdc=
go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
go.opentelemetry.io/otel/metric/x v0.68.0 h1:TA/cBT23D3MnxYPwHL7YFOdYGdx0A0v+s7Mzotpd1dU=
go.opentelemetry.io/otel/metric/x v0.68.0/go.mod h1:agudOmvWhwUTjgibWDzxD2PoWYnpw5Ht5jISYOD2Hd4=
go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI=
go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM=
go.opentelemetry.io/otel/sdk/log v0.22.0 h1:PRL+s6P63XT4E/bheEflopPUpVxuvANqZwtt89yhoGk=
go.opentelemetry.io/otel/sdk/log v0.22.0/go.mod h1:JNp0sBELrjCTcu5W3GzABVypeU6vDJjBS+X0JISuz+g=
go.opentelemetry.io/otel/sdk/log/logtest v0.22.0 h1:infPnfNrhCNgOUZRs3gWUg8vhoBUHihq02gwK05gzlg=
go.opentelemetry.io/otel/sdk/log/logtest v0.22.0/go.mod h1:gkQZA3z15Bv3KU9vigBTi8dFechSozRP7v94X4VZv+s=
go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE=
go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4=
go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
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.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
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-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
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.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74=
golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY=
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,8 +937,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
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.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
@@ -950,8 +948,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
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.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
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.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
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.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
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.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
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,8 +1013,8 @@ 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.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
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=
@@ -1031,16 +1029,16 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
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-20260825221802-da73d73af1c5 h1:izFU9hz7aeLI/Mi1J0991ae+xcwRLr7hTqWnB/9aIIU=
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5/go.mod h1:3LhxRw4YYkf+ylAfgaY9JlVLFKhokkCV8duhLLe7+t0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 h1:1VUiZAXyC+zmiFYi+WLtBzr68Cj8wOofHjjrA/kkizc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
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.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
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=
@@ -1068,32 +1066,32 @@ 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.22.0 h1:n1if5iQgHYsujPAcO5oonH/KiSNUmqmUF0iGVjSF3AU=
helm.sh/helm/v3 v3.22.0/go.mod h1:EVl/147Ai2z13JOykbO1Jn9HRaXyU7uHlbAJmODq/Fg=
helm.sh/helm/v4 v4.3.0 h1:wLRTNXzy96ro7waurWMsymlUPaCQOj5nokpJZJNen/w=
helm.sh/helm/v4 v4.3.0/go.mod h1:p6SMo6BMyg+4H52fJXvRNg1To0a0KGfiPxjVEcHHSk0=
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.37.0 h1:zRMQ3+/LIE5oZ0tVvXwYHC+dIkSP5cjNWju7AZU1LOI=
k8s.io/apiextensions-apiserver v0.37.0/go.mod h1:HU0PfSBwchHL5iDau6jjt9zU6ryWkDDlaVUiq91NK80=
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.37.0 h1:TXg7OxsOWrAH8J4Zi/gBAZuMw1Dfdd+6cca2h4qjRqo=
k8s.io/apiserver v0.37.0/go.mod h1:OddHDF4gy9qyIb8o/3+qaeP6S0vEObWLgOygVqXksv0=
k8s.io/cli-runtime v0.37.0 h1:U3XakUeirBQJMz5688r04z74SIHSE7V5SIZ6Ho5JyBM=
k8s.io/cli-runtime v0.37.0/go.mod h1:qiQMFkKwFFuPH6zy953On+nc3qfpEHAIDrJmAuRz5Vg=
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.37.0 h1:3SdSa4+itMdFTDFTeR8CxKGmSTSMXFlKL4ky8OqjguM=
k8s.io/component-base v0.37.0/go.mod h1:LjOebp4R9y6LODWZQv102ZQxGheLcDO2ZJLAw6bbh4I=
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-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.0 h1:cici6hiofx93ASldmprDmZF55SfhVt4o3HniltVLjTc=
k8s.io/kubectl v0.37.0/go.mod h1:RSeEl8e/yqDx6srG8Azr0uAtVPNIZljA0PNh9HCBcdg=
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=
@@ -1104,8 +1102,8 @@ 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.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3CIki3aW8c4Q=
+38 -5
View File
@@ -67,6 +67,9 @@ type App struct {
helms map[helmKey]helmexec.Interface
helmsMutex sync.Mutex
rootHelmfileDir string
rootHelmfileDirOnce sync.Once
ctx goContext.Context
}
@@ -189,6 +192,7 @@ func (a *App) Diff(c DiffConfigProvider) error {
Validate: c.Validate(),
Concurrency: c.Concurrency(),
IncludeTransitiveNeeds: c.IncludeNeeds(),
HelmOCIPlainHTTP: a.HelmOCIPlainHTTP,
PrefetchSharedRemoteCharts: true,
}, func() []error {
msg, matched, affected, errs = a.diff(run, c)
@@ -347,6 +351,7 @@ func (a *App) Lint(c LintConfigProvider) error {
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...)
@@ -390,6 +395,7 @@ func (a *App) Unittest(c UnittestConfigProvider) error {
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...)
@@ -470,6 +476,7 @@ func (a *App) Fetch(c FetchConfigProvider) error {
OutputDir: c.OutputDir(),
OutputDirTemplate: c.OutputDirTemplate(),
Concurrency: c.Concurrency(),
HelmOCIPlainHTTP: a.HelmOCIPlainHTTP,
}, func() []error {
if c.WriteOutput() {
for i := range run.state.Releases {
@@ -530,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 {
@@ -589,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,
@@ -652,6 +661,7 @@ func (a *App) Destroy(c DestroyConfigProvider) error {
AllowFailedReleases: c.AllowFailedReleases(),
SkipDeps: c.SkipDeps(),
Concurrency: c.Concurrency(),
HelmOCIPlainHTTP: a.HelmOCIPlainHTTP,
DeleteWait: c.DeleteWait(),
DeleteTimeout: c.DeleteTimeout(),
}, func() []error {
@@ -682,6 +692,7 @@ func (a *App) Test(c TestConfigProvider) error {
AllowFailedReleases: c.AllowFailedReleases(),
SkipDeps: c.SkipDeps(),
Concurrency: c.Concurrency(),
HelmOCIPlainHTTP: a.HelmOCIPlainHTTP,
}, func() []error {
errs = a.test(run, c)
return errs
@@ -1087,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
@@ -1168,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
@@ -1327,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)
@@ -1638,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,
}
@@ -2124,6 +2155,7 @@ 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(),
@@ -2622,6 +2654,7 @@ 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(),
+5
View File
@@ -2557,6 +2557,7 @@ type applyConfig struct {
trackMode string
trackTimeout int
trackLogs bool
trackLogsUntilReady bool
trackLogsInterval time.Duration
trackFailOnError bool
@@ -2802,6 +2803,10 @@ func (a applyConfig) TrackLogs() bool {
return a.trackLogs
}
func (a applyConfig) TrackLogsUntilReady() bool {
return a.trackLogsUntilReady
}
func (a applyConfig) TrackFailedLogs() bool {
return false
}
+2
View File
@@ -103,6 +103,7 @@ type ApplyConfigProvider interface {
TrackMode() string
TrackTimeout() int
TrackLogs() bool
TrackLogsUntilReady() bool
TrackFailedLogs() bool
TrackLogsInterval() time.Duration
HelmStuckGrace() int
@@ -148,6 +149,7 @@ type SyncConfigProvider interface {
TrackMode() string
TrackTimeout() int
TrackLogs() bool
TrackLogsUntilReady() bool
TrackFailedLogs() bool
TrackLogsInterval() time.Duration
HelmStuckGrace() int
+152
View File
@@ -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
}
+360
View File
@@ -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 -1
View File
@@ -20,7 +20,7 @@ import (
const (
HelmRequiredVersion = "v3.18.6" // Minimum required version (supports Helm 3.x and 4.x)
HelmDiffRecommendedVersion = "v3.15.13"
HelmRecommendedVersion = "v4.3.0" // Recommended Helm 4 version
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"
+14 -1
View File
@@ -4,6 +4,8 @@ import (
"fmt"
"slices"
"time"
"github.com/spf13/cobra"
)
// ApplyOptoons is the options for the apply command
@@ -94,6 +96,8 @@ 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.
@@ -119,6 +123,7 @@ func NewApplyOptions() *ApplyOptions {
type ApplyImpl struct {
*GlobalImpl
*ApplyOptions
Cmd *cobra.Command
}
// NewApplyImpl creates a new ApplyImpl
@@ -344,13 +349,21 @@ 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 kubedog log output interval.
// 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
}
+56
View File
@@ -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
View File
@@ -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()
}
+242
View File
@@ -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))
})
}
}
+14 -1
View File
@@ -4,6 +4,8 @@ import (
"fmt"
"slices"
"time"
"github.com/spf13/cobra"
)
// defaultTrackLogsInterval is the default --track-logs-interval shared by
@@ -67,6 +69,8 @@ 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.
@@ -108,6 +112,7 @@ func NewSyncOptions() *SyncOptions {
type SyncImpl struct {
*GlobalImpl
*SyncOptions
Cmd *cobra.Command
}
// NewSyncImpl creates a new SyncImpl
@@ -257,13 +262,21 @@ 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 kubedog log output interval.
// 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
}
+56
View File
@@ -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())
})
}
}
+157
View File
@@ -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
}
+206
View File
@@ -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)
})
}
}
+9
View File
@@ -34,6 +34,9 @@ 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.
@@ -81,6 +84,12 @@ func (o *TrackOptions) WithLogsInterval(interval time.Duration) *TrackOptions {
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
+5
View File
@@ -187,6 +187,7 @@ type progressPrinter struct {
logStore *kdutil.Concurrent[*logstore.LogStore]
skipLogs bool
failedLogsOnly bool
podLogFilter *podLogFilter
gates *gateStatuses
skipped *skippedKeys
useColor bool
@@ -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,
+89
View File
@@ -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"
)
@@ -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:"))
}
+5
View File
@@ -454,6 +454,11 @@ func (t *Tracker) TrackResources(ctx context.Context, resources []*resource.Reso
gateStatuses := newGateStatuses()
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
+7
View File
@@ -32,6 +32,7 @@ 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)
}
@@ -57,6 +58,12 @@ func TestTrackOptions_WithLogsInterval(t *testing.T) {
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.
+6
View File
@@ -223,6 +223,12 @@ func (c *StateCreator) ParseAndLoad(content []byte, baseDir, file string, envNam
}
state.RenderedValues = vals
if evaluateBases {
for _, w := range state.reservedLabelWarnings() {
c.logger.Warnf("WARNING: %s", w)
}
}
return state, nil
}
+12 -4
View File
@@ -75,7 +75,7 @@ func (st *HelmState) appendLabelsFlags(flags []string, helm helmexec.Interface,
case syncReleaseLabels:
isSyncReleaseLabels = true
// Check if SyncReleaseLabels is true in HelmDefaults.
case st.HelmDefaults.SyncReleaseLabels != nil && *st.HelmDefaults.SyncReleaseLabels:
case release.SyncReleaseLabels == nil && st.HelmDefaults.SyncReleaseLabels != nil && *st.HelmDefaults.SyncReleaseLabels:
isSyncReleaseLabels = true
}
if isSyncReleaseLabels {
@@ -185,8 +185,8 @@ func (st *HelmState) shouldSkipSchemaValidation(release *ReleaseSpec, skipSchema
// Check if skipSchemaValidation argument is true.
case skipSchemaValidation:
return true
// Check if SkipSchemaValidation is true in HelmDefaults.
case st.HelmDefaults.SkipSchemaValidation != nil && *st.HelmDefaults.SkipSchemaValidation:
// Check if SkipSchemaValidation is true in HelmDefaults and not set in the release spec.
case release.SkipSchemaValidation == nil && st.HelmDefaults.SkipSchemaValidation != nil && *st.HelmDefaults.SkipSchemaValidation:
return true
default:
return false
@@ -313,6 +313,11 @@ func (st *HelmState) buildReleaseTracker(release *ReleaseSpec, opts *SyncOpts, u
trackLogs = opts.TrackLogs
}
trackLogsUntilReady := release.TrackLogsUntilReady != nil && *release.TrackLogsUntilReady
if release.TrackLogsUntilReady == nil && opts != nil {
trackLogsUntilReady = opts.TrackLogsUntilReady
}
trackFailedLogs := release.TrackFailedLogs != nil && *release.TrackFailedLogs
if release.TrackFailedLogs == nil && opts != nil {
trackFailedLogs = opts.TrackFailedLogs
@@ -327,11 +332,14 @@ func (st *HelmState) buildReleaseTracker(release *ReleaseSpec, opts *SyncOpts, u
trackOpts := kubedog.NewTrackOptions().
WithTimeout(timeout).
WithLogs(trackLogs).
WithLogsUntilReady(trackLogsUntilReady).
WithFailedLogsOnly(trackFailedLogs).
WithFilterConfig(filterConfig).
WithColor(useColor)
if opts != nil && opts.TrackLogsInterval > 0 {
trackOpts.WithLogsInterval(opts.TrackLogsInterval)
} else if release.TrackLogsInterval != nil {
trackOpts.WithLogsInterval(*release.TrackLogsInterval)
}
tracker, err := kubedog.NewTracker(&kubedog.TrackerConfig{
@@ -827,7 +835,7 @@ func (st *HelmState) appendTakeOwnershipFlagsForUpgrade(flags []string, helm hel
flags = append(flags, "--take-ownership")
case takeOwnership:
flags = append(flags, "--take-ownership")
case st.HelmDefaults.TakeOwnership != nil && *st.HelmDefaults.TakeOwnership:
case release.TakeOwnership == nil && st.HelmDefaults.TakeOwnership != nil && *st.HelmDefaults.TakeOwnership:
flags = append(flags, "--take-ownership")
}
return flags
+211
View File
@@ -2,14 +2,69 @@ package state
import (
"bytes"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/helmfile/helmfile/pkg/helmexec"
"github.com/helmfile/helmfile/pkg/testutil"
"github.com/helmfile/helmfile/pkg/yaml"
)
func TestAppendLabelsFlags(t *testing.T) {
enabled := true
disabled := false
tests := []struct {
name string
defaults *bool
release *bool
fromCLI bool
expected []string
}{
{
name: "release false overrides helm defaults",
defaults: &enabled,
release: &disabled,
expected: []string{},
},
{
name: "helm defaults applies when release is unset",
defaults: &enabled,
expected: []string{"--labels", "app=test"},
},
{
name: "release true overrides helm defaults",
defaults: &disabled,
release: &enabled,
expected: []string{"--labels", "app=test"},
},
{
name: "does not append labels when unset",
expected: []string{},
},
{
name: "cli flag overrides release false",
release: &disabled,
fromCLI: true,
expected: []string{"--labels", "app=test"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st := &HelmState{HelmDefaults: HelmSpec{SyncReleaseLabels: tt.defaults}}
release := &ReleaseSpec{SyncReleaseLabels: tt.release, Labels: map[string]string{"app": "test"}}
got := st.appendLabelsFlags([]string{}, testutil.NewVersionHelmExec("3.13.0"), release, tt.fromCLI)
require.Equal(t, tt.expected, got)
})
}
}
func TestAppendWaitForJobsFlags(t *testing.T) {
tests := []struct {
name string
@@ -524,6 +579,48 @@ func TestAppendTakeOwnershipFlagsForUpgrade(t *testing.T) {
expected: []string{"--take-ownership"},
},
},
{
name: "release false overrides helm defaults",
args: args{
flags: []string{},
helm: testutil.NewVersionHelmExec("3.17.0"),
helmSpec: HelmSpec{
TakeOwnership: &[]bool{true}[0],
},
release: &ReleaseSpec{
TakeOwnership: &[]bool{false}[0],
},
opt: &SyncOpts{},
expected: []string{},
},
},
{
name: "take-ownership from helm defaults",
args: args{
flags: []string{},
helm: testutil.NewVersionHelmExec("3.17.0"),
helmSpec: HelmSpec{
TakeOwnership: &[]bool{true}[0],
},
release: &ReleaseSpec{},
opt: &SyncOpts{},
expected: []string{"--take-ownership"},
},
},
{
name: "cmd flag overrides release false",
args: args{
flags: []string{},
helm: testutil.NewVersionHelmExec("3.17.0"),
release: &ReleaseSpec{
TakeOwnership: &[]bool{false}[0],
},
opt: &SyncOpts{
TakeOwnership: true,
},
expected: []string{"--take-ownership"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -797,3 +894,117 @@ func TestAppendServerSideFlagsForUpgrade(t *testing.T) {
})
}
}
func TestBuildReleaseTracker_LogsUntilReady(t *testing.T) {
kubeconfig := filepath.Join(t.TempDir(), "kubeconfig")
require.NoError(t, os.WriteFile(kubeconfig, []byte(`apiVersion: v1
kind: Config
clusters:
- name: test
cluster:
server: http://127.0.0.1:1
contexts:
- name: test
context:
cluster: test
current-context: test
`), 0600))
tests := []struct {
name string
release string
opts *SyncOpts
want bool
}{
{
name: "disabled by default",
},
{
name: "cli enables startup logs",
opts: &SyncOpts{TrackLogs: true, TrackLogsUntilReady: true},
want: true,
},
{
name: "release enables startup logs",
release: "trackLogs: true\ntrackLogsUntilReady: true\n",
want: true,
},
{
name: "release false overrides cli true",
release: "trackLogsUntilReady: false\n",
opts: &SyncOpts{TrackLogs: true, TrackLogsUntilReady: true},
},
{
name: "release true overrides cli false",
release: "trackLogsUntilReady: true\n",
opts: &SyncOpts{TrackLogs: true},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var release ReleaseSpec
require.NoError(t, yaml.Unmarshal([]byte(tt.release), &release))
st := &HelmState{kubeconfig: kubeconfig}
_, opts, err := st.buildReleaseTracker(&release, tt.opts, false)
require.NoError(t, err)
require.Equal(t, tt.want, opts.LogsUntilReady)
})
}
}
func TestBuildReleaseTracker_LogsInterval(t *testing.T) {
kubeconfig := filepath.Join(t.TempDir(), "kubeconfig")
require.NoError(t, os.WriteFile(kubeconfig, []byte(`apiVersion: v1
kind: Config
clusters:
- name: test
cluster:
server: http://127.0.0.1:1
contexts:
- name: test
context:
cluster: test
current-context: test
`), 0600))
tests := []struct {
name string
release string
opts *SyncOpts
want time.Duration
}{
{
name: "10s default when unset",
want: 10 * time.Second,
},
{
name: "explicit cli interval overrides release setting",
release: "trackLogsInterval: 2s\n",
opts: &SyncOpts{TrackLogsInterval: 30 * time.Second},
want: 30 * time.Second,
},
{
name: "release interval used when cli flag absent",
release: "trackLogsInterval: 2s\n",
want: 2 * time.Second,
},
{
name: "cli interval used when release unset",
opts: &SyncOpts{TrackLogsInterval: 3 * time.Second},
want: 3 * time.Second,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var release ReleaseSpec
require.NoError(t, yaml.Unmarshal([]byte(tt.release), &release))
st := &HelmState{kubeconfig: kubeconfig}
_, opts, err := st.buildReleaseTracker(&release, tt.opts, false)
require.NoError(t, err)
require.Equal(t, tt.want, opts.LogsInterval)
})
}
}
+18
View File
@@ -8,6 +8,7 @@ import (
func TestAppendSkipSchemaValidationFlagToChartifyTemplateArgs(t *testing.T) {
enable := true
disable := false
tests := []struct {
name string
@@ -32,6 +33,15 @@ func TestAppendSkipSchemaValidationFlagToChartifyTemplateArgs(t *testing.T) {
release: &ReleaseSpec{},
want: "--skip-schema-validation",
},
{
name: "release setting disables helm defaults",
defaults: HelmSpec{
SkipSchemaValidation: &enable,
},
release: &ReleaseSpec{
SkipSchemaValidation: &disable,
},
},
{
name: "appends flag to existing args",
release: &ReleaseSpec{
@@ -63,6 +73,14 @@ func TestAppendSkipSchemaValidationFlagToChartifyTemplateArgs(t *testing.T) {
templateArgs: "--kube-context default",
want: "--kube-context default --skip-schema-validation",
},
{
name: "cli setting overrides release setting",
release: &ReleaseSpec{
SkipSchemaValidation: &disable,
},
fromCLI: true,
want: "--skip-schema-validation",
},
{
name: "does not add flag when disabled",
release: &ReleaseSpec{},
+191 -34
View File
@@ -2,6 +2,9 @@ package state
import (
"fmt"
"maps"
"path"
"path/filepath"
"regexp"
"slices"
"strings"
@@ -22,32 +25,121 @@ type LabelFilter struct {
// Match will match a release that has the same labels as the filter
func (l LabelFilter) Match(r ReleaseSpec) bool {
if len(l.positiveLabels) > 0 {
for _, element := range l.positiveLabels {
k := element[0]
v := element[1]
if rVal, ok := r.Labels[k]; !ok {
return false
} else if rVal != v {
return false
}
for _, element := range l.positiveLabels {
if !labelMatches(r.Labels, element[0], element[1]) {
return false
}
}
if len(l.negativeLabels) > 0 {
for _, element := range l.negativeLabels {
k := element[0]
v := element[1]
if rVal, ok := r.Labels[k]; !ok {
} else if rVal == v {
return false
}
for _, element := range l.negativeLabels {
if labelMatches(r.Labels, element[0], element[1]) {
return false
}
}
return true
}
// labelMatches reports whether the release's labels satisfy the single k=v
// constraint: positive callers require a match, negative callers require no
// match. The reserved "dir" key matches by directory prefix instead of
// strict equality.
func labelMatches(labels map[string]string, k, v string) bool {
if k == DirLabel {
return matchDirPrefix(labels[DirLabel], v)
}
val, ok := labels[k]
return ok && val == v
}
// DirLabel is the reserved selector key for path-based release filtering.
// Releases get this label injected at match time with the directory of their
// defining helmfile, relative to the root helmfile. It uses directory-prefix
// semantics in LabelFilter rather than the strict equality of other keys.
const DirLabel = "dir"
// withDirLabel returns r with the "dir" label set to the auto-populated dir
// for matching, without mutating the source release or surfacing the value in
// user-facing label output. A user-defined "dir" label is always shadowed:
// when dir is empty (remote or outside-root helmfiles) it is dropped, so such
// releases never take part in dir-based filtering.
func withDirLabel(r ReleaseSpec, dir string) ReleaseSpec {
if _, userDefined := r.Labels[DirLabel]; dir == "" && !userDefined {
return r
}
cloned := r
cloned.Labels = make(map[string]string, len(r.Labels)+1)
maps.Copy(cloned.Labels, r.Labels)
if dir == "" {
delete(cloned.Labels, DirLabel)
} else {
cloned.Labels[DirLabel] = dir
}
return cloned
}
// matchDirPrefix reports whether a release's dirLabel falls under the user's
// target value, using directory-prefix semantics: the label must equal the
// target or live under target/. Inputs are normalized so trailing slashes and
// `./` prefixes do not matter. Empty dirLabel never matches (releases from
// remote helmfiles or outside-root branches have no anchor).
func matchDirPrefix(dirLabel, value string) bool {
if dirLabel == "" {
return false
}
return dirAtOrBelow(NormalizeDirValue(dirLabel), NormalizeDirValue(value))
}
func dirAtOrBelow(dir, ancestor string) bool {
return dir == ancestor || strings.HasPrefix(dir, ancestor+"/")
}
func escapesRoot(normalizedDir string) bool {
return normalizedDir == ".." || strings.HasPrefix(normalizedDir, "../")
}
// DirRelativeToRoot returns absDir relative to rootDir in normalized slash
// form, or false when absDir lies outside rootDir.
func DirRelativeToRoot(rootDir, absDir string) (string, bool) {
rel, err := filepath.Rel(rootDir, absDir)
if err != nil {
return "", false
}
rel = NormalizeDirValue(rel)
if escapesRoot(rel) {
return "", false
}
return rel, true
}
// NormalizeDirValue canonicalizes a dir= selector value or a "dir" auto-label
// to slash form with no trailing slash and no redundant elements. Empty input
// stays empty. Examples: `apps/x/` → `apps/x`, `./apps/x` → `apps/x`,
// `apps//x` → `apps/x`, `.` → `.`.
func NormalizeDirValue(v string) string {
if v == "" {
return ""
}
return path.Clean(filepath.ToSlash(v))
}
// PositiveDirTargets parses one selector group string and returns the
// normalized positive dir= values declared in it. Returns the same error as
// ParseLabels on malformed input so callers can choose to skip the group.
// Used by traversal-skip logic to peek at dir constraints without exposing
// LabelFilter internals.
func PositiveDirTargets(selector string) ([]string, error) {
lf, err := ParseLabels(selector)
if err != nil {
return nil, err
}
var dirs []string
for _, kv := range lf.positiveLabels {
if kv[0] == DirLabel {
dirs = append(dirs, NormalizeDirValue(kv[1]))
}
}
return dirs, nil
}
// SelectorsAreCompatible checks whether any pair of selectors from two sets could
// potentially match the same release. It compares only positive labels (key=value):
// two selectors conflict if they require the same key to have different values.
@@ -92,11 +184,13 @@ func parseLabelFilters(selectors []string) ([]LabelFilter, error) {
}
// positiveLabelsCompatibleWith returns true if the positive labels of two filters
// do not conflict (i.e., no shared key with a different value).
// do not conflict (i.e., no shared key with a different value). The "dir" key
// uses directory-prefix semantics, so two `dir=` constraints are compatible
// when one path is at or below the other; only siblings genuinely conflict.
func (l LabelFilter) positiveLabelsCompatibleWith(other LabelFilter) bool {
for _, a := range l.positiveLabels {
for _, b := range other.positiveLabels {
if a[0] == b[0] && a[1] != b[1] {
if !positiveLabelPairCompatible(a, b) {
return false
}
}
@@ -104,6 +198,28 @@ func (l LabelFilter) positiveLabelsCompatibleWith(other LabelFilter) bool {
return true
}
// positiveLabelPairCompatible reports whether two positive k=v constraints
// could be satisfied by the same release. The "dir" key compares with
// directory-prefix semantics, so overlapping subtrees are compatible.
func positiveLabelPairCompatible(a, b []string) bool {
if a[0] != b[0] {
return true
}
if a[0] == DirLabel {
return DirsCompatible(a[1], b[1])
}
return a[1] == b[1]
}
// DirsCompatible reports whether two dir= values could both be satisfied by
// the same release. Compatible when one is at-or-below the other in the
// directory hierarchy; siblings or unrelated subtrees conflict.
func DirsCompatible(a, b string) bool {
a = NormalizeDirValue(a)
b = NormalizeDirValue(b)
return dirAtOrBelow(a, b) || dirAtOrBelow(b, a)
}
var (
reLabelMismatch = regexp.MustCompile(`^[a-zA-Z0-9_\.\/\+-]+!=[a-zA-Z0-9_\.\/\+-]+$`)
reLabelMatch = regexp.MustCompile(`^[a-zA-Z0-9_\.\/\+-]+=[a-zA-Z0-9_\.\/\+-]+$`)
@@ -111,21 +227,62 @@ var (
// ParseLabels takes a label in the form foo=bar,baz!=bat and returns a LabelFilter that will match the labels
func ParseLabels(l string) (LabelFilter, error) {
lf := LabelFilter{}
lf.positiveLabels = [][]string{}
lf.negativeLabels = [][]string{}
var err error
labels := strings.SplitSeq(l, ",")
for label := range labels {
if match := reLabelMismatch.MatchString(label); match {
kv := strings.Split(label, "!=")
lf := LabelFilter{
positiveLabels: [][]string{},
negativeLabels: [][]string{},
}
for label := range strings.SplitSeq(l, ",") {
kv, negative, err := parseOneLabel(label)
if err != nil {
return lf, err
}
if negative {
lf.negativeLabels = append(lf.negativeLabels, kv)
} else if match := reLabelMatch.MatchString(label); match {
kv := strings.Split(label, "=")
lf.positiveLabels = append(lf.positiveLabels, kv)
} else {
return lf, fmt.Errorf("malformed label: %s. Expected label in form k=v or k!=v", label)
lf.positiveLabels = append(lf.positiveLabels, kv)
}
}
return lf, err
return lf, nil
}
// parseOneLabel parses a single k=v or k!=v label. Values of the reserved
// "dir" key are validated so unusable targets fail fast.
func parseOneLabel(label string) (kv []string, negative bool, err error) {
sep := ""
if reLabelMismatch.MatchString(label) {
sep = "!="
} else if reLabelMatch.MatchString(label) {
sep = "="
}
if sep == "" {
return nil, false, fmt.Errorf("malformed label: %s. Expected label in form k=v or k!=v", label)
}
kv = strings.Split(label, sep)
if kv[0] != DirLabel {
return kv, sep == "!=", nil
}
if err := validateDirSelectorValue(kv[1]); err != nil {
return nil, false, err
}
return kv, sep == "!=", nil
}
// validateDirSelectorValue rejects dir= and dir!= values that cannot be
// matched against the auto-populated dir label: absolute paths (labels are
// always root-relative), paths that escape the root via "..", and the bare
// "." which would either be a no-op or carry surprising "root-only" semantics
// depending on interpretation. Callers should omit dir= entirely to select
// every release.
func validateDirSelectorValue(v string) error {
n := NormalizeDirValue(v)
if n == "." {
return fmt.Errorf("dir= selector value %q is not allowed; omit -l dir= entirely to match every release", v)
}
if strings.HasPrefix(n, "/") {
return fmt.Errorf("dir= selector value %q must be a path relative to the root helmfile, not absolute", v)
}
if escapesRoot(n) {
return fmt.Errorf("dir= selector value %q escapes the root helmfile directory", v)
}
return nil
}
+226
View File
@@ -169,3 +169,229 @@ func TestSelectorsAreCompatible(t *testing.T) {
})
}
}
func TestLabelFilterMatchDir(t *testing.T) {
tests := []struct {
name string
selector string
dir string
want bool
}{
{name: "exact dir match", selector: "dir=apps/opencloud", dir: "apps/opencloud", want: true},
{name: "prefix dir match", selector: "dir=apps/opencloud", dir: "apps/opencloud/sub", want: true},
{name: "sibling does not match", selector: "dir=apps/opencloud", dir: "apps/xwiki", want: false},
{name: "shallower target does not match deeper", selector: "dir=apps/opencloud/sub", dir: "apps/opencloud", want: false},
{name: "ancestor prefix match", selector: "dir=apps", dir: "apps/opencloud/sub", want: true},
{name: "missing dir label fails positive", selector: "dir=apps/x", dir: "", want: false},
{name: "negative dir on different branch matches", selector: "dir!=apps/x", dir: "apps/y", want: true},
{name: "negative dir on same branch excludes", selector: "dir!=apps/x", dir: "apps/x/foo", want: false},
{name: "missing dir label still matches negative", selector: "dir!=apps/x", dir: "", want: true},
{name: "partial-name prefix is not a dir match", selector: "dir=apps/open", dir: "apps/opencloud", want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lf, err := ParseLabels(tt.selector)
assert.NoError(t, err)
release := ReleaseSpec{Labels: map[string]string{}}
if tt.dir != "" {
release.Labels[DirLabel] = tt.dir
}
assert.Equal(t, tt.want, lf.Match(release))
})
}
}
func TestLabelFilterMatchDirComposesWithOtherKeys(t *testing.T) {
lf, err := ParseLabels("dir=apps/opencloud,name=foo")
assert.NoError(t, err)
tests := []struct {
name string
release ReleaseSpec
want bool
}{
{
name: "both dir and name match",
release: ReleaseSpec{Labels: map[string]string{DirLabel: "apps/opencloud", "name": "foo"}},
want: true,
},
{
name: "dir matches but name does not",
release: ReleaseSpec{Labels: map[string]string{DirLabel: "apps/opencloud", "name": "bar"}},
want: false,
},
{
name: "name matches but dir does not",
release: ReleaseSpec{Labels: map[string]string{DirLabel: "apps/other", "name": "foo"}},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, lf.Match(tt.release))
})
}
}
func TestMatchDirPrefix(t *testing.T) {
tests := []struct {
name string
dirLabel string
value string
want bool
}{
{"empty label never matches", "", "any", false},
{"exact equality", "apps/x", "apps/x", true},
{"deeper is prefix", "apps/x/sub", "apps/x", true},
{"sibling not matched", "apps/y", "apps/x", false},
{"target deeper than label", "apps/x", "apps/x/sub", false},
{"name-collision not a match", "apps/xenial", "apps/x", false},
{"value with trailing slash normalizes", "apps/x", "apps/x/", true},
{"label with trailing slash normalizes", "apps/x/", "apps/x", true},
{"value with ./ prefix normalizes", "apps/x", "./apps/x", true},
{"root label does not match nested target", ".", "apps/x", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, matchDirPrefix(tt.dirLabel, tt.value))
})
}
}
func TestLabelFilterMatchDirCompoundWithNegative(t *testing.T) {
lf, err := ParseLabels("name=foo,dir!=apps/excluded")
assert.NoError(t, err)
tests := []struct {
name string
release ReleaseSpec
want bool
}{
{
name: "name matches and dir is not excluded",
release: ReleaseSpec{Labels: map[string]string{DirLabel: "apps/wanted", "name": "foo"}},
want: true,
},
{
name: "name matches but dir falls under excluded subtree",
release: ReleaseSpec{Labels: map[string]string{DirLabel: "apps/excluded/sub", "name": "foo"}},
want: false,
},
{
name: "name does not match and dir is fine",
release: ReleaseSpec{Labels: map[string]string{DirLabel: "apps/wanted", "name": "bar"}},
want: false,
},
{
name: "no dir label still matches negative dir",
release: ReleaseSpec{Labels: map[string]string{"name": "foo"}},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, lf.Match(tt.release))
})
}
}
func TestNormalizeDirValue(t *testing.T) {
tests := []struct {
in string
want string
}{
{"", ""},
{".", "."},
{"apps/x", "apps/x"},
{"apps/x/", "apps/x"},
{"./apps/x", "apps/x"},
{"apps//x", "apps/x"},
{"apps/./x", "apps/x"},
}
for _, tt := range tests {
t.Run(tt.in, func(t *testing.T) {
assert.Equal(t, tt.want, NormalizeDirValue(tt.in))
})
}
}
func TestDirsCompatible(t *testing.T) {
tests := []struct {
name string
a, b string
want bool
}{
{"identical paths", "apps/x", "apps/x", true},
{"a is ancestor of b", "apps", "apps/x", true},
{"b is ancestor of a", "apps/x/sub", "apps", true},
{"siblings are incompatible", "apps/x", "apps/y", false},
{"name-collision incompatible", "apps/x", "apps/xenial", false},
{"identical after normalization", "apps/x/", "./apps/x", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, DirsCompatible(tt.a, tt.b))
})
}
}
func TestParseLabelsRejectsInvalidDirValues(t *testing.T) {
cases := []string{
"dir=.",
"dir=./",
"dir=apps/..",
"dir=/absolute/path",
"dir=..",
"dir=../escape",
"dir!=.",
"dir!=/absolute",
}
for _, sel := range cases {
t.Run(sel, func(t *testing.T) {
_, err := ParseLabels(sel)
assert.Error(t, err, "expected ParseLabels to reject %q", sel)
})
}
}
func TestParseLabelsAcceptsValidDirValues(t *testing.T) {
cases := []string{
"dir=apps/x",
"dir=apps/x/sub",
"dir=./apps/x",
"dir=apps/x/",
"dir!=apps/excluded",
"dir=apps/x,name=foo",
}
for _, sel := range cases {
t.Run(sel, func(t *testing.T) {
_, err := ParseLabels(sel)
assert.NoError(t, err, "expected ParseLabels to accept %q", sel)
})
}
}
func TestDirRelativeToRoot(t *testing.T) {
tests := []struct {
name string
absDir string
want string
wantOK bool
}{
{name: "root itself", absDir: "/abs/root", want: ".", wantOK: true},
{name: "nested", absDir: "/abs/root/apps/x", want: "apps/x", wantOK: true},
{name: "dir name starting with ..", absDir: "/abs/root/..apps", want: "..apps", wantOK: true},
{name: "sibling of root", absDir: "/abs/other", wantOK: false},
{name: "parent of root", absDir: "/abs", wantOK: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := DirRelativeToRoot("/abs/root", tt.absDir)
assert.Equal(t, tt.wantOK, ok)
assert.Equal(t, tt.want, got)
})
}
}
+88 -6
View File
@@ -154,6 +154,12 @@ type HelmState struct {
// (see issue #2770). Set via SetCancelContext; nil falls back to Background.
cancelCtx gocontext.Context
// rootDir is the absolute directory of the top-level helmfile. Set once
// by the loader and inherited unchanged by nested states. Used as the
// anchor for the "dir" auto-label applied to releases during selector
// matching. Set via SetRootDir.
rootDir string
ReleaseSetSpec `yaml:",inline"`
logger *zap.SugaredLogger
@@ -190,6 +196,13 @@ func (st *HelmState) SetKubeconfig(kubeconfig string) {
st.kubeconfig = kubeconfig
}
// SetRootDir sets the absolute directory of the top-level helmfile. Used by
// the app loader to anchor the "dir" auto-label so it stays stable across
// nested states.
func (st *HelmState) SetRootDir(rootDir string) {
st.rootDir = rootDir
}
// SubHelmfileSpec defines the subhelmfile path and options
type SubHelmfileSpec struct {
//path or glob pattern for the sub helmfiles
@@ -561,9 +574,13 @@ type ReleaseSpec struct {
TrackTimeout *int `yaml:"trackTimeout,omitempty"`
// TrackLogs enables log streaming with kubedog
TrackLogs *bool `yaml:"trackLogs,omitempty"`
// TrackLogsInterval specifies how often captured logs are printed. The minimum is 1s.
TrackLogsInterval *time.Duration `yaml:"trackLogsInterval,omitempty"`
// TrackLogsUntilReady stops pod log streaming when each pod becomes ready. Requires TrackLogs.
TrackLogsUntilReady *bool `yaml:"trackLogsUntilReady,omitempty"`
// TrackFailedLogs streams logs only for pods that enter a failed state
// (CrashLoopBackOff, Error, etc.). Pods that succeed produce no output.
// Has no effect when TrackLogs is true (full streaming wins).
// Has no effect when TrackLogs is true (TrackLogs wins).
TrackFailedLogs *bool `yaml:"trackFailedLogs,omitempty"`
// HelmStuckGrace, when > 0, enables the safety-valve helm-killer for
// kubedog tracking: if the cluster confirms every tracked resource has
@@ -1082,6 +1099,7 @@ type SyncOpts struct {
TrackMode string
TrackTimeout int
TrackLogs bool
TrackLogsUntilReady bool
TrackFailedLogs bool
TrackLogsInterval time.Duration
HelmStuckGrace int
@@ -3551,16 +3569,72 @@ func (st *HelmState) GetReleasesWithLabels() []ReleaseSpec {
return rs
}
// reservedLabelWarnings reports user-defined "dir" labels, which are shadowed
// by the auto-populated value at match time. Scheduled to become an error.
func (st *HelmState) reservedLabelWarnings() []string {
var warnings []string
if _, ok := st.CommonLabels[DirLabel]; ok {
warnings = append(warnings, fmt.Sprintf("commonLabels uses reserved key %q (auto-populated for dir-based filtering); it is ignored for selectors and will be rejected in a future release, rename it", DirLabel))
}
for _, name := range slices.Sorted(maps.Keys(st.Templates)) {
if _, ok := st.Templates[name].Labels[DirLabel]; ok {
warnings = append(warnings, fmt.Sprintf("template %q uses reserved label key %q (auto-populated for dir-based filtering); it is ignored for selectors and will be rejected in a future release, rename it", name, DirLabel))
}
}
for _, r := range st.Releases {
if _, ok := r.Labels[DirLabel]; ok {
warnings = append(warnings, fmt.Sprintf("release %q uses reserved label key %q (auto-populated for dir-based filtering); it is ignored for selectors and will be rejected in a future release, rename it", r.Name, DirLabel))
}
}
return warnings
}
// dirLabel returns the auto-populated "dir" label value: basePath relative
// to rootDir in normalized slash form, or "" when either is unset or basePath
// escapes rootDir via "..". Releases from such states (remote helmfiles,
// outside-root sub-helmfiles loaded via `helmfiles: ../shared/...`) carry no
// dir label and do not participate in dir-based filtering. basePath may be
// "." in single-file mode; the caller has chdir'd to the state's directory
// by then so filepath.Abs returns the correct absolute path.
func (st *HelmState) dirLabel() string {
if st.rootDir == "" || st.basePath == "" {
return ""
}
base := st.basePath
if !filepath.IsAbs(base) {
abs, err := st.absPath(base)
if err != nil {
return ""
}
base = abs
}
rel, ok := DirRelativeToRoot(st.rootDir, base)
if !ok {
return ""
}
return rel
}
// absPath resolves p against the state's filesystem abstraction so paths
// like "." land in the active state directory both in production (where
// within() has chdir'd) and in tests (which simulate CWD via a fake FS).
func (st *HelmState) absPath(p string) (string, error) {
if st.fs != nil && st.fs.Abs != nil {
return st.fs.Abs(p)
}
return filepath.Abs(p)
}
func (st *HelmState) SelectReleases(includeTransitiveNeeds bool) ([]Release, error) {
values := st.Values()
rs, err := markExcludedReleases(st.Releases, st.Selectors, values, includeTransitiveNeeds)
rs, err := markExcludedReleases(st.Releases, st.Selectors, values, st.dirLabel(), includeTransitiveNeeds)
if err != nil {
return nil, err
}
return rs, nil
}
func markExcludedReleases(releases []ReleaseSpec, selectors []string, values map[string]any, includeTransitiveNeeds bool) ([]Release, error) {
func markExcludedReleases(releases []ReleaseSpec, selectors []string, values map[string]any, dirLabel string, includeTransitiveNeeds bool) ([]Release, error) {
var filteredReleases []Release
filters := []ReleaseFilter{}
for _, label := range selectors {
@@ -3572,8 +3646,9 @@ func markExcludedReleases(releases []ReleaseSpec, selectors []string, values map
}
for _, r := range releases {
var filterMatch bool
matchTarget := withDirLabel(r, dirLabel)
for _, f := range filters {
if f.Match(r) {
if f.Match(matchTarget) {
filterMatch = true
break
}
@@ -4221,7 +4296,14 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp
flags = append(flags, "--force-conflicts")
}
if release.RecreatePods != nil && *release.RecreatePods || release.RecreatePods == nil && st.HelmDefaults.RecreatePods {
recreatePodsEnabled := (release.RecreatePods != nil && *release.RecreatePods) || (release.RecreatePods == nil && st.HelmDefaults.RecreatePods)
// Helm 4 removed --recreate-pods (helm/helm#31023) and fails with "unknown flag".
// It has no replacement flag; Helm recommends a checksum annotation on the pod template.
switch {
case recreatePodsEnabled && helm.IsHelm4():
st.logger.Warnf("recreatePods is not supported by Helm 4, ignoring it for release %s", release.Name)
case recreatePodsEnabled:
flags = append(flags, "--recreate-pods")
}
@@ -4542,7 +4624,7 @@ func (st *HelmState) appendTakeOwnershipFlagsForDiff(flags []string, release *Re
isAppendTakeOwnership = true
case takeOwnership:
isAppendTakeOwnership = true
case st.HelmDefaults.TakeOwnership != nil && *st.HelmDefaults.TakeOwnership:
case release.TakeOwnership == nil && st.HelmDefaults.TakeOwnership != nil && *st.HelmDefaults.TakeOwnership:
isAppendTakeOwnership = true
}
if isAppendTakeOwnership {
+4
View File
@@ -7,6 +7,7 @@ import (
"reflect"
"slices"
"strings"
"time"
"dario.cat/mergo"
@@ -147,6 +148,9 @@ func (st *HelmState) ExecuteTemplates() (*HelmState, error) {
if st.Releases[i].Chart == "" {
return nil, fmt.Errorf("encountered empty chart while reading release %q", st.Releases[i].Name)
}
if interval := st.Releases[i].TrackLogsInterval; interval != nil && *interval < time.Second {
return nil, fmt.Errorf("release %q: trackLogsInterval must be at least 1s, got: %s", st.Releases[i].Name, *interval)
}
}
return &r, nil
+61
View File
@@ -5,8 +5,10 @@ import (
"reflect"
"strings"
"testing"
"time"
"github.com/go-test/deep"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"github.com/helmfile/helmfile/pkg/environment"
@@ -277,6 +279,65 @@ func TestHelmState_executeTemplates(t *testing.T) {
}
}
func TestHelmState_executeTemplatesTrackLogsIntervalValidation(t *testing.T) {
tests := []struct {
name string
interval time.Duration
wantErr string
}{
{
name: "minimum 1s is valid",
interval: time.Second,
},
{
name: "above minimum is valid",
interval: 5 * time.Second,
},
{
name: "below minimum is rejected",
interval: 500 * time.Millisecond,
wantErr: `release "foo": trackLogsInterval must be at least 1s, got: 500ms`,
},
{
name: "zero is rejected",
interval: 0,
wantErr: `release "foo": trackLogsInterval must be at least 1s, got: 0s`,
},
{
name: "negative is rejected",
interval: -time.Second,
wantErr: `release "foo": trackLogsInterval must be at least 1s, got: -1s`,
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
interval := tt.interval
st := &HelmState{
fs: &filesystem.FileSystem{Glob: func(s string) ([]string, error) { return nil, nil }},
basePath: ".",
Env: environment.Environment{Name: "test_env"},
Releases: []ReleaseSpec{
{
Name: "foo",
Chart: "incubator/raw",
TrackLogsInterval: &interval,
},
},
RenderedValues: map[string]any{},
}
_, err := st.ExecuteTemplates()
if tt.wantErr == "" {
require.NoError(t, err)
return
}
require.ErrorContains(t, err, tt.wantErr)
})
}
}
func TestHelmState_recursiveRefsTemplates(t *testing.T) {
tests := []struct {
name string
+240 -4
View File
@@ -1,12 +1,14 @@
package state
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"slices"
"strings"
"testing"
"github.com/Masterminds/semver/v3"
@@ -459,10 +461,12 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
wantErr: "force and forceConflicts are mutually exclusive (check both releases[].force/forceConflicts and helmDefaults.force/forceConflicts)",
},
{
name: "recreate-pods",
name: "recreate-pods-helm3",
defaults: HelmSpec{
RecreatePods: false,
RecreatePods: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
@@ -477,10 +481,12 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
},
},
{
name: "recreate-pods-from-default",
name: "recreate-pods-from-default-helm3",
defaults: HelmSpec{
RecreatePods: true,
RecreatePods: true,
CreateNamespace: &disable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
@@ -493,6 +499,43 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
"--namespace", "test-namespace",
},
},
{
name: "recreate-pods-helm4",
defaults: HelmSpec{
RecreatePods: false,
CreateNamespace: &disable,
},
version: semver.MustParse("4.0.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
RecreatePods: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "recreate-pods-from-default-helm4",
defaults: HelmSpec{
RecreatePods: true,
CreateNamespace: &disable,
},
version: semver.MustParse("4.0.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "wait",
defaults: HelmSpec{
@@ -1438,6 +1481,7 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
basePath: "./",
Releases: []ReleaseSpec{*tt.release},
HelmDefaults: tt.defaults,
logger: logger,
valsRuntime: valsRuntime,
}
helm := &exectest.Helm{
@@ -1458,6 +1502,47 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
}
}
func TestHelmState_flagsForUpgrade_RecreatePodsWarning(t *testing.T) {
enable := true
disable := false
tests := []struct {
name string
version *semver.Version
wantWarn bool
}{
{name: "helm4", version: semver.MustParse("4.0.0"), wantWarn: true},
{name: "helm3", version: semver.MustParse("3.10.0"), wantWarn: false},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
var logs bytes.Buffer
release := &ReleaseSpec{
Chart: "test/chart",
RecreatePods: &enable,
Name: "test-charts",
Namespace: "test-namespace",
}
state := &HelmState{
basePath: "./",
Releases: []ReleaseSpec{*release},
HelmDefaults: HelmSpec{CreateNamespace: &disable},
logger: helmexec.NewLogger(&logs, "warn"),
valsRuntime: valsRuntime,
}
if _, _, err := state.flagsForUpgrade(&exectest.Helm{Version: tt.version}, release, 0, &SyncOpts{}); err != nil {
t.Fatalf("unexpected error flagsForUpgrade: %v", err)
}
warned := strings.Contains(logs.String(), "recreatePods is not supported by Helm 4, ignoring it for release test-charts")
if warned != tt.wantWarn {
t.Errorf("warning logged = %v, want %v; logs: %q", warned, tt.wantWarn, logs.String())
}
})
}
}
func TestHelmState_flagsForTemplate(t *testing.T) {
enable := true
disable := false
@@ -5350,6 +5435,51 @@ func TestCommonDiffFlags(t *testing.T) {
}
}
func TestAppendTakeOwnershipFlagsForDiff(t *testing.T) {
pluginsDir := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(pluginsDir, "diff"), 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(pluginsDir, "diff", "plugin.yaml"),
[]byte("name: diff\nversion: 3.11.0\n"),
0o600,
))
tests := []struct {
name string
defaults *bool
release *bool
expected []string
}{
{
name: "release false overrides helm defaults",
defaults: &[]bool{true}[0],
release: &[]bool{false}[0],
expected: []string{},
},
{
name: "take-ownership from helm defaults",
defaults: &[]bool{true}[0],
expected: []string{"--take-ownership"},
},
{
name: "no take-ownership when unset",
expected: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st := &HelmState{
HelmDefaults: HelmSpec{TakeOwnership: tt.defaults},
}
got, err := st.appendTakeOwnershipFlagsForDiff([]string{}, &ReleaseSpec{TakeOwnership: tt.release}, false, pluginsDir)
require.NoError(t, err)
require.Equal(t, tt.expected, got)
})
}
}
func TestAppendChartDownloadFlags(t *testing.T) {
tests := []struct {
name string
@@ -7153,3 +7283,109 @@ func TestResolveOCIAdhocDepChart(t *testing.T) {
})
}
}
func TestHelmState_dirLabel(t *testing.T) {
tests := []struct {
name string
rootDir string
basePath string
want string
}{
{name: "both empty", rootDir: "", basePath: "", want: ""},
{name: "rootDir empty", rootDir: "", basePath: "/abs/apps/x", want: ""},
{name: "basePath empty", rootDir: "/abs", basePath: "", want: ""},
{name: "equal yields .", rootDir: "/abs/root", basePath: "/abs/root", want: "."},
{name: "nested under root", rootDir: "/abs/root", basePath: "/abs/root/apps/x", want: "apps/x"},
{name: "deeply nested", rootDir: "/abs/root", basePath: "/abs/root/apps/x/sub", want: "apps/x/sub"},
{name: "escapes root via ..", rootDir: "/abs/root", basePath: "/abs/other", want: ""},
{name: "dir name starting with .. stays inside root", rootDir: "/abs/root", basePath: "/abs/root/..apps", want: "..apps"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st := &HelmState{rootDir: tt.rootDir, basePath: tt.basePath}
assert.Equal(t, tt.want, st.dirLabel())
})
}
}
func TestHelmState_reservedLabelWarnings(t *testing.T) {
tests := []struct {
name string
state *HelmState
wantWarnings int
}{
{
name: "no labels",
state: &HelmState{},
},
{
name: "commonLabels uses dir",
state: &HelmState{
CommonLabels: map[string]string{DirLabel: "backend"},
},
wantWarnings: 1,
},
{
name: "release labels use dir",
state: &HelmState{
Releases: []ReleaseSpec{
{Name: "foo", Labels: map[string]string{DirLabel: "backend"}},
{Name: "bar", Labels: map[string]string{DirLabel: "frontend"}},
},
},
wantWarnings: 2,
},
{
name: "template labels use dir",
state: &HelmState{
Templates: map[string]TemplateSpec{
"default": {ReleaseSpec: ReleaseSpec{Labels: map[string]string{DirLabel: "backend"}}},
},
},
wantWarnings: 1,
},
{
name: "unrelated labels are fine",
state: &HelmState{
CommonLabels: map[string]string{"tier": "backend"},
Releases: []ReleaseSpec{
{Name: "foo", Labels: map[string]string{"team": "data"}},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
warnings := tt.state.reservedLabelWarnings()
assert.Len(t, warnings, tt.wantWarnings)
for _, w := range warnings {
assert.Contains(t, w, "will be rejected in a future release")
}
})
}
}
func TestMarkExcludedReleases_UserDirLabelIsShadowed(t *testing.T) {
tests := []struct {
name string
dirLabel string
selector string
wantSelected bool
}{
{name: "no auto label: user value does not match dir=", dirLabel: "", selector: "dir=apps", wantSelected: false},
{name: "no auto label: user value does not match dir!=", dirLabel: "", selector: "dir!=apps", wantSelected: true},
{name: "auto label wins over user value for dir=", dirLabel: "infra", selector: "dir=apps", wantSelected: false},
{name: "auto label is matched", dirLabel: "infra", selector: "dir=infra", wantSelected: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
releases := []ReleaseSpec{{Name: "foo", Labels: map[string]string{DirLabel: "apps/foo"}}}
got, err := markExcludedReleases(releases, []string{tt.selector}, nil, tt.dirLabel, false)
require.NoError(t, err)
require.Len(t, got, 1)
assert.Equal(t, tt.wantSelected, !got[0].Filtered)
assert.Equal(t, "apps/foo", got[0].Labels[DirLabel], "user-facing labels stay untouched")
})
}
}
+6 -6
View File
@@ -42,39 +42,39 @@ func TestGenerateID(t *testing.T) {
run(testcase{
subject: "baseline",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
want: "foo-values-565b4d4448",
want: "foo-values-66b6b57c5",
})
run(testcase{
subject: "different bytes content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: []byte(`{"k":"v"}`),
want: "foo-values-5bf759b9cf",
want: "foo-values-764568cb",
})
run(testcase{
subject: "different map content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: map[string]any{"k": "v"},
want: "foo-values-cbd47bb9c",
want: "foo-values-78dcc498c4",
})
run(testcase{
subject: "different chart",
release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"},
want: "foo-values-84bcbdd8f9",
want: "foo-values-788fc9bdf8",
})
run(testcase{
subject: "different name",
release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"},
want: "bar-values-757bdb6889",
want: "bar-values-6b4546d548",
})
run(testcase{
subject: "specific ns",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"},
want: "myns-foo-values-f4bcf8c77",
want: "myns-foo-values-75b997c6dd",
})
for id, n := range ids {
+1
View File
@@ -8,3 +8,4 @@ The `testdata` directory is composed of:
- `snapshot/$NAME/input.yaml`: The input helmfile config for the test case of `$NAME`
- `snapshot/$NAME/output.yaml`: The expected output of the helmfile command
- `snapshot/$NAME/config.yaml`: The snapshot test configuration file. See the `Config` struct defined in `snapshot_test.go` for more information
- `snapshot/$NAME/chart`: Optional chart fixture local to the test case, referenced from the input as `./chart/`. Unlike `charts`, it is never packaged/pushed to the test registry, so it may hold directories without `Chart.yaml` (raw manifests that must go through chartify)
@@ -112,6 +112,18 @@ func prepareInputFile(t *testing.T, originalFile, tmpDir string, hostPort int, c
// breaking relative paths like ../../charts/raw-0.1.0
inputStr = strings.ReplaceAll(inputStr, "../../charts/", chartsDir+"/")
// Convert test-case-local chart references like `./chart/` to an absolute
// path pointing at the `chart` directory shipped next to the case's input
// file. This lets a case bundle chart fixtures that cannot live in the
// shared ../../charts directory — e.g. a directory without Chart.yaml,
// which must stay unpackaged because setupLocalDockerRegistry runs
// `helm package` on every chart under testdata/charts.
caseDir, err := filepath.Abs(filepath.Dir(originalFile))
if err != nil {
t.Fatalf("Failed to determine test case directory for %s: %v", originalFile, err)
}
inputStr = strings.ReplaceAll(inputStr, "./chart/", filepath.Join(caseDir, "chart")+"/")
// Convert relative postrenderer paths to absolute paths for Helm 3 only
// Helm 3 resolves postrenderer paths relative to the helmfile location.
// When the input file is copied to a temp directory, relative paths break.
@@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: foo
data:
foo: FOO
@@ -0,0 +1,6 @@
localDockerRegistry:
enabled: true
# Port is not specified, will be dynamically allocated to avoid conflicts
chartifyTempDir: temp2824fetch
helmfileArgs:
- fetch
@@ -0,0 +1,21 @@
# The primary chart is a local directory WITHOUT Chart.yaml. That is the only
# chart shape for which `helmfile fetch` enters chartification
# (NeedsChartifyForLocalDir in pkg/state). Chartify then materializes a
# temporary chart and resolves the adhoc OCI dependency with its own
# `helm dependency up`, which requires the propagated --oci-plain-http on
# Helm 4. Fetching a packaged or regular local chart never chartifies, so this
# case uses a manifest-only directory to protect the Fetch wiring.
#
# After chartify renders the adhoc dependency into the chart, it strips the
# Chart.yaml dependencies field and removes charts/, so the follow-up
# `helm dependency build` is a silent no-op (helm3/helm4 return early when
# Chart.yaml declares no dependencies) — the golden is just its "Building
# dependency" log line. The regression signal is the command itself failing
# when chartify's `helm dependency up` lacks --plain-http on Helm 4.
releases:
- name: foo
chart: ./chart/
dependencies:
- alias: dep
chart: oci://localhost:$REGISTRY_PORT/myrepo/raw
version: 0.1.0
@@ -0,0 +1 @@
Building dependency release=foo, chart=$WD/temp2824fetch/foo
@@ -0,0 +1 @@
Building dependency release=foo, chart=$WD/temp2824fetch/foo
@@ -0,0 +1,6 @@
localDockerRegistry:
enabled: true
# Port is not specified, will be dynamically allocated to avoid conflicts
chartifyTempDir: temp2824
helmfileArgs:
- lint
@@ -0,0 +1,7 @@
releases:
- name: foo
chart: ../../charts/raw-0.1.0
dependencies:
- alias: dep
chart: oci://localhost:$REGISTRY_PORT/myrepo/raw
version: 0.1.0
@@ -0,0 +1,12 @@
Building dependency release=foo, chart=$WD/temp2824/foo
Saving 1 charts
Downloading raw from repo oci://localhost:$REGISTRY_PORT/myrepo
Pulled: localhost:$REGISTRY_PORT/myrepo/raw:0.1.0
Digest: sha256:$DIGEST
Deleting outdated charts
Linting release=foo, chart=$WD/temp2824/foo
==> Linting __workingdir__/temp2824/foo
[INFO] Chart.yaml: icon is recommended
1 chart(s) linted, 0 chart(s) failed
@@ -0,0 +1,10 @@
Building dependency release=foo, chart=$WD/temp2824/foo
Saving 1 charts
Downloading raw from repo oci://localhost:$REGISTRY_PORT/myrepo
Deleting outdated charts
Linting release=foo, chart=$WD/temp2824/foo
==> Linting __workingdir__/temp2824/foo
[INFO] Chart.yaml: icon is recommended
1 chart(s) linted, 0 chart(s) failed