mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-22 11:42:42 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfce9cf570 | ||
|
|
0d5311ba4d | ||
|
|
54c5944fce | ||
|
|
d5a354144a | ||
|
|
5d4db7ab89 | ||
|
|
254b2aa617 | ||
|
|
b2644c6ec2 | ||
|
|
8fc23a4d80 | ||
|
|
5c225fd200 | ||
|
|
b71d27e85b | ||
|
|
735c484ff5 | ||
|
|
706ceda804 | ||
|
|
fa878f5ebb | ||
|
|
eeb913472a | ||
|
|
e088d125f6 | ||
|
|
b7f71a0ce3 | ||
|
|
12fb6887c0 | ||
|
|
3216c02743 | ||
|
|
892f2f9604 | ||
|
|
eeec9e17a2 | ||
|
|
f1c86aa9dd | ||
|
|
25cd13e148 | ||
|
|
443cef0dc1 | ||
|
|
c25d16a438 | ||
|
|
000a6fa660 |
@@ -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
|
||||
|
||||
@@ -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,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
@@ -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}... && \
|
||||
|
||||
@@ -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
@@ -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}... && \
|
||||
|
||||
@@ -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
@@ -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
@@ -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.`)
|
||||
|
||||
@@ -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
@@ -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"`)
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/filesystem"
|
||||
"github.com/helmfile/helmfile/pkg/remote"
|
||||
"github.com/helmfile/helmfile/pkg/state"
|
||||
)
|
||||
|
||||
// RootHelmfileDir returns the absolute directory of the top-level helmfile,
|
||||
// or "" when the root is a remote URL or otherwise unresolvable. Callers
|
||||
// treat "" as "dir-based filtering not available". The value is cached by
|
||||
// resolveRootHelmfileDir; it must be invoked before any goroutine fan-out
|
||||
// or working-directory change so that the CWD-sensitive resolution lands
|
||||
// against the process CWD at command start, not the (possibly chdir'd)
|
||||
// directory of a leaf helmfile being processed.
|
||||
func (a *App) RootHelmfileDir() string {
|
||||
return a.rootHelmfileDir
|
||||
}
|
||||
|
||||
// resolveRootHelmfileDir computes and caches the root directory anchor for
|
||||
// dir-based filtering. Idempotent; safe to call multiple times. Must be
|
||||
// called from the top-level entry of each command, before within() or any
|
||||
// goroutine spawn.
|
||||
func (a *App) resolveRootHelmfileDir() {
|
||||
a.rootHelmfileDirOnce.Do(func() {
|
||||
a.rootHelmfileDir = a.computeRootHelmfileDir()
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) computeRootHelmfileDir() string {
|
||||
dir, err := DirSelectorRoot(a.fs, a.FileOrDir)
|
||||
if err != nil {
|
||||
a.Logger.Debugf("dir selector unavailable: cannot resolve root helmfile directory: %v", err)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// DirSelectorRoot returns the absolute directory that dir= selector values
|
||||
// are relative to for the given -f value: the working directory when it is
|
||||
// empty, the directory itself when it points to one, the file's directory
|
||||
// otherwise. Returns "" for a remote helmfile.
|
||||
func DirSelectorRoot(fs *filesystem.FileSystem, fileOrDir string) (string, error) {
|
||||
if fileOrDir == "" {
|
||||
return fs.Getwd()
|
||||
}
|
||||
if remote.IsRemote(fileOrDir) {
|
||||
return "", nil
|
||||
}
|
||||
absPath, err := fs.Abs(fileOrDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if fs.DirectoryExistsAt(absPath) {
|
||||
return absPath, nil
|
||||
}
|
||||
return filepath.Dir(absPath), nil
|
||||
}
|
||||
|
||||
// dirSelectorGroup holds the positive dir= target values inside one -l
|
||||
// argument. An empty targets slice means the group has no dir= constraint
|
||||
// and is therefore path-permissive during traversal-skip.
|
||||
type dirSelectorGroup struct {
|
||||
targets []string
|
||||
}
|
||||
|
||||
// extractDirSelectorTargets returns one dirSelectorGroup per -l argument
|
||||
// with that group's positive dir= values. Negative dir!= is intentionally
|
||||
// dropped: skipping a branch on a negative constraint would require proving
|
||||
// it contains only excluded releases, which generally requires loading it.
|
||||
// Malformed groups are treated as path-permissive so traversal does not
|
||||
// short-circuit on selector strings the filter will later report on.
|
||||
// Returns nil when no positive dir= appears, so callers can skip the check.
|
||||
func extractDirSelectorTargets(selectors []string) []dirSelectorGroup {
|
||||
if len(selectors) == 0 {
|
||||
return nil
|
||||
}
|
||||
groups := make([]dirSelectorGroup, 0, len(selectors))
|
||||
anyDir := false
|
||||
for _, s := range selectors {
|
||||
targets, err := state.PositiveDirTargets(s)
|
||||
if err != nil {
|
||||
groups = append(groups, dirSelectorGroup{})
|
||||
continue
|
||||
}
|
||||
groups = append(groups, dirSelectorGroup{targets: targets})
|
||||
if len(targets) > 0 {
|
||||
anyDir = true
|
||||
}
|
||||
}
|
||||
if !anyDir {
|
||||
return nil
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
// skipForDirFilter reports whether a `helmfiles:` entry can be skipped for
|
||||
// the given selectors. Remote entries and unresolvable roots always descend.
|
||||
func (a *App) skipForDirFilter(stateDir, entryPath string, selectors []string) bool {
|
||||
rootDir := a.RootHelmfileDir()
|
||||
if rootDir == "" || remote.IsRemote(entryPath) {
|
||||
return false
|
||||
}
|
||||
groups := extractDirSelectorTargets(selectors)
|
||||
if len(groups) == 0 {
|
||||
return false
|
||||
}
|
||||
entryDir, ok := a.entryDirRelativeToRoot(rootDir, stateDir, entryPath)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return !shouldDescendForDirFilter(entryDir, groups)
|
||||
}
|
||||
|
||||
// entryDirRelativeToRoot returns the directory holding the helmfile(s) of a
|
||||
// `helmfiles:` entry, relative to rootDir: the entry itself when it is a
|
||||
// directory, its parent otherwise. Returns false when the entry lies outside
|
||||
// rootDir, signaling the caller to descend conservatively.
|
||||
func (a *App) entryDirRelativeToRoot(rootDir, stateDir, entryPath string) (string, bool) {
|
||||
abs := entryPath
|
||||
if !filepath.IsAbs(abs) {
|
||||
abs = filepath.Join(stateDir, entryPath)
|
||||
}
|
||||
if !a.fs.DirectoryExistsAt(abs) {
|
||||
abs = filepath.Dir(abs)
|
||||
}
|
||||
return state.DirRelativeToRoot(rootDir, abs)
|
||||
}
|
||||
|
||||
// shouldDescendForDirFilter returns true when a sub-helmfile located in
|
||||
// entryDir (relative to the root) could contain a release matching at least
|
||||
// one of the dir-selector groups. A branch is relevant for a dir=target when
|
||||
// it lives at or below target, or when target lives below it (e.g. an
|
||||
// aggregator at apps/helmfile.yaml for target apps/x). The decision only
|
||||
// looks at the entry's own location, so it assumes nested entries stay inside
|
||||
// their parent's directory.
|
||||
func shouldDescendForDirFilter(entryDir string, groups []dirSelectorGroup) bool {
|
||||
if len(groups) == 0 || entryDir == "." {
|
||||
return true
|
||||
}
|
||||
for _, g := range groups {
|
||||
conflicts := slices.ContainsFunc(g.targets, func(target string) bool {
|
||||
return !state.DirsCompatible(entryDir, target)
|
||||
})
|
||||
if !conflicts {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/helmfile/vals"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/testhelper"
|
||||
)
|
||||
|
||||
func TestExtractDirSelectorTargets(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
selectors []string
|
||||
wantNil bool
|
||||
wantPer []int
|
||||
}{
|
||||
{
|
||||
name: "nil selectors",
|
||||
selectors: nil,
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "no dir selectors",
|
||||
selectors: []string{"name=foo", "namespace=bar"},
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "single dir selector",
|
||||
selectors: []string{"dir=apps/x"},
|
||||
wantPer: []int{1},
|
||||
},
|
||||
{
|
||||
name: "dir with companion in same group",
|
||||
selectors: []string{"dir=apps/x,name=foo"},
|
||||
wantPer: []int{1},
|
||||
},
|
||||
{
|
||||
name: "dir spread across groups",
|
||||
selectors: []string{"dir=apps/x", "dir=apps/y"},
|
||||
wantPer: []int{1, 1},
|
||||
},
|
||||
{
|
||||
name: "negative dir is ignored",
|
||||
selectors: []string{"dir!=apps/x"},
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "mixed group with no dir and group with dir",
|
||||
selectors: []string{"name=foo", "dir=apps/x"},
|
||||
wantPer: []int{0, 1},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
groups := extractDirSelectorTargets(tt.selectors)
|
||||
if tt.wantNil {
|
||||
assert.Nil(t, groups)
|
||||
return
|
||||
}
|
||||
assert.Len(t, groups, len(tt.wantPer))
|
||||
for i, want := range tt.wantPer {
|
||||
assert.Len(t, groups[i].targets, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldDescendForDirFilter(t *testing.T) {
|
||||
const target = "opendesk/helmfile/apps/openproject"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
entryDir string
|
||||
groups []dirSelectorGroup
|
||||
wantSkip bool
|
||||
}{
|
||||
{
|
||||
name: "no groups means caller skipped check; treat as descend",
|
||||
entryDir: "opendesk/helmfile/apps/openproject",
|
||||
groups: nil,
|
||||
},
|
||||
{
|
||||
name: "entry in target dir → descend",
|
||||
entryDir: "opendesk/helmfile/apps/openproject",
|
||||
groups: []dirSelectorGroup{{targets: []string{target}}},
|
||||
},
|
||||
{
|
||||
name: "entry below target → descend",
|
||||
entryDir: "opendesk/helmfile/apps/openproject/sub",
|
||||
groups: []dirSelectorGroup{{targets: []string{target}}},
|
||||
},
|
||||
{
|
||||
name: "sibling service → skip",
|
||||
entryDir: "opendesk/helmfile/apps/xwiki",
|
||||
groups: []dirSelectorGroup{{targets: []string{target}}},
|
||||
wantSkip: true,
|
||||
},
|
||||
{
|
||||
name: "name-collision is not a match → skip",
|
||||
entryDir: "opendesk/helmfile/apps/openproject-extras",
|
||||
groups: []dirSelectorGroup{{targets: []string{target}}},
|
||||
wantSkip: true,
|
||||
},
|
||||
{
|
||||
name: "entry is ancestor of target → descend",
|
||||
entryDir: "opendesk",
|
||||
groups: []dirSelectorGroup{{targets: []string{target}}},
|
||||
},
|
||||
{
|
||||
name: "entry in root dir → descend",
|
||||
entryDir: ".",
|
||||
groups: []dirSelectorGroup{{targets: []string{target}}},
|
||||
},
|
||||
{
|
||||
name: "group with no targets is permissive",
|
||||
entryDir: "opendesk/helmfile/apps/xwiki",
|
||||
groups: []dirSelectorGroup{{targets: nil}, {targets: []string{target}}},
|
||||
},
|
||||
{
|
||||
name: "multiple targets in one group: all must allow",
|
||||
entryDir: "opendesk/helmfile/apps/openproject",
|
||||
groups: []dirSelectorGroup{{targets: []string{target, "opendesk/helmfile/apps/xwiki"}}},
|
||||
wantSkip: true,
|
||||
},
|
||||
{
|
||||
name: "OR across groups: either matches descends",
|
||||
entryDir: "opendesk/helmfile/apps/openproject",
|
||||
groups: []dirSelectorGroup{{targets: []string{"opendesk/helmfile/apps/xwiki"}}, {targets: []string{target}}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, !tt.wantSkip, shouldDescendForDirFilter(tt.entryDir, tt.groups))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newDirFilterTestApp(t *testing.T, files map[string]string, fileOrDir string, selectors []string) (*App, *testhelper.TestFs) {
|
||||
t.Helper()
|
||||
|
||||
testFs := testhelper.NewTestFs(files)
|
||||
testFs.Cwd = "/workspace"
|
||||
|
||||
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
||||
require.NoError(t, err)
|
||||
|
||||
app := &App{
|
||||
OverrideHelmBinary: DefaultHelmBinary,
|
||||
OverrideKubeContext: "default",
|
||||
DisableKubeVersionAutoDetection: true,
|
||||
Env: "default",
|
||||
Logger: newAppTestLogger(),
|
||||
valsRuntime: valsRuntime,
|
||||
FileOrDir: fileOrDir,
|
||||
Selectors: selectors,
|
||||
}
|
||||
app = injectFs(app, testFs)
|
||||
expectNoCallsToHelm(app)
|
||||
return app, testFs
|
||||
}
|
||||
|
||||
// TestProcessNestedHelmfiles_DirFilterSkipsSiblings drives the full visit
|
||||
// chain against an opendesk-shaped fixture: a root helmfile that includes an
|
||||
// aggregator, which in turn lists several sibling service helmfiles. With
|
||||
// `-l dir=…/openproject` only the matching service helmfile should be opened,
|
||||
// asserted by counting how often a helmfile-child.yaml file is read. The
|
||||
// release names also let us confirm the post-load filter sees only the
|
||||
// expected entry.
|
||||
func TestProcessNestedHelmfiles_DirFilterSkipsSiblings(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/workspace/helmfile.yaml": `
|
||||
helmfiles:
|
||||
- opendesk/helmfile_generic.yaml
|
||||
`,
|
||||
"/workspace/opendesk/helmfile_generic.yaml": `
|
||||
helmfiles:
|
||||
- apps/openproject/helmfile-child.yaml
|
||||
- apps/xwiki/helmfile-child.yaml
|
||||
- apps/jitsi/helmfile-child.yaml
|
||||
`,
|
||||
"/workspace/opendesk/apps/openproject/helmfile-child.yaml": `
|
||||
releases:
|
||||
- name: openproject-web
|
||||
chart: stable/openproject
|
||||
`,
|
||||
"/workspace/opendesk/apps/xwiki/helmfile-child.yaml": `
|
||||
releases:
|
||||
- name: xwiki
|
||||
chart: stable/xwiki
|
||||
`,
|
||||
"/workspace/opendesk/apps/jitsi/helmfile-child.yaml": `
|
||||
releases:
|
||||
- name: jitsi
|
||||
chart: stable/jitsi
|
||||
`,
|
||||
}
|
||||
|
||||
app, testFs := newDirFilterTestApp(t, files, "/workspace/helmfile.yaml", []string{"dir=opendesk/apps/openproject"})
|
||||
|
||||
err := app.ForEachState(Noop, false, SetFilter(true))
|
||||
assert.NoError(t, err)
|
||||
|
||||
childReads := 0
|
||||
for _, r := range testFs.SuccessfulReads() {
|
||||
if strings.HasSuffix(r, "helmfile-child.yaml") {
|
||||
childReads++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, 1, childReads, "exactly one helmfile-child.yaml should be read (the matching one), got reads=%v", testFs.SuccessfulReads())
|
||||
}
|
||||
|
||||
// TestProcessNestedHelmfiles_DirFilterExcludesTopLevelReleases verifies that a
|
||||
// dir filter targeting a nested subtree excludes releases declared directly
|
||||
// in the root helmfile (whose dirLabel is ".") while still descending into the
|
||||
// matching sub-helmfile. The root file itself must still be read because its
|
||||
// `helmfiles:` directive is what points to the nested branch. Uses distinct
|
||||
// filenames per level so the read log is unambiguous.
|
||||
func TestProcessNestedHelmfiles_DirFilterExcludesTopLevelReleases(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/workspace/root.yaml": `
|
||||
releases:
|
||||
- name: root-release
|
||||
chart: stable/root
|
||||
helmfiles:
|
||||
- apps/openproject/leaf.yaml
|
||||
`,
|
||||
"/workspace/apps/openproject/leaf.yaml": `
|
||||
releases:
|
||||
- name: openproject-web
|
||||
chart: stable/openproject
|
||||
`,
|
||||
}
|
||||
|
||||
app, testFs := newDirFilterTestApp(t, files, "/workspace/root.yaml", []string{"dir=apps/openproject"})
|
||||
|
||||
err := app.ForEachState(Noop, false, SetFilter(true))
|
||||
assert.NoError(t, err)
|
||||
|
||||
reads := testFs.SuccessfulReads()
|
||||
readSet := map[string]bool{}
|
||||
for _, r := range reads {
|
||||
readSet[r] = true
|
||||
}
|
||||
assert.True(t, readSet["root.yaml"], "root helmfile must be parsed to learn its helmfiles: list, reads=%v", reads)
|
||||
assert.True(t, readSet["leaf.yaml"], "matching nested helmfile must be descended into, reads=%v", reads)
|
||||
}
|
||||
|
||||
// TestProcessNestedHelmfiles_DirDotIsRejected pins down the current behavior
|
||||
// that `dir=.` and equivalent forms (`dir=./`, `dir=apps/..`) are rejected at
|
||||
// selector parse time. Semantics for `.` were under discussion: matching
|
||||
// everything (no-op), matching only root-level releases (useful but uneven),
|
||||
// or rejecting outright. Outright rejection is the current decision; this
|
||||
// test makes any change to that policy a deliberate code change.
|
||||
func TestProcessNestedHelmfiles_DirDotIsRejected(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/workspace/root.yaml": `
|
||||
releases:
|
||||
- name: root-release
|
||||
chart: stable/root
|
||||
`,
|
||||
}
|
||||
for _, selector := range []string{"dir=.", "dir=./", "dir=apps/.."} {
|
||||
t.Run(selector, func(t *testing.T) {
|
||||
app, _ := newDirFilterTestApp(t, files, "/workspace/root.yaml", []string{selector})
|
||||
|
||||
err := app.ForEachState(Noop, false, SetFilter(true))
|
||||
assert.Error(t, err, "dir=. and equivalents should be rejected at parse time")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkipForDirFilter(t *testing.T) {
|
||||
const rootDir = "/workspace"
|
||||
const stateDir = "/workspace/opendesk"
|
||||
selectors := []string{"dir=opendesk/apps/openproject"}
|
||||
|
||||
testFs := testhelper.NewTestFs(map[string]string{
|
||||
"/workspace/opendesk/apps/openproject/helmfile.yaml": "",
|
||||
"/workspace/opendesk/apps/xwiki/helmfile.yaml": "",
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
entryPath string
|
||||
selectors []string
|
||||
rootDir string
|
||||
skip bool
|
||||
}{
|
||||
{name: "matching entry descends", entryPath: "apps/openproject/helmfile.yaml", selectors: selectors, rootDir: rootDir, skip: false},
|
||||
{name: "sibling entry is skipped", entryPath: "apps/xwiki/helmfile.yaml", selectors: selectors, rootDir: rootDir, skip: true},
|
||||
{name: "remote entry is never short-circuited", entryPath: "git::https://github.com/example/xwiki.git@helmfile.yaml?ref=main", selectors: selectors, rootDir: rootDir, skip: false},
|
||||
{name: "matching directory entry descends", entryPath: "apps/openproject", selectors: selectors, rootDir: rootDir, skip: false},
|
||||
{name: "sibling directory entry is skipped", entryPath: "apps/xwiki", selectors: selectors, rootDir: rootDir, skip: true},
|
||||
{name: "ancestor directory entry descends", entryPath: "apps", selectors: selectors, rootDir: rootDir, skip: false},
|
||||
{name: "absolute sibling entry is skipped", entryPath: "/workspace/opendesk/apps/xwiki/helmfile.yaml", selectors: selectors, rootDir: rootDir, skip: true},
|
||||
{name: "entry outside root descends", entryPath: "../../shared/helmfile.yaml", selectors: selectors, rootDir: rootDir, skip: false},
|
||||
{name: "no dir selector descends", entryPath: "apps/xwiki/helmfile.yaml", selectors: []string{"tier=frontend"}, rootDir: rootDir, skip: false},
|
||||
{name: "negative dir selector does not prune", entryPath: "apps/xwiki/helmfile.yaml", selectors: []string{"dir!=opendesk/apps/xwiki"}, rootDir: rootDir, skip: false},
|
||||
{name: "unresolvable root descends", entryPath: "apps/xwiki/helmfile.yaml", selectors: selectors, rootDir: "", skip: false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
app := injectFs(&App{rootHelmfileDir: tc.rootDir}, testFs)
|
||||
assert.Equal(t, tc.skip, app.skipForDirFilter(stateDir, tc.entryPath, tc.selectors))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessNestedHelmfiles_DirFilterSkipsSiblingDirectoryEntries(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/workspace/helmfile.yaml": `
|
||||
helmfiles:
|
||||
- apps/openproject
|
||||
- apps/xwiki
|
||||
`,
|
||||
"/workspace/apps/openproject/leaf.yaml": `
|
||||
releases:
|
||||
- name: openproject-web
|
||||
chart: stable/openproject
|
||||
`,
|
||||
"/workspace/apps/xwiki/leaf.yaml": `
|
||||
releases:
|
||||
- name: xwiki
|
||||
chart: stable/xwiki
|
||||
`,
|
||||
}
|
||||
|
||||
app, testFs := newDirFilterTestApp(t, files, "/workspace/helmfile.yaml", []string{"dir=apps/openproject"})
|
||||
for _, dir := range []string{"/workspace/apps/openproject", "/workspace/apps/xwiki"} {
|
||||
testFs.GlobFixtures[dir] = []string{dir}
|
||||
}
|
||||
|
||||
var selected []string
|
||||
err := app.ForEachState(func(run *Run) (bool, []error) {
|
||||
releases, err := run.state.GetSelectedReleases(false)
|
||||
if err != nil {
|
||||
return false, []error{err}
|
||||
}
|
||||
for _, r := range releases {
|
||||
selected = append(selected, r.Name)
|
||||
}
|
||||
return len(releases) > 0, nil
|
||||
}, false, SetFilter(true))
|
||||
require.NoError(t, err)
|
||||
|
||||
leafReads := 0
|
||||
for _, r := range testFs.SuccessfulReads() {
|
||||
if strings.HasSuffix(r, "leaf.yaml") {
|
||||
leafReads++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, 1, leafReads, "only the matching directory entry should be read, got reads=%v", testFs.SuccessfulReads())
|
||||
assert.Equal(t, []string{"openproject-web"}, selected)
|
||||
}
|
||||
+1
-1
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestApplyImpl_TrackLogsInterval(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setFlag string // explicit --track-logs-interval value; empty means not supplied
|
||||
noCmd bool // construct ApplyImpl without a Cmd, like non-cobra callers
|
||||
want time.Duration
|
||||
}{
|
||||
{
|
||||
name: "zero when flag not supplied",
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "explicit value returned when flag supplied",
|
||||
setFlag: "3s",
|
||||
want: 3 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "explicit default value still counts as supplied",
|
||||
setFlag: "10s",
|
||||
want: 10 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "nil cmd falls back to option value",
|
||||
noCmd: true,
|
||||
want: defaultTrackLogsInterval,
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
tt := tests[i]
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
options := NewApplyOptions()
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().DurationVar(&options.TrackLogsInterval, "track-logs-interval", options.TrackLogsInterval, "")
|
||||
if tt.setFlag != "" {
|
||||
require.NoError(t, cmd.Flags().Set("track-logs-interval", tt.setFlag))
|
||||
}
|
||||
|
||||
impl := NewApplyImpl(nil, options)
|
||||
if !tt.noCmd {
|
||||
impl.Cmd = cmd
|
||||
}
|
||||
require.Equal(t, tt.want, impl.TrackLogsInterval())
|
||||
})
|
||||
}
|
||||
}
|
||||
+126
-29
@@ -1,44 +1,141 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/maputil"
|
||||
)
|
||||
|
||||
// stateValuesSetStringRE matches a single key=value assignment within one
|
||||
// --state-values-set-string flag value. The value is either a quoted string,
|
||||
// which may contain commas, or a run of plain or backslash-escaped characters
|
||||
// ending at the next unescaped comma.
|
||||
var stateValuesSetStringRE = regexp.MustCompile(`(?s)(?:,|^)([^\s=]+)=(['"][^'"]*['"]|(?:\\.|[^,\\])*\\?)`)
|
||||
|
||||
// NewCLIConfigImpl parses the raw --state-values-set-string and
|
||||
// --state-values-set flag values into state value overrides on g.
|
||||
func NewCLIConfigImpl(g *GlobalImpl) error {
|
||||
re := regexp.MustCompile(`(?:,|^)([^\s=]+)=(['"][^'"]*['"]|[^,]+)`)
|
||||
if err := parseStateValuesSetString(g); err != nil {
|
||||
return err
|
||||
}
|
||||
return parseStateValuesSet(g)
|
||||
}
|
||||
|
||||
// parseStateValuesSetString applies the --state-values-set-string flag values
|
||||
// to g. Unlike --state-values-set, values are kept as strings without type
|
||||
// conversion.
|
||||
func parseStateValuesSetString(g *GlobalImpl) error {
|
||||
optsSet := g.RawStateValuesSetString()
|
||||
if len(optsSet) > 0 {
|
||||
set := map[string]any{}
|
||||
for i := range optsSet {
|
||||
ops := re.FindAllStringSubmatch(optsSet[i], -1)
|
||||
for j := range ops {
|
||||
op := ops[j]
|
||||
k := maputil.ParseKey(op[1])
|
||||
v := op[2]
|
||||
|
||||
maputil.Set(set, k, v, true)
|
||||
}
|
||||
}
|
||||
g.SetSet(set)
|
||||
}
|
||||
optsSet = g.RawStateValuesSet()
|
||||
if len(optsSet) > 0 {
|
||||
set := map[string]any{}
|
||||
for i := range optsSet {
|
||||
ops := strings.Split(optsSet[i], ",")
|
||||
for j := range ops {
|
||||
op := strings.SplitN(ops[j], "=", 2)
|
||||
k := maputil.ParseKey(op[0])
|
||||
v := op[1]
|
||||
|
||||
maputil.Set(set, k, v, false)
|
||||
}
|
||||
}
|
||||
g.SetSet(set)
|
||||
if len(optsSet) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
set := map[string]any{}
|
||||
for _, raw := range optsSet {
|
||||
for _, match := range stateValuesSetStringRE.FindAllStringSubmatch(raw, -1) {
|
||||
key, value := match[1], match[2]
|
||||
maputil.Set(set, maputil.ParseKey(key), unescapeCommas(value), true)
|
||||
}
|
||||
}
|
||||
g.SetSet(set)
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseStateValuesSet applies the --state-values-set flag values to g. Values
|
||||
// are type-converted like Helm's --set.
|
||||
func parseStateValuesSet(g *GlobalImpl) error {
|
||||
optsSet := g.RawStateValuesSet()
|
||||
if len(optsSet) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
set := map[string]any{}
|
||||
for _, raw := range optsSet {
|
||||
for _, assignment := range splitOnUnescapedCommas(raw) {
|
||||
if err := setAssignment(set, assignment); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
g.SetSet(set)
|
||||
return nil
|
||||
}
|
||||
|
||||
// setAssignment parses a single key=value assignment into set, returning an
|
||||
// error instead of panicking when the assignment is malformed.
|
||||
func setAssignment(set map[string]any, assignment string) error {
|
||||
key, value, found := strings.Cut(unescapeCommas(assignment), "=")
|
||||
if !found || key == "" {
|
||||
return fmt.Errorf("--state-values-set: invalid assignment %q: expected <key>=<value>", assignment)
|
||||
}
|
||||
|
||||
maputil.Set(set, maputil.ParseKey(key), value, false)
|
||||
return nil
|
||||
}
|
||||
|
||||
// splitOnUnescapedCommas splits the input on commas that are not escaped by a
|
||||
// preceding backslash, keeping every backslash intact so that escapes remain
|
||||
// available to key and value parsing.
|
||||
func splitOnUnescapedCommas(input string) []string {
|
||||
segments := make([]string, 0, 1+strings.Count(input, ","))
|
||||
var current strings.Builder
|
||||
escaped := false
|
||||
|
||||
for i := range len(input) {
|
||||
char := input[i]
|
||||
|
||||
switch {
|
||||
case escaped:
|
||||
current.WriteByte('\\')
|
||||
current.WriteByte(char)
|
||||
escaped = false
|
||||
case char == '\\':
|
||||
escaped = true
|
||||
case char == ',':
|
||||
segments = append(segments, current.String())
|
||||
current.Reset()
|
||||
default:
|
||||
current.WriteByte(char)
|
||||
}
|
||||
}
|
||||
if escaped {
|
||||
current.WriteByte('\\')
|
||||
}
|
||||
return append(segments, current.String())
|
||||
}
|
||||
|
||||
// unescapeCommas turns every backslash-escaped comma into a literal comma,
|
||||
// preserving all other backslashes, including a trailing one.
|
||||
func unescapeCommas(input string) string {
|
||||
if !strings.Contains(input, `\,`) {
|
||||
return input
|
||||
}
|
||||
|
||||
var out strings.Builder
|
||||
out.Grow(len(input))
|
||||
escaped := false
|
||||
|
||||
for i := range len(input) {
|
||||
char := input[i]
|
||||
|
||||
switch {
|
||||
case escaped && char == ',':
|
||||
out.WriteByte(',')
|
||||
escaped = false
|
||||
case escaped:
|
||||
out.WriteByte('\\')
|
||||
out.WriteByte(char)
|
||||
escaped = false
|
||||
case char == '\\':
|
||||
escaped = true
|
||||
default:
|
||||
out.WriteByte(char)
|
||||
}
|
||||
}
|
||||
if escaped {
|
||||
out.WriteByte('\\')
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewCLIConfigImplStateValuesSet(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
set []string
|
||||
want map[string]any
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "comma separated assignments retain their types",
|
||||
set: []string{"count=2,enabled=true,disabled=false,empty=null,zero=0,code=001"},
|
||||
want: map[string]any{
|
||||
"count": int64(2), "enabled": true, "disabled": false,
|
||||
"empty": nil, "zero": int64(0), "code": "001",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "escaped commas within multiple values",
|
||||
set: []string{`a=1,b=2\,3,c=9\,10\,11`},
|
||||
want: map[string]any{"a": int64(1), "b": "2,3", "c": "9,10,11"},
|
||||
},
|
||||
{
|
||||
name: "escaped commas at the start and end of a value",
|
||||
set: []string{`message=\,hello\,`},
|
||||
want: map[string]any{"message": ",hello,"},
|
||||
},
|
||||
{
|
||||
name: "repeated flags overwrite earlier values",
|
||||
set: []string{`message=first\,second`, `message=third\,fourth,count=2`},
|
||||
want: map[string]any{"message": "third,fourth", "count": int64(2)},
|
||||
},
|
||||
{
|
||||
name: "equals signs remain within values",
|
||||
set: []string{`message=first\,second=value,count=2`},
|
||||
want: map[string]any{"message": "first,second=value", "count": int64(2)},
|
||||
},
|
||||
{
|
||||
name: "unicode values",
|
||||
set: []string{`message=你好\,世界`},
|
||||
want: map[string]any{"message": "你好,世界"},
|
||||
},
|
||||
{
|
||||
name: "empty value",
|
||||
set: []string{"message=,count=2"},
|
||||
want: map[string]any{"message": "", "count": int64(2)},
|
||||
},
|
||||
{
|
||||
name: "backslashes before other characters are preserved",
|
||||
set: []string{`path=C:\tmp\config`},
|
||||
want: map[string]any{"path": `C:\tmp\config`},
|
||||
},
|
||||
{
|
||||
name: "trailing backslash is preserved",
|
||||
set: []string{`path=C:\tmp\`},
|
||||
want: map[string]any{"path": `C:\tmp\`},
|
||||
},
|
||||
{
|
||||
name: "escaped dots in keys are preserved for key parsing",
|
||||
set: []string{`annotations.example\.com/key=first\,second`},
|
||||
want: map[string]any{
|
||||
"annotations": map[string]any{"example.com/key": "first,second"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested array keys",
|
||||
set: []string{`servers[0].host=east\,west,enabled=true`},
|
||||
want: map[string]any{
|
||||
"servers": []any{map[string]any{"host": "east,west"}},
|
||||
"enabled": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "even backslashes leave comma as an assignment separator",
|
||||
set: []string{`path=C:\\,count=2`},
|
||||
want: map[string]any{"path": `C:\\`, "count": int64(2)},
|
||||
},
|
||||
{
|
||||
name: "odd backslashes escape the comma",
|
||||
set: []string{`message=first\\\,second`},
|
||||
want: map[string]any{"message": `first\\,second`},
|
||||
},
|
||||
{
|
||||
name: "assignment without equals sign is rejected",
|
||||
set: []string{"message"},
|
||||
wantErr: `invalid assignment "message": expected <key>=<value>`,
|
||||
},
|
||||
{
|
||||
name: "trailing comma is rejected",
|
||||
set: []string{"message=hello,"},
|
||||
wantErr: `invalid assignment "": expected <key>=<value>`,
|
||||
},
|
||||
{
|
||||
name: "empty key is rejected",
|
||||
set: []string{"=hello"},
|
||||
wantErr: `invalid assignment "=hello": expected <key>=<value>`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewGlobalImpl(&GlobalOptions{StateValuesSet: tt.set})
|
||||
err := NewCLIConfigImpl(g)
|
||||
if tt.wantErr != "" {
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, g.StateValuesSet())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCLIConfigImplStateValuesSetString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
set []string
|
||||
want map[string]any
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "values are kept as strings without type conversion",
|
||||
set: []string{"count=2,enabled=true"},
|
||||
want: map[string]any{"count": "2", "enabled": "true"},
|
||||
},
|
||||
{
|
||||
name: "quoted values may contain commas",
|
||||
set: []string{`zone="zone1,zone2",imageTag=1.23.3`},
|
||||
want: map[string]any{"zone": `"zone1,zone2"`, "imageTag": "1.23.3"},
|
||||
},
|
||||
{
|
||||
name: "escaped commas within values",
|
||||
set: []string{`zone=zone1\,zone2,imageTag=1.23.3`},
|
||||
want: map[string]any{"zone": "zone1,zone2", "imageTag": "1.23.3"},
|
||||
},
|
||||
{
|
||||
name: "backslashes and trailing backslash are preserved",
|
||||
set: []string{`path=C:\tmp\config,trailing=C:\tmp\`},
|
||||
want: map[string]any{"path": `C:\tmp\config`, "trailing": `C:\tmp\`},
|
||||
},
|
||||
{
|
||||
name: "even backslashes leave comma as an assignment separator",
|
||||
set: []string{`path=C:\\,count=2`},
|
||||
want: map[string]any{"path": `C:\\`, "count": "2"},
|
||||
},
|
||||
{
|
||||
name: "empty value",
|
||||
set: []string{"message=,count=2"},
|
||||
want: map[string]any{"message": "", "count": "2"},
|
||||
},
|
||||
{
|
||||
name: "repeated flags overwrite earlier values",
|
||||
set: []string{`zone=a\,b`, `zone=c\,d`},
|
||||
want: map[string]any{"zone": "c,d"},
|
||||
},
|
||||
{
|
||||
name: "escaped dots in keys are preserved for key parsing",
|
||||
set: []string{`annotations.example\.com/key=first\,second`},
|
||||
want: map[string]any{
|
||||
"annotations": map[string]any{"example.com/key": "first,second"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewGlobalImpl(&GlobalOptions{StateValuesSetString: tt.set})
|
||||
err := NewCLIConfigImpl(g)
|
||||
if tt.wantErr != "" {
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, g.StateValuesSet())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCLIConfigImplBothSetFlags(t *testing.T) {
|
||||
g := NewGlobalImpl(&GlobalOptions{
|
||||
StateValuesSet: []string{"count=2"},
|
||||
StateValuesSetString: []string{"imageTag=1.23.3"},
|
||||
})
|
||||
require.NoError(t, NewCLIConfigImpl(g))
|
||||
|
||||
want := map[string]any{
|
||||
"count": int64(2),
|
||||
"imageTag": "1.23.3",
|
||||
}
|
||||
assert.Equal(t, want, g.StateValuesSet())
|
||||
}
|
||||
|
||||
func TestSplitOnUnescapedCommas(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want []string
|
||||
}{
|
||||
{name: "no commas", input: "a=1", want: []string{"a=1"}},
|
||||
{name: "plain separators", input: "a=1,b=2", want: []string{"a=1", "b=2"}},
|
||||
{name: "escaped separator", input: `a=1\,2,b=3`, want: []string{`a=1\,2`, "b=3"}},
|
||||
{name: "escaped separator only", input: `a=1\,2`, want: []string{`a=1\,2`}},
|
||||
{name: "even backslashes unescape the separator", input: `a=1\\,b=2`, want: []string{`a=1\\`, "b=2"}},
|
||||
{name: "trailing separator", input: "a=1,", want: []string{"a=1", ""}},
|
||||
{name: "trailing backslash", input: `a=1\`, want: []string{`a=1\`}},
|
||||
{name: "unicode", input: `a=你好\,世界,b=2`, want: []string{`a=你好\,世界`, "b=2"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, splitOnUnescapedCommas(tt.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnescapeCommas(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{name: "no escapes", input: "a=1,b=2", want: "a=1,b=2"},
|
||||
{name: "escaped comma", input: `a=1\,2`, want: "a=1,2"},
|
||||
{name: "escaped dot preserved", input: `a\.b=1`, want: `a\.b=1`},
|
||||
{name: "even backslashes preserved", input: `a=1\\`, want: `a=1\\`},
|
||||
{name: "trailing backslash preserved", input: `a=1\`, want: `a=1\`},
|
||||
{name: "unicode preserved", input: `a=你好\,世界`, want: "a=你好,世界"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, unescapeCommas(tt.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
+14
-1
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSyncImpl_TrackLogsInterval(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setFlag string // explicit --track-logs-interval value; empty means not supplied
|
||||
noCmd bool // construct SyncImpl without a Cmd, like non-cobra callers
|
||||
want time.Duration
|
||||
}{
|
||||
{
|
||||
name: "zero when flag not supplied",
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "explicit value returned when flag supplied",
|
||||
setFlag: "3s",
|
||||
want: 3 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "explicit default value still counts as supplied",
|
||||
setFlag: "10s",
|
||||
want: 10 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "nil cmd falls back to option value",
|
||||
noCmd: true,
|
||||
want: defaultTrackLogsInterval,
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
tt := tests[i]
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
options := NewSyncOptions()
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().DurationVar(&options.TrackLogsInterval, "track-logs-interval", options.TrackLogsInterval, "")
|
||||
if tt.setFlag != "" {
|
||||
require.NoError(t, cmd.Flags().Set("track-logs-interval", tt.setFlag))
|
||||
}
|
||||
|
||||
impl := NewSyncImpl(nil, options)
|
||||
if !tt.noCmd {
|
||||
impl.Cmd = cmd
|
||||
}
|
||||
require.Equal(t, tt.want, impl.TrackLogsInterval())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package kubedog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/werf/kubedog/pkg/informer"
|
||||
kdutil "github.com/werf/kubedog/pkg/trackers/dyntracker/util"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
type podLogCutoff struct {
|
||||
uid types.UID
|
||||
time time.Time
|
||||
}
|
||||
|
||||
// podLogFilter remembers the first Ready transition independently of kubedog's
|
||||
// workload readiness. It only filters output; pod tracking and log capture continue.
|
||||
type podLogFilter struct {
|
||||
mu sync.RWMutex
|
||||
cutoffs map[string]podLogCutoff
|
||||
}
|
||||
|
||||
func newPodLogFilter() *podLogFilter {
|
||||
return &podLogFilter{cutoffs: make(map[string]podLogCutoff)}
|
||||
}
|
||||
|
||||
// newStartupLogFilter returns a readiness-tracking log filter when log
|
||||
// streaming is limited to startup output, or nil otherwise.
|
||||
func newStartupLogFilter(options *TrackOptions, factory *kdutil.Concurrent[*informer.InformerFactory], targets []trackTarget) (*podLogFilter, error) {
|
||||
if options == nil || !options.Logs || !options.LogsUntilReady {
|
||||
return nil, nil
|
||||
}
|
||||
filter := newPodLogFilter()
|
||||
if err := filter.watch(factory, targets); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
// cutoffTrackedKind reports whether pods of the target kind are subject to
|
||||
// readiness-based log cutoff. Jobs are excluded: they can be Ready while
|
||||
// still executing.
|
||||
func cutoffTrackedKind(kind string) bool {
|
||||
switch kind {
|
||||
case "deploy", "sts", "ds":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// watchNamespacePods attaches the readiness handler to the shared pod
|
||||
// informer for namespace and starts it.
|
||||
func (f *podLogFilter) watchNamespacePods(factory *kdutil.Concurrent[*informer.InformerFactory], gvr schema.GroupVersionResource, namespace string) error {
|
||||
var podInformer *kdutil.Concurrent[*informer.Informer]
|
||||
err := factory.RWTransactionErr(func(factory *informer.InformerFactory) error {
|
||||
informer, err := factory.ForNamespace(gvr, namespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create pod log informer: %w", err)
|
||||
}
|
||||
podInformer = informer
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return podInformer.RWTransactionErr(func(inf *informer.Informer) error {
|
||||
_, err := inf.AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: f.update,
|
||||
UpdateFunc: func(_, obj any) {
|
||||
f.update(obj)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("watch pod readiness for logs: %w", err)
|
||||
}
|
||||
inf.Run()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (f *podLogFilter) watch(factory *kdutil.Concurrent[*informer.InformerFactory], targets []trackTarget) error {
|
||||
podGVR := schema.GroupVersionResource{Version: "v1", Resource: "pods"}
|
||||
namespaces := make(map[string]struct{})
|
||||
for _, target := range targets {
|
||||
if !cutoffTrackedKind(target.kind) {
|
||||
continue
|
||||
}
|
||||
if _, watched := namespaces[target.namespace]; watched {
|
||||
continue
|
||||
}
|
||||
if err := f.watchNamespacePods(factory, podGVR, target.namespace); err != nil {
|
||||
return err
|
||||
}
|
||||
namespaces[target.namespace] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *podLogFilter) update(obj any) {
|
||||
pod, ok := obj.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
id := kdutil.ResourceID(pod.GetName(), pod.GetNamespace(), watchdogPodGVK)
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
// Keep the first Ready transition recorded for this pod instance. A
|
||||
// replacement pod reusing the name starts fresh; delete on a missing key
|
||||
// is a no-op.
|
||||
if cutoff, found := f.cutoffs[id]; found && cutoff.uid == pod.GetUID() {
|
||||
return
|
||||
}
|
||||
delete(f.cutoffs, id)
|
||||
// Job pods can be Ready while still executing; keep their complete output.
|
||||
for _, owner := range pod.GetOwnerReferences() {
|
||||
if owner.Kind == "Job" {
|
||||
return
|
||||
}
|
||||
}
|
||||
conditions, _, err := unstructured.NestedSlice(pod.Object, "status", "conditions")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, raw := range conditions {
|
||||
condition, ok := raw.(map[string]any)
|
||||
if !ok || condition["type"] != "Ready" || condition["status"] != "True" {
|
||||
continue
|
||||
}
|
||||
timestamp, _ := condition["lastTransitionTime"].(string)
|
||||
readyTime, err := time.Parse(time.RFC3339Nano, timestamp)
|
||||
if err != nil {
|
||||
readyTime = time.Now()
|
||||
} else if readyTime.Nanosecond() == 0 {
|
||||
// Kubernetes rounds Ready timestamps to seconds. Preserve startup
|
||||
// lines within that second too, even when delivered after readiness.
|
||||
readyTime = readyTime.Add(time.Second - time.Nanosecond)
|
||||
}
|
||||
f.cutoffs[id] = podLogCutoff{uid: pod.GetUID(), time: readyTime}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (f *podLogFilter) cutoff(name, namespace string, gvk schema.GroupVersionKind) time.Time {
|
||||
if f == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
f.mu.RLock()
|
||||
defer f.mu.RUnlock()
|
||||
return f.cutoffs[kdutil.ResourceID(name, namespace, gvk)].time
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package kubedog
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/werf/kubedog/pkg/informer"
|
||||
kdutil "github.com/werf/kubedog/pkg/trackers/dyntracker/util"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/dynamic/fake"
|
||||
clienttesting "k8s.io/client-go/testing"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
func newLogFilterPod(name, namespace, uid, readyStatus string, readyTime time.Time) *unstructured.Unstructured {
|
||||
pod := &unstructured.Unstructured{Object: map[string]any{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": map[string]any{"name": name, "namespace": namespace, "uid": uid},
|
||||
"status": map[string]any{"phase": "Running"},
|
||||
}}
|
||||
if readyStatus != "" {
|
||||
condition := map[string]any{"type": "Ready", "status": readyStatus}
|
||||
if !readyTime.IsZero() {
|
||||
condition["lastTransitionTime"] = readyTime.Format(time.RFC3339Nano)
|
||||
}
|
||||
pod.Object["status"].(map[string]any)["conditions"] = []any{condition}
|
||||
}
|
||||
return pod
|
||||
}
|
||||
|
||||
func TestPodLogFilter_ReadyCondition(t *testing.T) {
|
||||
readyTime := time.Date(2026, 10, 1, 12, 0, 0, 123, time.UTC)
|
||||
for _, status := range []string{"", "False", "Unknown", "True"} {
|
||||
t.Run("Ready="+status, func(t *testing.T) {
|
||||
filter := newPodLogFilter()
|
||||
filter.update(newLogFilterPod("app", "ns", "uid", status, readyTime))
|
||||
cutoff := filter.cutoff("app", "ns", watchdogPodGVK)
|
||||
if status == "True" {
|
||||
assert.Equal(t, readyTime, cutoff)
|
||||
} else {
|
||||
assert.True(t, cutoff.IsZero(), "Running alone does not mean that a pod is ready")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPodLogFilter_FirstReadyTransitionIsRetained(t *testing.T) {
|
||||
filter := newPodLogFilter()
|
||||
firstReady := time.Date(2026, 10, 1, 12, 0, 0, 123, time.UTC)
|
||||
filter.update(newLogFilterPod("app", "ns", "uid", "True", firstReady))
|
||||
filter.update(newLogFilterPod("app", "ns", "uid", "False", firstReady.Add(time.Minute)))
|
||||
filter.update(newLogFilterPod("app", "ns", "uid", "True", firstReady.Add(2*time.Minute)))
|
||||
assert.Equal(t, firstReady, filter.cutoff("app", "ns", watchdogPodGVK))
|
||||
}
|
||||
|
||||
func TestPodLogFilter_JobPodsAreExempt(t *testing.T) {
|
||||
filter := newPodLogFilter()
|
||||
pod := newLogFilterPod("job-pod", "ns", "uid", "True", time.Now())
|
||||
pod.SetOwnerReferences([]metav1.OwnerReference{{APIVersion: "batch/v1", Kind: "Job", Name: "job"}})
|
||||
filter.update(pod)
|
||||
assert.True(t, filter.cutoff("job-pod", "ns", watchdogPodGVK).IsZero())
|
||||
}
|
||||
|
||||
func TestPodLogFilter_ReplacementPodStartsLoggingAgain(t *testing.T) {
|
||||
filter := newPodLogFilter()
|
||||
firstReady := time.Date(2026, 10, 1, 12, 0, 0, 123, time.UTC)
|
||||
filter.update(newLogFilterPod("app", "ns", "old-uid", "True", firstReady))
|
||||
filter.update(newLogFilterPod("app", "ns", "new-uid", "False", time.Time{}))
|
||||
assert.True(t, filter.cutoff("app", "ns", watchdogPodGVK).IsZero())
|
||||
|
||||
nextReady := firstReady.Add(time.Minute)
|
||||
filter.update(newLogFilterPod("app", "ns", "new-uid", "True", nextReady))
|
||||
assert.Equal(t, nextReady, filter.cutoff("app", "ns", watchdogPodGVK))
|
||||
}
|
||||
|
||||
func TestPodLogFilter_NamespacesAreIndependent(t *testing.T) {
|
||||
filter := newPodLogFilter()
|
||||
readyTime := time.Date(2026, 10, 1, 12, 0, 0, 123, time.UTC)
|
||||
filter.update(newLogFilterPod("app", "ready-ns", "uid", "True", readyTime))
|
||||
filter.update(newLogFilterPod("app", "starting-ns", "uid", "False", time.Time{}))
|
||||
assert.Equal(t, readyTime, filter.cutoff("app", "ready-ns", watchdogPodGVK))
|
||||
assert.True(t, filter.cutoff("app", "starting-ns", watchdogPodGVK).IsZero())
|
||||
}
|
||||
|
||||
func TestPodLogFilter_PreservesReadinessSecond(t *testing.T) {
|
||||
filter := newPodLogFilter()
|
||||
readyTime := time.Date(2026, 10, 1, 12, 0, 0, 0, time.UTC)
|
||||
filter.update(newLogFilterPod("app", "ns", "uid", "True", readyTime))
|
||||
assert.Equal(t, readyTime.Add(time.Second-time.Nanosecond), filter.cutoff("app", "ns", watchdogPodGVK))
|
||||
}
|
||||
|
||||
func TestPodLogFilter_MissingOrInvalidTransitionTime(t *testing.T) {
|
||||
for _, timestamp := range []string{"", "invalid"} {
|
||||
t.Run(timestamp, func(t *testing.T) {
|
||||
filter := newPodLogFilter()
|
||||
pod := newLogFilterPod("app", "ns", "uid", "True", time.Time{})
|
||||
conditions := pod.Object["status"].(map[string]any)["conditions"].([]any)
|
||||
conditions[0].(map[string]any)["lastTransitionTime"] = timestamp
|
||||
before := time.Now()
|
||||
filter.update(pod)
|
||||
after := time.Now()
|
||||
cutoff := filter.cutoff("app", "ns", watchdogPodGVK)
|
||||
assert.False(t, cutoff.Before(before))
|
||||
assert.False(t, cutoff.After(after))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPodLogFilter_WatchesExistingPodsAndUpdatesUsingSharedInformer(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
podGVR := schema.GroupVersionResource{Version: "v1", Resource: "pods"}
|
||||
readyTime := time.Date(2026, 10, 1, 12, 0, 0, 123, time.UTC)
|
||||
readyPod := newLogFilterPod("ready", "ns", "ready-uid", "True", readyTime)
|
||||
startingPod := newLogFilterPod("starting", "ns", "starting-uid", "False", time.Time{})
|
||||
client := fake.NewSimpleDynamicClientWithCustomListKinds(
|
||||
runtime.NewScheme(), map[schema.GroupVersionResource]string{podGVR: "PodList"}, readyPod, startingPod,
|
||||
)
|
||||
var watches atomic.Int32
|
||||
client.PrependWatchReactor("pods", func(clienttesting.Action) (bool, watch.Interface, error) {
|
||||
watches.Add(1)
|
||||
return false, nil, nil
|
||||
})
|
||||
factory := informer.NewConcurrentInformerFactory(ctx.Done(), make(chan error, 1), client,
|
||||
informer.ConcurrentInformerFactoryOptions{})
|
||||
|
||||
// Start another consumer first, as kubedog does during resource tracking.
|
||||
var shared *kdutil.Concurrent[*informer.Informer]
|
||||
var err error
|
||||
factory.RWTransaction(func(factory *informer.InformerFactory) {
|
||||
shared, err = factory.ForNamespace(podGVR, "ns")
|
||||
})
|
||||
require.NoError(t, err)
|
||||
shared.RWTransaction(func(inf *informer.Informer) {
|
||||
_, err = inf.AddEventHandler(cache.ResourceEventHandlerFuncs{})
|
||||
if err == nil {
|
||||
inf.Run()
|
||||
}
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
filter := newPodLogFilter()
|
||||
require.NoError(t, filter.watch(factory, []trackTarget{
|
||||
{kind: "deploy", namespace: "ns"},
|
||||
{kind: "sts", namespace: "ns"},
|
||||
{kind: "job", namespace: "jobs"},
|
||||
{kind: "pvc", namespace: "storage"},
|
||||
{kind: "canary", namespace: "canaries"},
|
||||
}))
|
||||
require.Eventually(t, func() bool {
|
||||
return filter.cutoff("ready", "ns", watchdogPodGVK).Equal(readyTime)
|
||||
}, 5*time.Second, 10*time.Millisecond, "initial add must retain the pod's historical readiness time")
|
||||
assert.True(t, filter.cutoff("starting", "ns", watchdogPodGVK).IsZero())
|
||||
|
||||
nextReady := readyTime.Add(time.Minute)
|
||||
startingPod = newLogFilterPod("starting", "ns", "starting-uid", "True", nextReady)
|
||||
_, err = client.Resource(podGVR).Namespace("ns").Update(ctx, startingPod, metav1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Eventually(t, func() bool {
|
||||
return filter.cutoff("starting", "ns", watchdogPodGVK).Equal(nextReady)
|
||||
}, 5*time.Second, 10*time.Millisecond, "updates must stop logs independently of the other pod")
|
||||
assert.EqualValues(t, 1, watches.Load(), "consumers and repeated targets must share one pod watch")
|
||||
}
|
||||
|
||||
func TestCutoffTrackedKind(t *testing.T) {
|
||||
for kind, want := range map[string]bool{
|
||||
"deploy": true,
|
||||
"sts": true,
|
||||
"ds": true,
|
||||
"job": false,
|
||||
"pvc": false,
|
||||
"canary": false,
|
||||
} {
|
||||
t.Run(kind, func(t *testing.T) {
|
||||
assert.Equal(t, want, cutoffTrackedKind(kind))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewStartupLogFilter(t *testing.T) {
|
||||
// Every combination below must yield a nil filter without touching the
|
||||
// informer factory: startup-only cutoffs apply solely to full streaming.
|
||||
nilFilterCases := []struct {
|
||||
name string
|
||||
options *TrackOptions
|
||||
}{
|
||||
{name: "nil options", options: nil},
|
||||
{name: "no log streaming", options: NewTrackOptions().WithLogsUntilReady(true)},
|
||||
{name: "streaming without cutoff", options: NewTrackOptions().WithLogs(true)},
|
||||
{name: "failed-only mode", options: NewTrackOptions().WithFailedLogsOnly(true).WithLogsUntilReady(true)},
|
||||
}
|
||||
for _, tt := range nilFilterCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
filter, err := newStartupLogFilter(tt.options, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, filter)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:"))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: foo
|
||||
data:
|
||||
foo: FOO
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
localDockerRegistry:
|
||||
enabled: true
|
||||
# Port is not specified, will be dynamically allocated to avoid conflicts
|
||||
chartifyTempDir: temp2824fetch
|
||||
helmfileArgs:
|
||||
- fetch
|
||||
Vendored
+21
@@ -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
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
Building dependency release=foo, chart=$WD/temp2824fetch/foo
|
||||
+1
@@ -0,0 +1 @@
|
||||
Building dependency release=foo, chart=$WD/temp2824fetch/foo
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
localDockerRegistry:
|
||||
enabled: true
|
||||
# Port is not specified, will be dynamically allocated to avoid conflicts
|
||||
chartifyTempDir: temp2824
|
||||
helmfileArgs:
|
||||
- lint
|
||||
Vendored
+7
@@ -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
|
||||
Vendored
+12
@@ -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
|
||||
|
||||
+10
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user