Merge branch 'main' into fix/issue-1904-patch-template-values
This commit is contained in:
commit
6aec9fd6b1
|
|
@ -96,35 +96,35 @@ jobs:
|
|||
- helm-version: v3.18.6
|
||||
kustomize-version: v5.8.0
|
||||
plugin-secrets-version: 4.7.4
|
||||
plugin-diff-version: 3.15.3
|
||||
plugin-diff-version: 3.15.7
|
||||
extra-helmfile-flags: ''
|
||||
# In case you need to test some optional helmfile features,
|
||||
# enable it via extra-helmfile-flags below.
|
||||
- helm-version: v3.18.6
|
||||
kustomize-version: v5.8.0
|
||||
plugin-secrets-version: 4.7.4
|
||||
plugin-diff-version: 3.15.3
|
||||
plugin-diff-version: 3.15.7
|
||||
extra-helmfile-flags: '--enable-live-output'
|
||||
- helm-version: v3.21.0
|
||||
kustomize-version: v5.8.0
|
||||
plugin-secrets-version: 4.7.4
|
||||
plugin-diff-version: 3.15.3
|
||||
plugin-diff-version: 3.15.7
|
||||
extra-helmfile-flags: ''
|
||||
- helm-version: v3.21.0
|
||||
kustomize-version: v5.8.0
|
||||
plugin-secrets-version: 4.7.4
|
||||
plugin-diff-version: 3.15.3
|
||||
plugin-diff-version: 3.15.7
|
||||
extra-helmfile-flags: '--enable-live-output'
|
||||
# Helmfile now supports both Helm 3.x and Helm 4.x
|
||||
- helm-version: v4.2.0
|
||||
kustomize-version: v5.8.0
|
||||
plugin-secrets-version: 4.7.4
|
||||
plugin-diff-version: 3.15.3
|
||||
plugin-diff-version: 3.15.7
|
||||
extra-helmfile-flags: ''
|
||||
- helm-version: v4.2.0
|
||||
kustomize-version: v5.8.0
|
||||
plugin-secrets-version: 4.7.4
|
||||
plugin-diff-version: 3.15.3
|
||||
plugin-diff-version: 3.15.7
|
||||
extra-helmfile-flags: '--enable-live-output'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
|
|
|||
26
cmd/root.go
26
cmd/root.go
|
|
@ -48,6 +48,8 @@ func toCLIError(g *config.GlobalImpl, err error) error {
|
|||
|
||||
// NewRootCmd creates the root command for the CLI.
|
||||
func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) {
|
||||
globalImpl := config.NewGlobalImpl(globalConfig)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "helmfile",
|
||||
Short: globalUsage,
|
||||
|
|
@ -58,11 +60,11 @@ func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) {
|
|||
PersistentPreRunE: func(c *cobra.Command, args []string) error {
|
||||
// Valid levels:
|
||||
// https://github.com/uber-go/zap/blob/7e7e266a8dbce911a49554b945538c5b950196b8/zapcore/level.go#L126
|
||||
logLevel := globalConfig.LogLevel
|
||||
logLevel := globalImpl.LogLevel()
|
||||
switch {
|
||||
case globalConfig.Debug:
|
||||
case globalImpl.Debug():
|
||||
logLevel = "debug"
|
||||
case globalConfig.Quiet:
|
||||
case globalImpl.Quiet():
|
||||
logLevel = "warn"
|
||||
}
|
||||
|
||||
|
|
@ -83,8 +85,6 @@ func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) {
|
|||
|
||||
flags.ParseErrorsAllowlist.UnknownFlags = true
|
||||
|
||||
globalImpl := config.NewGlobalImpl(globalConfig)
|
||||
|
||||
// when set environment HELMFILE_UPGRADE_NOTICE_DISABLED any value, skip upgrade notice.
|
||||
var versionOpts []extension.CobraOption
|
||||
if os.Getenv(envvar.UpgradeNoticeDisabled) == "" {
|
||||
|
|
@ -121,8 +121,8 @@ func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) {
|
|||
}
|
||||
|
||||
func setGlobalOptionsForRootCmd(fs *pflag.FlagSet, globalOptions *config.GlobalOptions) {
|
||||
fs.StringVarP(&globalOptions.HelmBinary, "helm-binary", "b", app.DefaultHelmBinary, "Path to the helm binary")
|
||||
fs.StringVarP(&globalOptions.KustomizeBinary, "kustomize-binary", "k", app.DefaultKustomizeBinary, "Path to the kustomize binary")
|
||||
fs.StringVarP(&globalOptions.HelmBinary, "helm-binary", "b", "", fmt.Sprintf(`Path to the helm binary. Overrides "HELMFILE_HELM_BINARY" OS environment variable when specified (default %q)`, app.DefaultHelmBinary))
|
||||
fs.StringVarP(&globalOptions.KustomizeBinary, "kustomize-binary", "k", "", fmt.Sprintf(`Path to the kustomize binary. Overrides "HELMFILE_KUSTOMIZE_BINARY" OS environment variable when specified (default %q)`, app.DefaultKustomizeBinary))
|
||||
fs.StringVarP(&globalOptions.File, "file", "f", "", "load config from file or directory. defaults to \"`helmfile.yaml`\" or \"helmfile.yaml.gotmpl\" or \"helmfile.d\" (means \"helmfile.d/*.yaml\" or \"helmfile.d/*.yaml.gotmpl\") in this preference. Specify - to load the config from the standard input.")
|
||||
fs.StringVarP(&globalOptions.Environment, "environment", "e", "", `specify the environment name. Overrides "HELMFILE_ENVIRONMENT" OS environment variable when specified. defaults to "default"`)
|
||||
fs.StringArrayVar(&globalOptions.StateValuesSet, "state-values-set", nil, "set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2). Used to override .Values within the helmfile template (not values template).")
|
||||
|
|
@ -134,14 +134,14 @@ func setGlobalOptionsForRootCmd(fs *pflag.FlagSet, globalOptions *config.GlobalO
|
|||
fs.BoolVar(&globalOptions.DisableForceUpdate, "disable-force-update", false, `do not force helm repos to update when executing "helm repo add" (Helm 3 only)`)
|
||||
fs.BoolVar(&globalOptions.EnforcePluginVerification, "enforce-plugin-verification", false, `fail plugin installation if verification is not supported (for security purposes)`)
|
||||
fs.BoolVar(&globalOptions.HelmOCIPlainHTTP, "oci-plain-http", false, `use plain HTTP for OCI registries (required for local/insecure registries in Helm 4)`)
|
||||
fs.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "Silence output. Equivalent to log-level warn")
|
||||
fs.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, `Silence output. Equivalent to log-level warn. Overrides "HELMFILE_QUIET" OS environment variable when specified`)
|
||||
fs.StringVar(&globalOptions.Kubeconfig, "kubeconfig", "", "Use a particular kubeconfig file")
|
||||
fs.StringVar(&globalOptions.KubeContext, "kube-context", "", "Set kubectl context. Uses current context by default")
|
||||
fs.BoolVar(&globalOptions.Debug, "debug", false, "Enable verbose output for Helm and set log-level to debug, this disables --quiet/-q effect")
|
||||
fs.StringVar(&globalOptions.KubeContext, "kube-context", "", `Set kubectl context. Overrides "HELMFILE_KUBE_CONTEXT" OS environment variable when specified. Uses current kubectl context by default`)
|
||||
fs.BoolVar(&globalOptions.Debug, "debug", false, `Enable verbose output for Helm and set log-level to debug, this disables --quiet/-q effect. Overrides "HELMFILE_DEBUG" OS environment variable when specified`)
|
||||
fs.BoolVar(&globalOptions.Color, "color", false, "Output with color")
|
||||
fs.BoolVar(&globalOptions.NoColor, "no-color", false, "Output without color")
|
||||
fs.StringVar(&globalOptions.LogLevel, "log-level", "info", "Set log level, default info")
|
||||
fs.StringVarP(&globalOptions.Namespace, "namespace", "n", "", "Set namespace. Uses the namespace set in the context by default, and is available in templates as {{ .Namespace }}")
|
||||
fs.BoolVar(&globalOptions.NoColor, "no-color", false, `Output without color. Overrides "HELMFILE_NO_COLOR" and "NO_COLOR" OS environment variables when specified`)
|
||||
fs.StringVar(&globalOptions.LogLevel, "log-level", "", `Set log level. Overrides "HELMFILE_LOG_LEVEL" OS environment variable when specified (default "info")`)
|
||||
fs.StringVarP(&globalOptions.Namespace, "namespace", "n", "", `Set namespace. Overrides "HELMFILE_NAMESPACE" OS environment variable when specified. Uses the namespace set in the context by default, and is available in templates as {{ .Namespace }}`)
|
||||
fs.StringVarP(&globalOptions.Chart, "chart", "c", "", "Set chart. Uses the chart set in release by default, and is available in template as {{ .Chart }}")
|
||||
fs.StringArrayVarP(&globalOptions.Selector, "selector", "l", nil, `Only run using the releases that match labels. Labels can take the form of foo=bar or foo!=bar.
|
||||
A release must match all labels in a group in order to be used. Multiple groups can be specified at once.
|
||||
|
|
|
|||
15
cmd/sync.go
15
cmd/sync.go
|
|
@ -60,5 +60,20 @@ func NewSyncCmd(globalCfg *config.GlobalImpl) *cobra.Command {
|
|||
f.BoolVar(&syncOptions.TrackFailOnError, "track-fail-on-error", false, "Fail with non-zero exit code when kubedog tracking fails")
|
||||
f.StringVar(&syncOptions.Description, "description", "", `Set description for all releases. If set, overrides descriptions in helmfile.yaml. Will be passed to "helm upgrade --description"`)
|
||||
|
||||
// Diff-related flags for --interactive mode
|
||||
f.IntVar(&syncOptions.Context, "context", 0, "output NUM lines of context around changes (interactive preview only)")
|
||||
f.StringVar(&syncOptions.DiffOutput, "output", "", "output format for diff plugin (interactive preview only)")
|
||||
f.StringVar(&syncOptions.DiffArgs, "diff-args", "", "pass args to helm-diff (interactive preview only)")
|
||||
f.StringArrayVar(&syncOptions.Suppress, "suppress", nil, "suppress specified Kubernetes objects in the diff output (interactive preview only). Can be provided multiple times. For example: --suppress KeycloakClient --suppress VaultSecret")
|
||||
f.BoolVar(&syncOptions.SuppressSecrets, "suppress-secrets", false, "suppress secrets in the diff output (interactive preview only). highly recommended to specify on CI/CD use-cases")
|
||||
f.BoolVar(&syncOptions.ShowSecrets, "show-secrets", false, "do not redact secret values in the diff output (interactive preview only). should be used for debug purpose only")
|
||||
f.BoolVar(&syncOptions.NoHooks, "no-hooks", false, "do not diff changes made by hooks (interactive preview only)")
|
||||
f.BoolVar(&syncOptions.SuppressDiff, "suppress-diff", false, "suppress diff in the output (interactive preview only). Usable in new installs")
|
||||
f.BoolVar(&syncOptions.SkipDiffOnInstall, "skip-diff-on-install", false, "Skips running helm-diff on releases being newly installed on this sync (interactive preview only). Useful when the release manifests are too huge to be reviewed, or it's too time-consuming to diff at all")
|
||||
f.BoolVar(&syncOptions.IncludeTests, "include-tests", false, "enable the diffing of the helm test hooks (interactive preview only)")
|
||||
f.BoolVar(&syncOptions.DetailedExitcode, "detailed-exitcode", false, "return a non-zero exit code 2 instead of 0 when releases are synced (use --interactive to also see a diff preview)")
|
||||
f.BoolVar(&syncOptions.StripTrailingCR, "strip-trailing-cr", false, "strip trailing carriage return on input (interactive preview only)")
|
||||
f.StringArrayVar(&syncOptions.SuppressOutputLineRegex, "suppress-output-line-regex", nil, "a list of regex patterns to suppress output lines from diff output (interactive preview only)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
|||
16
docs/cli.md
16
docs/cli.md
|
|
@ -40,21 +40,21 @@ Flags:
|
|||
--allow-no-matching-release Do not exit with an error code if the provided selector has no matching releases.
|
||||
-c, --chart string Set chart. Uses the chart set in release by default, and is available in template as {{ .Chart }}
|
||||
--color Output with color
|
||||
--debug Enable verbose output for Helm and set log-level to debug, this disables --quiet/-q effect
|
||||
--debug Enable verbose output for Helm and set log-level to debug, this disables --quiet/-q effect. Overrides "HELMFILE_DEBUG" OS environment variable when specified
|
||||
--disable-force-update do not force helm repos to update when executing "helm repo add"
|
||||
--enable-live-output Show live output from the Helm binary Stdout/Stderr into Helmfile own Stdout/Stderr.
|
||||
It only applies for the Helm CLI commands, Stdout/Stderr for Hooks are still displayed only when it's execution finishes.
|
||||
-e, --environment string specify the environment name. Overrides "HELMFILE_ENVIRONMENT" OS environment variable when specified. defaults to "default"
|
||||
-f, --file helmfile.yaml load config from file or directory. defaults to "helmfile.yaml" or "helmfile.yaml.gotmpl" or "helmfile.d" (means "helmfile.d/*.yaml" or "helmfile.d/*.yaml.gotmpl") in this preference. Specify - to load the config from the standard input.
|
||||
-b, --helm-binary string Path to the helm binary (default "helm")
|
||||
-b, --helm-binary string Path to the helm binary. Overrides "HELMFILE_HELM_BINARY" OS environment variable when specified (default "helm")
|
||||
-h, --help help for helmfile
|
||||
-i, --interactive Request confirmation before attempting to modify clusters
|
||||
--kube-context string Set kubectl context. Uses current context by default
|
||||
-k, --kustomize-binary string Path to the kustomize binary (default "kustomize")
|
||||
--log-level string Set log level, default info (default "info")
|
||||
-n, --namespace string Set namespace. Uses the namespace set in the context by default, and is available in templates as {{ .Namespace }}
|
||||
--no-color Output without color
|
||||
-q, --quiet Silence output. Equivalent to log-level warn
|
||||
--kube-context string Set kubectl context. Overrides "HELMFILE_KUBE_CONTEXT" OS environment variable when specified. Uses current kubectl context by default
|
||||
-k, --kustomize-binary string Path to the kustomize binary. Overrides "HELMFILE_KUSTOMIZE_BINARY" OS environment variable when specified (default "kustomize")
|
||||
--log-level string Set log level. Overrides "HELMFILE_LOG_LEVEL" OS environment variable when specified (default "info")
|
||||
-n, --namespace string Set namespace. Overrides "HELMFILE_NAMESPACE" OS environment variable when specified. Uses the namespace set in the context by default, and is available in templates as {{ .Namespace }}
|
||||
--no-color Output without color. Overrides "HELMFILE_NO_COLOR" and "NO_COLOR" OS environment variables when specified
|
||||
-q, --quiet Silence output. Equivalent to log-level warn. Overrides "HELMFILE_QUIET" OS environment variable when specified
|
||||
-l, --selector stringArray Only run using the releases that match labels. Labels can take the form of foo=bar or foo!=bar.
|
||||
A release must match all labels in a group in order to be used. Multiple groups can be specified at once.
|
||||
"--selector tier=frontend,tier!=proxy --selector tier=backend" will match all frontend, non-proxy releases AND all backend releases.
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ A `helmfile.yaml` has these top-level sections:
|
|||
| `values` | Default values available in templates |
|
||||
| `commonLabels` | Labels applied to all releases |
|
||||
| `templates` | Reusable release templates |
|
||||
| `defaultInherit` | Default template(s) for all releases to inherit |
|
||||
| `hooks` | Global lifecycle hooks |
|
||||
| `apiVersions` / `kubeVersion` | Kubernetes version capabilities |
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,14 @@ Helmfile uses some OS environment variables to override default behaviour:
|
|||
* `HELMFILE_USE_HELM_STATUS_TO_CHECK_RELEASE_EXISTENCE` - expecting non-empty value to use `helm status` to check release existence, instead of `helm list` which is the default behaviour
|
||||
* `HELMFILE_EXPERIMENTAL` - enable experimental features, expecting `true` lower case
|
||||
* `HELMFILE_ENVIRONMENT` - specify [Helmfile environment](environments.md), it has lower priority than CLI argument `--environment`
|
||||
* `HELMFILE_KUBE_CONTEXT` - specify the kubectl context, it has lower priority than CLI argument `--kube-context`
|
||||
* `HELMFILE_NAMESPACE` - specify the namespace, it has lower priority than CLI argument `--namespace`
|
||||
* `HELMFILE_HELM_BINARY` - specify the path to the helm binary, it has lower priority than CLI argument `--helm-binary`
|
||||
* `HELMFILE_KUSTOMIZE_BINARY` - specify the path to the kustomize binary, it has lower priority than CLI argument `--kustomize-binary`
|
||||
* `HELMFILE_LOG_LEVEL` - specify the log level, it has lower priority than CLI argument `--log-level`
|
||||
* `HELMFILE_DEBUG` - enable debug output, expecting `true` lower case. The same as `--debug` CLI flag
|
||||
* `HELMFILE_QUIET` - silence output (equivalent to log-level warn), expecting `true` lower case. The same as `--quiet`/`-q` CLI flag
|
||||
* `HELMFILE_NO_COLOR` - disable colored output, expecting `true` lower case. The same as `--no-color` CLI flag. `NO_COLOR` (any non-empty value, per [no-color.org](https://no-color.org/)) is also honored
|
||||
* `HELMFILE_TEMPDIR` - specify directory to store temporary files
|
||||
* `HELMFILE_UPGRADE_NOTICE_DISABLED` - expecting any non-empty value to skip the check for the latest version of Helmfile in [helmfile version](cli.md#version)
|
||||
* `HELMFILE_GO_YAML_V3` - use *go.yaml.in/yaml/v3* instead of *go.yaml.in/yaml/v2*. It's `false` by default in Helmfile v0.x, and `true` in Helmfile v1.x.
|
||||
|
|
|
|||
|
|
@ -159,6 +159,48 @@ See [issue helmfile/helmfile#435](https://github.com/helmfile/helmfile/issues/43
|
|||
|
||||
You might also find [issue roboll/helmfile#428](https://github.com/roboll/helmfile/issues/428) useful for more context on how we originally designed the release template and what it's supposed to solve.
|
||||
|
||||
### Default Template Inheritance
|
||||
|
||||
When all releases share the same template, specifying `inherit` on each one becomes repetitive. Use `defaultInherit` to apply a template to all releases automatically:
|
||||
|
||||
```yaml
|
||||
templates:
|
||||
default:
|
||||
namespace: kube-system
|
||||
missingFileHandler: Warn
|
||||
values:
|
||||
- config/{{`{{ .Release.Name }}`}}/values.yaml
|
||||
|
||||
defaultInherit: default
|
||||
|
||||
releases:
|
||||
- name: heapster
|
||||
chart: stable/heapster
|
||||
version: 0.3.2
|
||||
# inherits from "default" automatically
|
||||
- name: kubernetes-dashboard
|
||||
chart: stable/kubernetes-dashboard
|
||||
version: 0.10.0
|
||||
inherit:
|
||||
- template: default
|
||||
except:
|
||||
- values
|
||||
```
|
||||
|
||||
`defaultInherit` accepts a single template name or a list:
|
||||
|
||||
```yaml
|
||||
# Single template
|
||||
defaultInherit: default
|
||||
|
||||
# Multiple templates (merged in order)
|
||||
defaultInherit:
|
||||
- ns-template
|
||||
- defaults
|
||||
```
|
||||
|
||||
If a release already inherits from the same template explicitly, the default is not duplicated. Use `except` in the release's `inherit` to exclude specific fields when needed.
|
||||
|
||||
## Layering Release Values
|
||||
|
||||
Please note, that it is not possible to layer `values` sections. If `values` is defined in the release and in the release template, only the `values` defined in the release will be considered. The same applies to `secrets` and `set`.
|
||||
|
|
|
|||
40
go.mod
40
go.mod
|
|
@ -6,18 +6,19 @@ require (
|
|||
dario.cat/mergo v1.0.2
|
||||
github.com/Masterminds/semver/v3 v3.5.0
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.17
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.20
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.102.1
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
|
||||
github.com/go-test/deep v1.1.1
|
||||
github.com/gofrs/flock v0.13.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/gookit/color v1.6.1
|
||||
github.com/gosuri/uitable v0.0.4
|
||||
github.com/hashicorp/go-cty-funcs v0.1.0
|
||||
github.com/hashicorp/go-getter/v2 v2.2.3
|
||||
github.com/hashicorp/hcl/v2 v2.24.0
|
||||
github.com/helmfile/chartify v0.26.3
|
||||
github.com/helmfile/chartify v0.26.4
|
||||
github.com/helmfile/vals v0.44.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
|
|
@ -163,26 +164,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.41.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.51.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.1.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.68.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 // indirect
|
||||
github.com/aws/smithy-go v1.25.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 // indirect
|
||||
github.com/aws/smithy-go v1.26.0 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
|
|
@ -192,7 +193,7 @@ require (
|
|||
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
|
||||
github.com/containerd/containerd v1.7.30 // indirect
|
||||
github.com/containerd/containerd v1.7.32 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
|
|
@ -254,7 +255,6 @@ require (
|
|||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/hashicorp/go-safetemp v1.0.0 // indirect
|
||||
|
|
|
|||
82
go.sum
82
go.sum
|
|
@ -152,50 +152,50 @@ github.com/avelino/slugify v0.0.0-20180501145920-855f152bd774 h1:HrMVYtly2IVqg9E
|
|||
github.com/avelino/slugify v0.0.0-20180501145920-855f152bd774/go.mod h1:5wi5YYOpfuAKwL5XLFYopbgIl/v7NZxaJpa/4X6yFKE=
|
||||
github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ=
|
||||
github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.9 h1:/rYeyO2+HrMztAmxAq9++XJtFMqSIpSsNA0yDGALYq4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.9/go.mod h1:+HsoOEX80qAVUitj1A2DhCNTjmb3edVyuDypb6LNEeo=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10/go.mod h1:qqY157uZoqm5OXq/amuaBJyC9hgBCBQnsaWnPe905GY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.20 h1:8VMDnWc/kEzxsI/1ngGM9mG81a8IGmIHD8KLcYGwagc=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.20/go.mod h1:PuwEpciweIXGULWeOeSTXtSbH4CW9mWdWrhdCKQI1sM=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.19 h1:yuFzSV1U0aRNYCQGVaTY2zW2M/L93pYHnXnrJUphYhU=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.19/go.mod h1:7y63L1kGzeoDlJaQ3Z578KrnmfBut96JjvJUzGwR+YE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25 h1:0w6dCiO8iez+YKwRhRBlL1CH/E3GTfdkuzrwj1by8vo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25/go.mod h1:9FDWUothyr5RCRAHc45XOiVCzUR8n/IhCYX+uVqw6vk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.2 h1:1i1SUOTLk0TbMh7+eJYxgv1r1f47BfR69LL6yaELoI0=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.2/go.mod h1:bo7DhmS/OyVeAJTC768nEk92YKWskqJ4gn0gB5e59qQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 h1:ieLCO1JxUWuxTZ1cRd0GAaeX7O6cIxnwk7tc1LsQhC4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15/go.mod h1:e3IzZvQ3kAWNykvE0Tr0RDZCMFInMvhku3qNpcIQXhM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 h1:03xatSQO4+AM1lTAbnRg5OK528EUg744nW7F73U8DKw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23/go.mod h1:M8l3mwgx5ToK7wot2sBBce/ojzgnPzZXUV445gTSyE8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25 h1:Uii3frf9ztec/ABM2/FSH9/z7PLzxfpG8h4RpkUFflQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25/go.mod h1:G6kntsA2GorAxDPbap6xgB2F+amSLUF8GJTi7PUoX44=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25 h1:r1+/l6m+WaUJF9HISEsNOLHSNj5EXYQxK8VX6Cz9NlA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25/go.mod h1:cKf+D+NMDK1LndD7BowHbBZPgR9V0/5HubH0PFWvA+c=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26 h1:A1PmWU2zfkIm9EyFlJncFXL4W4phML+h8KjltUsCvNQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26/go.mod h1:dY4MRzXEizrD4hqtpKvWVGPX7QleSGGVY+EBolo1RmM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10 h1:d5/908OJ4bXg8lyjeMPvXetEKqoDoLi5Owy1zNue3yg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10/go.mod h1:a57l7Hwh+FWI+we50g5NPJHYUKeJKfXbc4w8SyXu8Ig=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.17 h1:Zma31M1f9bbD/bsl6haTxupA0+z72L3l2ujKAH37zuI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.17/go.mod h1:ZNHrGwBST3tZxBCTKbindx0BEdPN0Jnh7yJ7EVnktUM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25 h1:dD3dhHNglpd98gs72my22Ndqi1hqQGllFFg1F+twfxg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25/go.mod h1:0yAbjPfd64gG7mj85RW+fMEYdfBgCRZw8g/oWcL1pjc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.24 h1:yPLVC8Lbsw92eepgdIZCChHRNQek5eAvAz5wS+UIpJE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.24/go.mod h1:H2h39H1AivHYkozUIUYoVJGMUOvdJ4Lv9DLyUSMAjW8=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.51.0 h1:696UM+NwOrETBCLQJyCAGtVmmZmziBT59yMwgg6Fvrw=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.51.0/go.mod h1:GBO/aaEi47QldDVoqw2CsM2UZQDoqDiFIMJD/ztHPs0=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 h1:etqBTKY581iwLL/H/S2sVgk3C9lAsTJFeXWFDsDcWOU=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.102.1 h1:vttIo8BQwfnhimKRBZBBF3Y38SAIxif72B/M91m9hDk=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.102.1/go.mod h1:2qjInACJr84m/Tm4XXCcVNpejmbKy9kz7TEa6viQHSk=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.6 h1:XR42AXidhYs4HwH0I+yElLXVt7zb2hAyNHQJe6Blv7w=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.6/go.mod h1:nOTsSVQlAsgwVRdtZYtECSnsInF8IUhrpnclCPat7Fs=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.1.1 h1:1VwbP3qMNfxUDEXWki4rCE5iA+44VA1lokTz9HasGzw=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.1.1/go.mod h1:vUtyoSj0OPji3kjIVSc/GlKuWEiL33f/WFxl6dmpy/A=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.68.5 h1:TY5Vh7uXQgJVuc6ahI6toLcRajG1aYSDCP3a0xsPvmo=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.68.5/go.mod h1:UkzShnbxHRIIL2cHi/7fBGLUAZIVTEADQjaA53bWWCE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio=
|
||||
github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
|
||||
github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.19 h1:N6pIsdFOW1Kd9S4KyFKXdGRBojPPxkP32+uHFWLv4Hc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.19/go.mod h1:3gt5WJArFooNmyLONS+h/R4J+o86II8du38IgCwj9dE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2 h1:hc+lBYiiTr8Zk4MTzIsQ92MeDWCIDvWGmzKUWOaBcOg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2/go.mod h1:hU6fqB3OJA6/ePheD47LQnxvjYk6br6PtQxs+Q9ojvk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 h1:ErklX/7uhSbkAAeyQD/Y1OoQ9hO3SJXQNEgksORW3Js=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.42.3/go.mod h1:ULe4HCzfKPiR6R3HEurE3b1upEkuk8AkMrOKtaOxKO8=
|
||||
github.com/aws/smithy-go v1.26.0 h1:9ouqbi+NyKP7fV3Te7UElCwdAb6Y8uk7LGwPE5tVe/s=
|
||||
github.com/aws/smithy-go v1.26.0/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=
|
||||
|
|
@ -227,8 +227,8 @@ github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJ
|
|||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
|
||||
github.com/containerd/containerd v1.7.30 h1:/2vezDpLDVGGmkUXmlNPLCCNKHJ5BbC5tJB5JNzQhqE=
|
||||
github.com/containerd/containerd v1.7.30/go.mod h1:fek494vwJClULlTpExsmOyKCMUAbuVjlFsJQc4/j44M=
|
||||
github.com/containerd/containerd v1.7.32 h1:S54xuVcPxeLaYgaRABtpJ2VyVUVsy0IGf7qHBs+sbY8=
|
||||
github.com/containerd/containerd v1.7.32/go.mod h1:jdwD6s/BhV4XVJGrvtziNPVA+83n66TwptVaPKprq4E=
|
||||
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
|
||||
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
|
|
@ -469,8 +469,10 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA
|
|||
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||
github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI=
|
||||
github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4=
|
||||
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||
github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=
|
||||
github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=
|
||||
github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU=
|
||||
github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
|
||||
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
|
||||
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
|
|
@ -532,8 +534,8 @@ github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e h1:xwy/1T0cxHW
|
|||
github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM=
|
||||
github.com/hashicorp/vault/api v1.23.0 h1:gXgluBsSECfRWTSW9niY2jwg2e9mMJc4WoHNv4g3h6A=
|
||||
github.com/hashicorp/vault/api v1.23.0/go.mod h1:zransKiB9ftp+kgY8ydjnvCU7Wk8i9L0DYWpXeMj9ko=
|
||||
github.com/helmfile/chartify v0.26.3 h1:2wR0yfqtP/yG9y6uqM6nSKZ7W0E+nhhGGRsl14TOVVs=
|
||||
github.com/helmfile/chartify v0.26.3/go.mod h1:/ReUGTnbNHIV5tKAGXODkRtS7HwnUiJi2EXbJ34RzgY=
|
||||
github.com/helmfile/chartify v0.26.4 h1:pIzVe+mqBiBMlJEH3qUVKgFQKV/m4vGOVccdYWY4VbI=
|
||||
github.com/helmfile/chartify v0.26.4/go.mod h1:jnMhinkuwSMfgPPNb3JYges/13xkXPEdUVnh1eGxTOQ=
|
||||
github.com/helmfile/vals v0.44.0 h1:9Yf5JDIl3JUHE1XWR9GopurvAbuXowCSsgUShB4aWcI=
|
||||
github.com/helmfile/vals v0.44.0/go.mod h1:siAvy7f4VPPCrgLGzDOW21ZbvR6Tbf9g7oGRme9fMH4=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||
|
|
|
|||
100
pkg/app/app.go
100
pkg/app/app.go
|
|
@ -484,7 +484,11 @@ func (a *App) Fetch(c FetchConfigProvider) error {
|
|||
}
|
||||
|
||||
func (a *App) Sync(c SyncConfigProvider) error {
|
||||
return a.ForEachState(func(run *Run) (ok bool, errs []error) {
|
||||
var any bool
|
||||
|
||||
mut := &sync.Mutex{}
|
||||
|
||||
err := a.ForEachState(func(run *Run) (ok bool, errs []error) {
|
||||
includeCRDs := !c.SkipCRDs()
|
||||
|
||||
prepErr := run.WithPreparedCharts("sync", state.ChartPrepareOptions{
|
||||
|
|
@ -500,7 +504,14 @@ func (a *App) Sync(c SyncConfigProvider) error {
|
|||
Validate: c.Validate(),
|
||||
Concurrency: c.Concurrency(),
|
||||
}, func() []error {
|
||||
ok, errs = a.SyncState(run, c)
|
||||
matched, updated, es := a.SyncState(run, c)
|
||||
|
||||
mut.Lock()
|
||||
any = any || updated
|
||||
mut.Unlock()
|
||||
|
||||
ok = matched
|
||||
errs = es
|
||||
return errs
|
||||
})
|
||||
|
||||
|
|
@ -510,6 +521,18 @@ func (a *App) Sync(c SyncConfigProvider) error {
|
|||
|
||||
return
|
||||
}, c.IncludeTransitiveNeeds())
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ec, ok := c.(interface{ DetailedExitcode() bool }); ok && ec.DetailedExitcode() && any {
|
||||
code := 2
|
||||
|
||||
return &Error{msg: "", code: &code}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) Apply(c ApplyConfigProvider) error {
|
||||
|
|
@ -2210,16 +2233,16 @@ func (a *App) status(r *Run, c StatusesConfigProvider) (bool, []error) {
|
|||
return true, errs
|
||||
}
|
||||
|
||||
func (a *App) SyncState(r *Run, c SyncConfigProvider) (bool, []error) {
|
||||
func (a *App) SyncState(r *Run, c SyncConfigProvider) (bool, bool, []error) {
|
||||
st := r.state
|
||||
helm := r.helm
|
||||
|
||||
releasesWithNeeds, selectedAndNeededReleases, err := a.GetPlannedAndSelectedReleasesWithNeeds(r, c.SkipNeeds(), c.IncludeNeeds(), c.IncludeTransitiveNeeds())
|
||||
if err != nil {
|
||||
return false, []error{err}
|
||||
return false, false, []error{err}
|
||||
}
|
||||
if len(releasesWithNeeds) == 0 {
|
||||
return false, nil
|
||||
return false, false, nil
|
||||
}
|
||||
|
||||
// Do build deps and prepare only on selected releases so that we won't waste time
|
||||
|
|
@ -2228,7 +2251,7 @@ func (a *App) SyncState(r *Run, c SyncConfigProvider) (bool, []error) {
|
|||
|
||||
toDelete, err := st.DetectReleasesToBeDeletedForSync(helm, releasesWithNeeds)
|
||||
if err != nil {
|
||||
return false, []error{err}
|
||||
return false, false, []error{err}
|
||||
}
|
||||
|
||||
releasesToDelete := map[string]state.ReleaseSpec{}
|
||||
|
|
@ -2279,9 +2302,57 @@ func (a *App) SyncState(r *Run, c SyncConfigProvider) (bool, []error) {
|
|||
// Make the output deterministic for testing purpose
|
||||
sort.Strings(names)
|
||||
|
||||
infoMsg := fmt.Sprintf(`Affected releases are:
|
||||
interactive := c.Interactive()
|
||||
|
||||
var infoMsg string
|
||||
var errs []error
|
||||
|
||||
r.helm.SetExtraArgs(GetArgs(c.Args(), r.state)...)
|
||||
|
||||
operationsAttempted := false
|
||||
|
||||
if interactive {
|
||||
if diffC, ok := c.(DiffConfigProvider); ok {
|
||||
detectedKubeVersion := a.detectKubeVersion(st)
|
||||
diffOpts := &state.DiffOpts{
|
||||
Context: diffC.Context(),
|
||||
Output: diffC.DiffOutput(),
|
||||
Color: diffC.Color(),
|
||||
NoColor: diffC.NoColor(),
|
||||
Set: diffC.Set(),
|
||||
DiffArgs: diffC.DiffArgs(),
|
||||
SkipDiffOnInstall: diffC.SkipDiffOnInstall(),
|
||||
ReuseValues: diffC.ReuseValues(),
|
||||
ResetValues: diffC.ResetValues(),
|
||||
PostRenderer: diffC.PostRenderer(),
|
||||
PostRendererArgs: diffC.PostRendererArgs(),
|
||||
SkipSchemaValidation: diffC.SkipSchemaValidation(),
|
||||
SuppressOutputLineRegex: diffC.SuppressOutputLineRegex(),
|
||||
TakeOwnership: diffC.TakeOwnership(),
|
||||
DetectedKubeVersion: detectedKubeVersion,
|
||||
}
|
||||
infoMsgPtr, _, _, diffErrs := r.diff(false, diffC.DetailedExitcode(), diffC, diffOpts)
|
||||
if len(diffErrs) > 0 {
|
||||
return false, false, diffErrs
|
||||
}
|
||||
if infoMsgPtr != nil {
|
||||
infoMsg = *infoMsgPtr
|
||||
} else {
|
||||
infoMsg = fmt.Sprintf(`Affected releases are:
|
||||
%s
|
||||
`, strings.Join(names, "\n"))
|
||||
}
|
||||
} else {
|
||||
infoMsg = fmt.Sprintf(`Affected releases are:
|
||||
%s
|
||||
`, strings.Join(names, "\n"))
|
||||
}
|
||||
} else {
|
||||
infoMsg = fmt.Sprintf(`Affected releases are:
|
||||
%s
|
||||
`, strings.Join(names, "\n"))
|
||||
a.Logger.Debug(infoMsg)
|
||||
}
|
||||
|
||||
confMsg := fmt.Sprintf(`%s
|
||||
Do you really want to sync?
|
||||
|
|
@ -2289,15 +2360,6 @@ Do you really want to sync?
|
|||
|
||||
`, infoMsg)
|
||||
|
||||
interactive := c.Interactive()
|
||||
if !interactive {
|
||||
a.Logger.Debug(infoMsg)
|
||||
}
|
||||
|
||||
var errs []error
|
||||
|
||||
r.helm.SetExtraArgs(GetArgs(c.Args(), r.state)...)
|
||||
|
||||
// Traverse DAG of all the releases so that we don't suffer from false-positive missing dependencies
|
||||
st.Releases = selectedAndNeededReleases
|
||||
|
||||
|
|
@ -2305,6 +2367,7 @@ Do you really want to sync?
|
|||
|
||||
if !interactive || interactive && r.askForConfirmation(confMsg) {
|
||||
if len(releasesToDelete) > 0 {
|
||||
operationsAttempted = true
|
||||
_, deletionErrs := withDAG(st, helm, a.Logger, state.PlanOptions{Reverse: true, SelectedReleases: toDelete, SkipNeeds: true}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error {
|
||||
var rs []state.ReleaseSpec
|
||||
|
||||
|
|
@ -2326,6 +2389,7 @@ Do you really want to sync?
|
|||
}
|
||||
|
||||
if len(releasesToUpdate) > 0 {
|
||||
operationsAttempted = true
|
||||
_, syncErrs := withDAG(st, helm, a.Logger, state.PlanOptions{SelectedReleases: toUpdate, SkipNeeds: true, IncludeTransitiveNeeds: c.IncludeTransitiveNeeds()}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error {
|
||||
var rs []state.ReleaseSpec
|
||||
|
||||
|
|
@ -2378,7 +2442,9 @@ Do you really want to sync?
|
|||
}
|
||||
}
|
||||
|
||||
return true, errs
|
||||
changesApplied := operationsAttempted && len(errs) == 0
|
||||
|
||||
return true, changesApplied, errs
|
||||
}
|
||||
|
||||
func (a *App) template(r *Run, c TemplateConfigProvider) (bool, []error) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,187 @@ import (
|
|||
"github.com/helmfile/helmfile/pkg/helmexec"
|
||||
)
|
||||
|
||||
func TestSyncInteractive(t *testing.T) {
|
||||
type testcase struct {
|
||||
interactive bool
|
||||
confirm bool
|
||||
error string
|
||||
files map[string]string
|
||||
selectors []string
|
||||
lists map[exectest.ListKey]string
|
||||
diffs map[exectest.DiffKey]error
|
||||
wantDiffs int
|
||||
upgraded []exectest.Release
|
||||
deleted []exectest.Release
|
||||
}
|
||||
|
||||
check := func(t *testing.T, tc testcase) {
|
||||
t.Helper()
|
||||
|
||||
wantUpgrades := tc.upgraded
|
||||
wantDeletes := tc.deleted
|
||||
|
||||
var helm = &exectest.Helm{
|
||||
FailOnUnexpectedList: true,
|
||||
FailOnUnexpectedDiff: true,
|
||||
Lists: tc.lists,
|
||||
Diffs: tc.diffs,
|
||||
DiffMutex: &sync.Mutex{},
|
||||
ChartsMutex: &sync.Mutex{},
|
||||
ReleasesMutex: &sync.Mutex{},
|
||||
}
|
||||
|
||||
bs := runWithLogCapture(t, "debug", func(t *testing.T, logger *zap.SugaredLogger) {
|
||||
t.Helper()
|
||||
|
||||
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error creating vals runtime: %v", err)
|
||||
}
|
||||
|
||||
app := appWithFs(&App{
|
||||
OverrideHelmBinary: DefaultHelmBinary,
|
||||
fs: ffs.DefaultFileSystem(),
|
||||
OverrideKubeContext: "default",
|
||||
DisableKubeVersionAutoDetection: true,
|
||||
Env: "default",
|
||||
Logger: logger,
|
||||
helms: map[helmKey]helmexec.Interface{
|
||||
createHelmKey("helm", "default"): helm,
|
||||
},
|
||||
valsRuntime: valsRuntime,
|
||||
}, tc.files)
|
||||
|
||||
if tc.selectors != nil {
|
||||
app.Selectors = tc.selectors
|
||||
}
|
||||
|
||||
// Use ForEachState to gain access to the Run so we can inject Ask
|
||||
forEachErr := app.ForEachState(func(run *Run) (bool, []error) {
|
||||
run.Ask = func(msg string) bool {
|
||||
return tc.confirm
|
||||
}
|
||||
ok, _, errs := app.SyncState(run, applyConfig{
|
||||
concurrency: 1,
|
||||
interactive: tc.interactive,
|
||||
skipNeeds: true,
|
||||
logger: logger,
|
||||
})
|
||||
return ok, errs
|
||||
}, false)
|
||||
|
||||
var gotErr string
|
||||
if forEachErr != nil {
|
||||
gotErr = forEachErr.Error()
|
||||
}
|
||||
|
||||
if d := cmp.Diff(tc.error, gotErr); d != "" {
|
||||
t.Fatalf("unexpected error: want (-), got (+): %s", d)
|
||||
}
|
||||
|
||||
if len(wantUpgrades) > len(helm.Releases) {
|
||||
t.Fatalf("insufficient number of upgrades: got %d, want %d", len(helm.Releases), len(wantUpgrades))
|
||||
}
|
||||
|
||||
for relIdx := range wantUpgrades {
|
||||
if wantUpgrades[relIdx].Name != helm.Releases[relIdx].Name {
|
||||
t.Errorf("releases[%d].name: got %q, want %q", relIdx, helm.Releases[relIdx].Name, wantUpgrades[relIdx].Name)
|
||||
}
|
||||
for flagIdx := range wantUpgrades[relIdx].Flags {
|
||||
if wantUpgrades[relIdx].Flags[flagIdx] != helm.Releases[relIdx].Flags[flagIdx] {
|
||||
t.Errorf("releases[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Releases[relIdx].Flags[flagIdx], wantUpgrades[relIdx].Flags[flagIdx])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(helm.Diffed) != tc.wantDiffs {
|
||||
t.Fatalf("unexpected number of diffs: got %d, want %d", len(helm.Diffed), tc.wantDiffs)
|
||||
}
|
||||
|
||||
if len(wantDeletes) > len(helm.Deleted) {
|
||||
t.Fatalf("insufficient number of deletes: got %d, want %d", len(helm.Deleted), len(wantDeletes))
|
||||
}
|
||||
})
|
||||
|
||||
_ = bs
|
||||
}
|
||||
|
||||
t.Run("non-interactive: sync proceeds without diff", func(t *testing.T) {
|
||||
check(t, testcase{
|
||||
interactive: false,
|
||||
confirm: false,
|
||||
files: map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
releases:
|
||||
- name: my-release
|
||||
chart: incubator/raw
|
||||
namespace: default
|
||||
`,
|
||||
},
|
||||
upgraded: []exectest.Release{
|
||||
{Name: "my-release", Flags: []string{"--kube-context", "default", "--namespace", "default"}},
|
||||
},
|
||||
lists: map[exectest.ListKey]string{
|
||||
{Filter: "^my-release$", Flags: listFlags("default", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
|
||||
my-release 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default
|
||||
`,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("interactive with diff: user confirms", func(t *testing.T) {
|
||||
check(t, testcase{
|
||||
interactive: true,
|
||||
confirm: true,
|
||||
wantDiffs: 1,
|
||||
files: map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
releases:
|
||||
- name: my-release
|
||||
chart: incubator/raw
|
||||
namespace: default
|
||||
`,
|
||||
},
|
||||
upgraded: []exectest.Release{
|
||||
{Name: "my-release", Flags: []string{"--kube-context", "default", "--namespace", "default"}},
|
||||
},
|
||||
diffs: map[exectest.DiffKey]error{
|
||||
{Name: "my-release", Chart: "incubator/raw", Flags: "--kube-context default --namespace default --reset-values"}: helmexec.ExitError{Code: 2},
|
||||
},
|
||||
lists: map[exectest.ListKey]string{
|
||||
{Filter: "^my-release$", Flags: listFlags("default", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
|
||||
my-release 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default
|
||||
`,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("interactive with diff: user rejects", func(t *testing.T) {
|
||||
check(t, testcase{
|
||||
interactive: true,
|
||||
confirm: false,
|
||||
wantDiffs: 1,
|
||||
files: map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
releases:
|
||||
- name: my-release
|
||||
chart: incubator/raw
|
||||
namespace: default
|
||||
`,
|
||||
},
|
||||
upgraded: []exectest.Release{},
|
||||
diffs: map[exectest.DiffKey]error{
|
||||
{Name: "my-release", Chart: "incubator/raw", Flags: "--kube-context default --namespace default --reset-values"}: helmexec.ExitError{Code: 2},
|
||||
},
|
||||
lists: map[exectest.ListKey]string{
|
||||
{Filter: "^my-release$", Flags: listFlags("default", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
|
||||
my-release 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default
|
||||
`,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestSync(t *testing.T) {
|
||||
type fields struct {
|
||||
skipNeeds bool
|
||||
|
|
@ -27,7 +208,9 @@ func TestSync(t *testing.T) {
|
|||
concurrency int
|
||||
timeout int
|
||||
skipDiffOnInstall bool
|
||||
detailedExitcode bool
|
||||
error string
|
||||
errorCode int
|
||||
files map[string]string
|
||||
selectors []string
|
||||
lists map[exectest.ListKey]string
|
||||
|
|
@ -88,6 +271,7 @@ func TestSync(t *testing.T) {
|
|||
skipNeeds: tc.fields.skipNeeds,
|
||||
includeNeeds: tc.fields.includeNeeds,
|
||||
includeTransitiveNeeds: tc.fields.includeTransitiveNeeds,
|
||||
detailedExitcode: tc.detailedExitcode,
|
||||
})
|
||||
|
||||
var gotErr string
|
||||
|
|
@ -99,6 +283,16 @@ func TestSync(t *testing.T) {
|
|||
t.Fatalf("unexpected error: want (-), got (+): %s", d)
|
||||
}
|
||||
|
||||
if tc.errorCode >= 0 {
|
||||
var gotCode int
|
||||
if appErr, ok := syncErr.(*Error); ok && appErr != nil {
|
||||
gotCode = appErr.Code()
|
||||
}
|
||||
if tc.errorCode != gotCode {
|
||||
t.Fatalf("unexpected error code: got %d, want %d", gotCode, tc.errorCode)
|
||||
}
|
||||
}
|
||||
|
||||
if len(wantUpgrades) > len(helm.Releases) {
|
||||
t.Fatalf("insufficient number of upgrades: got %d, want %d", len(helm.Releases), len(wantUpgrades))
|
||||
}
|
||||
|
|
@ -109,7 +303,7 @@ func TestSync(t *testing.T) {
|
|||
}
|
||||
for flagIdx := range wantUpgrades[relIdx].Flags {
|
||||
if wantUpgrades[relIdx].Flags[flagIdx] != helm.Releases[relIdx].Flags[flagIdx] {
|
||||
t.Errorf("releaes[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Releases[relIdx].Flags[flagIdx], wantUpgrades[relIdx].Flags[flagIdx])
|
||||
t.Errorf("releases[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Releases[relIdx].Flags[flagIdx], wantUpgrades[relIdx].Flags[flagIdx])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -499,6 +693,30 @@ releases:
|
|||
lists: map[exectest.ListKey]string{
|
||||
{Filter: "^my-release$", Flags: listFlags("default", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
|
||||
my-release 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default
|
||||
`,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("detailed-exitcode returns exit code 2 on successful sync", func(t *testing.T) {
|
||||
check(t, testcase{
|
||||
files: map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
releases:
|
||||
- name: my-release
|
||||
chart: incubator/raw
|
||||
namespace: default
|
||||
`,
|
||||
},
|
||||
detailedExitcode: true,
|
||||
errorCode: 2,
|
||||
concurrency: 1,
|
||||
upgraded: []exectest.Release{
|
||||
{Name: "my-release", Flags: []string{"--kube-context", "default", "--namespace", "default"}},
|
||||
},
|
||||
lists: map[exectest.ListKey]string{
|
||||
{Filter: "^my-release$", Flags: listFlags("default", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
|
||||
my-release 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default
|
||||
`,
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -529,3 +529,239 @@ releases:
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestTemplate_DefaultInherit(t *testing.T) {
|
||||
type testcase struct {
|
||||
error string
|
||||
templated []exectest.Release
|
||||
}
|
||||
|
||||
check := func(t *testing.T, tc testcase) {
|
||||
t.Helper()
|
||||
|
||||
var helm = &exectest.Helm{
|
||||
FailOnUnexpectedList: true,
|
||||
FailOnUnexpectedDiff: true,
|
||||
DiffMutex: &sync.Mutex{},
|
||||
ChartsMutex: &sync.Mutex{},
|
||||
ReleasesMutex: &sync.Mutex{},
|
||||
}
|
||||
|
||||
_ = runWithLogCapture(t, "debug", func(t *testing.T, logger *zap.SugaredLogger) {
|
||||
t.Helper()
|
||||
|
||||
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error creating vals runtime: %v", err)
|
||||
}
|
||||
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
templates:
|
||||
default:
|
||||
namespace: default-ns
|
||||
labels:
|
||||
managed: "true"
|
||||
defaultInherit: default
|
||||
releases:
|
||||
- name: app1
|
||||
chart: incubator/raw
|
||||
- name: app2
|
||||
chart: incubator/raw
|
||||
inherit:
|
||||
- template: default
|
||||
except:
|
||||
- labels
|
||||
`,
|
||||
}
|
||||
|
||||
app := appWithFs(&App{
|
||||
OverrideHelmBinary: DefaultHelmBinary,
|
||||
fs: &ffs.FileSystem{Glob: filepath.Glob},
|
||||
OverrideKubeContext: "default",
|
||||
DisableKubeVersionAutoDetection: true,
|
||||
Env: "default",
|
||||
Logger: logger,
|
||||
helms: map[helmKey]helmexec.Interface{
|
||||
createHelmKey("helm", "default"): helm,
|
||||
},
|
||||
valsRuntime: valsRuntime,
|
||||
}, files)
|
||||
|
||||
tmplErr := app.Template(applyConfig{
|
||||
concurrency: 1,
|
||||
logger: logger,
|
||||
})
|
||||
|
||||
var gotErr string
|
||||
if tmplErr != nil {
|
||||
gotErr = tmplErr.Error()
|
||||
}
|
||||
|
||||
if d := cmp.Diff(tc.error, gotErr); d != "" {
|
||||
t.Fatalf("unexpected error: want (-), got (+): %s", d)
|
||||
}
|
||||
|
||||
require.Equal(t, tc.templated, helm.Templated)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("default inherit applies template to all releases", func(t *testing.T) {
|
||||
check(t, testcase{
|
||||
templated: []exectest.Release{
|
||||
{Name: "app1", Flags: []string{"--kube-context", "default", "--namespace", "default-ns"}},
|
||||
{Name: "app2", Flags: []string{"--kube-context", "default", "--namespace", "default-ns"}},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestTemplate_DefaultInherit_Multiple(t *testing.T) {
|
||||
type testcase struct {
|
||||
error string
|
||||
templated []exectest.Release
|
||||
}
|
||||
|
||||
check := func(t *testing.T, tc testcase) {
|
||||
t.Helper()
|
||||
|
||||
var helm = &exectest.Helm{
|
||||
FailOnUnexpectedList: true,
|
||||
FailOnUnexpectedDiff: true,
|
||||
DiffMutex: &sync.Mutex{},
|
||||
ChartsMutex: &sync.Mutex{},
|
||||
ReleasesMutex: &sync.Mutex{},
|
||||
}
|
||||
|
||||
_ = runWithLogCapture(t, "debug", func(t *testing.T, logger *zap.SugaredLogger) {
|
||||
t.Helper()
|
||||
|
||||
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error creating vals runtime: %v", err)
|
||||
}
|
||||
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
templates:
|
||||
ns:
|
||||
namespace: from-ns-template
|
||||
override:
|
||||
namespace: from-ctx-template
|
||||
defaultInherit:
|
||||
- ns
|
||||
- override
|
||||
releases:
|
||||
- name: app1
|
||||
chart: incubator/raw
|
||||
`,
|
||||
}
|
||||
|
||||
app := appWithFs(&App{
|
||||
OverrideHelmBinary: DefaultHelmBinary,
|
||||
fs: &ffs.FileSystem{Glob: filepath.Glob},
|
||||
OverrideKubeContext: "default",
|
||||
DisableKubeVersionAutoDetection: true,
|
||||
Env: "default",
|
||||
Logger: logger,
|
||||
helms: map[helmKey]helmexec.Interface{
|
||||
createHelmKey("helm", "default"): helm,
|
||||
},
|
||||
valsRuntime: valsRuntime,
|
||||
}, files)
|
||||
|
||||
tmplErr := app.Template(applyConfig{
|
||||
concurrency: 1,
|
||||
logger: logger,
|
||||
})
|
||||
|
||||
var gotErr string
|
||||
if tmplErr != nil {
|
||||
gotErr = tmplErr.Error()
|
||||
}
|
||||
|
||||
if d := cmp.Diff(tc.error, gotErr); d != "" {
|
||||
t.Fatalf("unexpected error: want (-), got (+): %s", d)
|
||||
}
|
||||
|
||||
require.Equal(t, tc.templated, helm.Templated)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("multiple default inherits are applied in order", func(t *testing.T) {
|
||||
check(t, testcase{
|
||||
templated: []exectest.Release{
|
||||
{Name: "app1", Flags: []string{"--kube-context", "default", "--namespace", "from-ctx-template"}},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestTemplate_DefaultInherit_NonExistent(t *testing.T) {
|
||||
type testcase struct {
|
||||
error string
|
||||
}
|
||||
|
||||
check := func(t *testing.T, tc testcase) {
|
||||
t.Helper()
|
||||
|
||||
var helm = &exectest.Helm{
|
||||
FailOnUnexpectedList: true,
|
||||
FailOnUnexpectedDiff: true,
|
||||
DiffMutex: &sync.Mutex{},
|
||||
ChartsMutex: &sync.Mutex{},
|
||||
ReleasesMutex: &sync.Mutex{},
|
||||
}
|
||||
|
||||
_ = runWithLogCapture(t, "debug", func(t *testing.T, logger *zap.SugaredLogger) {
|
||||
t.Helper()
|
||||
|
||||
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error creating vals runtime: %v", err)
|
||||
}
|
||||
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
defaultInherit: nonexistent
|
||||
releases:
|
||||
- name: app1
|
||||
chart: incubator/raw
|
||||
`,
|
||||
}
|
||||
|
||||
app := appWithFs(&App{
|
||||
OverrideHelmBinary: DefaultHelmBinary,
|
||||
fs: &ffs.FileSystem{Glob: filepath.Glob},
|
||||
OverrideKubeContext: "default",
|
||||
DisableKubeVersionAutoDetection: true,
|
||||
Env: "default",
|
||||
Logger: logger,
|
||||
helms: map[helmKey]helmexec.Interface{
|
||||
createHelmKey("helm", "default"): helm,
|
||||
},
|
||||
valsRuntime: valsRuntime,
|
||||
}, files)
|
||||
|
||||
tmplErr := app.Template(applyConfig{
|
||||
concurrency: 1,
|
||||
logger: logger,
|
||||
})
|
||||
|
||||
var gotErr string
|
||||
if tmplErr != nil {
|
||||
gotErr = tmplErr.Error()
|
||||
}
|
||||
|
||||
if d := cmp.Diff(tc.error, gotErr); d != "" {
|
||||
t.Fatalf("unexpected error: want (-), got (+): %s", d)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("fail due to non-existent template in defaultInherit", func(t *testing.T) {
|
||||
check(t, testcase{
|
||||
error: `in ./helmfile.yaml: failed executing release templates in "helmfile.yaml": release "app1" tried to inherit inexistent release template "nonexistent"`,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,12 +109,63 @@ func (g *GlobalImpl) SetSet(set map[string]any) {
|
|||
|
||||
// HelmBinary returns the path to the Helm binary.
|
||||
func (g *GlobalImpl) HelmBinary() string {
|
||||
return g.GlobalOptions.HelmBinary
|
||||
var helmBinary string
|
||||
|
||||
switch {
|
||||
case g.GlobalOptions.HelmBinary != "":
|
||||
helmBinary = g.GlobalOptions.HelmBinary
|
||||
case os.Getenv("HELMFILE_HELM_BINARY") != "":
|
||||
helmBinary = os.Getenv("HELMFILE_HELM_BINARY")
|
||||
default:
|
||||
helmBinary = state.DefaultHelmBinary
|
||||
}
|
||||
return helmBinary
|
||||
}
|
||||
|
||||
// KustomizeBinary returns the path to the Kustomize binary.
|
||||
func (g *GlobalImpl) KustomizeBinary() string {
|
||||
return g.GlobalOptions.KustomizeBinary
|
||||
var kustomizeBinary string
|
||||
|
||||
switch {
|
||||
case g.GlobalOptions.KustomizeBinary != "":
|
||||
kustomizeBinary = g.GlobalOptions.KustomizeBinary
|
||||
case os.Getenv("HELMFILE_KUSTOMIZE_BINARY") != "":
|
||||
kustomizeBinary = os.Getenv("HELMFILE_KUSTOMIZE_BINARY")
|
||||
default:
|
||||
kustomizeBinary = state.DefaultKustomizeBinary
|
||||
}
|
||||
return kustomizeBinary
|
||||
}
|
||||
|
||||
// LogLevel returns the log level to use.
|
||||
func (g *GlobalImpl) LogLevel() string {
|
||||
var logLevel string
|
||||
|
||||
switch {
|
||||
case g.GlobalOptions.LogLevel != "":
|
||||
logLevel = g.GlobalOptions.LogLevel
|
||||
case os.Getenv("HELMFILE_LOG_LEVEL") != "":
|
||||
logLevel = os.Getenv("HELMFILE_LOG_LEVEL")
|
||||
default:
|
||||
logLevel = "info"
|
||||
}
|
||||
return logLevel
|
||||
}
|
||||
|
||||
// Debug returns whether debug output is enabled.
|
||||
func (g *GlobalImpl) Debug() bool {
|
||||
if g.GlobalOptions.Debug {
|
||||
return true
|
||||
}
|
||||
return os.Getenv(envvar.Debug) == "true"
|
||||
}
|
||||
|
||||
// Quiet returns whether quiet output is enabled.
|
||||
func (g *GlobalImpl) Quiet() bool {
|
||||
if g.GlobalOptions.Quiet {
|
||||
return true
|
||||
}
|
||||
return os.Getenv(envvar.Quiet) == "true"
|
||||
}
|
||||
|
||||
// Kubeconfig returns the path to the kubeconfig file to use.
|
||||
|
|
@ -124,12 +175,32 @@ func (g *GlobalImpl) Kubeconfig() string {
|
|||
|
||||
// KubeContext returns the name of the kubectl context to use.
|
||||
func (g *GlobalImpl) KubeContext() string {
|
||||
return g.GlobalOptions.KubeContext
|
||||
var kubeContext string
|
||||
|
||||
switch {
|
||||
case g.GlobalOptions.KubeContext != "":
|
||||
kubeContext = g.GlobalOptions.KubeContext
|
||||
case os.Getenv("HELMFILE_KUBE_CONTEXT") != "":
|
||||
kubeContext = os.Getenv("HELMFILE_KUBE_CONTEXT")
|
||||
default:
|
||||
kubeContext = ""
|
||||
}
|
||||
return kubeContext
|
||||
}
|
||||
|
||||
// Namespace returns the namespace to use.
|
||||
func (g *GlobalImpl) Namespace() string {
|
||||
return g.GlobalOptions.Namespace
|
||||
var namespace string
|
||||
|
||||
switch {
|
||||
case g.GlobalOptions.Namespace != "":
|
||||
namespace = g.GlobalOptions.Namespace
|
||||
case os.Getenv("HELMFILE_NAMESPACE") != "":
|
||||
namespace = os.Getenv("HELMFILE_NAMESPACE")
|
||||
default:
|
||||
namespace = ""
|
||||
}
|
||||
return namespace
|
||||
}
|
||||
|
||||
// Chart returns the chart to use.
|
||||
|
|
@ -222,7 +293,7 @@ func (g *GlobalImpl) Color() bool {
|
|||
return c
|
||||
}
|
||||
|
||||
if g.GlobalOptions.NoColor {
|
||||
if g.NoColor() {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -239,7 +310,18 @@ func (g *GlobalImpl) Color() bool {
|
|||
|
||||
// NoColor returns the no color flag
|
||||
func (g *GlobalImpl) NoColor() bool {
|
||||
return g.GlobalOptions.NoColor
|
||||
if g.GlobalOptions.NoColor {
|
||||
return true
|
||||
}
|
||||
// Explicit --color short-circuits env-derived no-color: a flag must win over an env var.
|
||||
if g.GlobalOptions.Color {
|
||||
return false
|
||||
}
|
||||
if os.Getenv(envvar.NoColor) == "true" {
|
||||
return true
|
||||
}
|
||||
// Honor the de-facto https://no-color.org/ standard: any non-empty value disables color.
|
||||
return os.Getenv("NO_COLOR") != ""
|
||||
}
|
||||
|
||||
// Env returns the environment to use.
|
||||
|
|
@ -276,7 +358,7 @@ func (g *GlobalImpl) Interactive() bool {
|
|||
// Args returns the args to use for helm
|
||||
func (g *GlobalImpl) Args() string {
|
||||
args := g.GlobalOptions.Args
|
||||
enableHelmDebug := g.Debug
|
||||
enableHelmDebug := g.Debug()
|
||||
|
||||
if enableHelmDebug {
|
||||
args = fmt.Sprintf("%s %s", args, "--debug")
|
||||
|
|
|
|||
|
|
@ -45,3 +45,378 @@ func TestFileOrDir(t *testing.T) {
|
|||
}
|
||||
os.Unsetenv(envvar.FilePath)
|
||||
}
|
||||
|
||||
// TestKubeContext tests the kube-context flag and HELMFILE_KUBE_CONTEXT env var fallback
|
||||
func TestKubeContext(t *testing.T) {
|
||||
tests := []struct {
|
||||
opts GlobalOptions
|
||||
env string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
opts: GlobalOptions{},
|
||||
env: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{},
|
||||
env: "envset",
|
||||
expected: "envset",
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{KubeContext: "flagset"},
|
||||
env: "",
|
||||
expected: "flagset",
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{KubeContext: "flagset"},
|
||||
env: "envset",
|
||||
expected: "flagset",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
os.Setenv(envvar.KubeContext, test.env)
|
||||
received := NewGlobalImpl(&test.opts).KubeContext()
|
||||
require.Equalf(t, test.expected, received, "KubeContext expected %s, received %s", test.expected, received)
|
||||
}
|
||||
os.Unsetenv(envvar.KubeContext)
|
||||
}
|
||||
|
||||
// TestNamespace tests the namespace flag and HELMFILE_NAMESPACE env var fallback
|
||||
func TestNamespace(t *testing.T) {
|
||||
tests := []struct {
|
||||
opts GlobalOptions
|
||||
env string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
opts: GlobalOptions{},
|
||||
env: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{},
|
||||
env: "envset",
|
||||
expected: "envset",
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{Namespace: "flagset"},
|
||||
env: "",
|
||||
expected: "flagset",
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{Namespace: "flagset"},
|
||||
env: "envset",
|
||||
expected: "flagset",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
os.Setenv(envvar.Namespace, test.env)
|
||||
received := NewGlobalImpl(&test.opts).Namespace()
|
||||
require.Equalf(t, test.expected, received, "Namespace expected %s, received %s", test.expected, received)
|
||||
}
|
||||
os.Unsetenv(envvar.Namespace)
|
||||
}
|
||||
|
||||
// TestHelmBinary tests the helm-binary flag and HELMFILE_HELM_BINARY env var fallback
|
||||
func TestHelmBinary(t *testing.T) {
|
||||
tests := []struct {
|
||||
opts GlobalOptions
|
||||
env string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
opts: GlobalOptions{},
|
||||
env: "",
|
||||
expected: "helm",
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{},
|
||||
env: "envset",
|
||||
expected: "envset",
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{HelmBinary: "flagset"},
|
||||
env: "",
|
||||
expected: "flagset",
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{HelmBinary: "flagset"},
|
||||
env: "envset",
|
||||
expected: "flagset",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
os.Setenv(envvar.HelmBinary, test.env)
|
||||
received := NewGlobalImpl(&test.opts).HelmBinary()
|
||||
require.Equalf(t, test.expected, received, "HelmBinary expected %s, received %s", test.expected, received)
|
||||
}
|
||||
os.Unsetenv(envvar.HelmBinary)
|
||||
}
|
||||
|
||||
// TestKustomizeBinary tests the kustomize-binary flag and HELMFILE_KUSTOMIZE_BINARY env var fallback
|
||||
func TestKustomizeBinary(t *testing.T) {
|
||||
tests := []struct {
|
||||
opts GlobalOptions
|
||||
env string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
opts: GlobalOptions{},
|
||||
env: "",
|
||||
expected: "kustomize",
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{},
|
||||
env: "envset",
|
||||
expected: "envset",
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{KustomizeBinary: "flagset"},
|
||||
env: "",
|
||||
expected: "flagset",
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{KustomizeBinary: "flagset"},
|
||||
env: "envset",
|
||||
expected: "flagset",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
os.Setenv(envvar.KustomizeBinary, test.env)
|
||||
received := NewGlobalImpl(&test.opts).KustomizeBinary()
|
||||
require.Equalf(t, test.expected, received, "KustomizeBinary expected %s, received %s", test.expected, received)
|
||||
}
|
||||
os.Unsetenv(envvar.KustomizeBinary)
|
||||
}
|
||||
|
||||
// TestLogLevel tests the log-level flag and HELMFILE_LOG_LEVEL env var fallback
|
||||
func TestLogLevel(t *testing.T) {
|
||||
tests := []struct {
|
||||
opts GlobalOptions
|
||||
env string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
opts: GlobalOptions{},
|
||||
env: "",
|
||||
expected: "info",
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{},
|
||||
env: "envset",
|
||||
expected: "envset",
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{LogLevel: "flagset"},
|
||||
env: "",
|
||||
expected: "flagset",
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{LogLevel: "flagset"},
|
||||
env: "envset",
|
||||
expected: "flagset",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
os.Setenv(envvar.LogLevel, test.env)
|
||||
received := NewGlobalImpl(&test.opts).LogLevel()
|
||||
require.Equalf(t, test.expected, received, "LogLevel expected %s, received %s", test.expected, received)
|
||||
}
|
||||
os.Unsetenv(envvar.LogLevel)
|
||||
}
|
||||
|
||||
// TestDebug tests the debug flag and HELMFILE_DEBUG env var fallback
|
||||
func TestDebug(t *testing.T) {
|
||||
tests := []struct {
|
||||
opts GlobalOptions
|
||||
env string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
opts: GlobalOptions{},
|
||||
env: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{},
|
||||
env: "true",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{},
|
||||
env: "anything",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{Debug: true},
|
||||
env: "",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{Debug: true},
|
||||
env: "true",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
os.Setenv(envvar.Debug, test.env)
|
||||
received := NewGlobalImpl(&test.opts).Debug()
|
||||
require.Equalf(t, test.expected, received, "Debug expected %t, received %t", test.expected, received)
|
||||
}
|
||||
os.Unsetenv(envvar.Debug)
|
||||
}
|
||||
|
||||
// TestQuiet tests the quiet flag and HELMFILE_QUIET env var fallback
|
||||
func TestQuiet(t *testing.T) {
|
||||
tests := []struct {
|
||||
opts GlobalOptions
|
||||
env string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
opts: GlobalOptions{},
|
||||
env: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{},
|
||||
env: "true",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{},
|
||||
env: "anything",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{Quiet: true},
|
||||
env: "",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{Quiet: true},
|
||||
env: "true",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
os.Setenv(envvar.Quiet, test.env)
|
||||
received := NewGlobalImpl(&test.opts).Quiet()
|
||||
require.Equalf(t, test.expected, received, "Quiet expected %t, received %t", test.expected, received)
|
||||
}
|
||||
os.Unsetenv(envvar.Quiet)
|
||||
}
|
||||
|
||||
// TestNoColor tests the no-color flag, HELMFILE_NO_COLOR and NO_COLOR env var fallbacks
|
||||
func TestNoColor(t *testing.T) {
|
||||
tests := []struct {
|
||||
opts GlobalOptions
|
||||
helmfileEnv string
|
||||
standardEnv string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
opts: GlobalOptions{},
|
||||
helmfileEnv: "",
|
||||
standardEnv: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{},
|
||||
helmfileEnv: "true",
|
||||
standardEnv: "",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{},
|
||||
helmfileEnv: "anything",
|
||||
standardEnv: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{},
|
||||
helmfileEnv: "",
|
||||
standardEnv: "1",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{},
|
||||
helmfileEnv: "",
|
||||
standardEnv: "anything",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
opts: GlobalOptions{NoColor: true},
|
||||
helmfileEnv: "",
|
||||
standardEnv: "",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
os.Setenv(envvar.NoColor, test.helmfileEnv)
|
||||
os.Setenv("NO_COLOR", test.standardEnv)
|
||||
received := NewGlobalImpl(&test.opts).NoColor()
|
||||
require.Equalf(t, test.expected, received, "NoColor expected %t, received %t", test.expected, received)
|
||||
}
|
||||
os.Unsetenv(envvar.NoColor)
|
||||
os.Unsetenv("NO_COLOR")
|
||||
}
|
||||
|
||||
// TestColorRespectsNoColorEnv guards against ValidateConfig() firing when
|
||||
// HELMFILE_NO_COLOR / NO_COLOR is set without an explicit --color/--no-color flag.
|
||||
// Color() must consult NoColor() (which is env-aware) before falling back to TTY autodetect.
|
||||
func TestColorRespectsNoColorEnv(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
helmfileEnv string
|
||||
standardEnv string
|
||||
}{
|
||||
{name: "HELMFILE_NO_COLOR=true", helmfileEnv: "true"},
|
||||
{name: "NO_COLOR set", standardEnv: "1"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Setenv(envvar.NoColor, test.helmfileEnv)
|
||||
t.Setenv("NO_COLOR", test.standardEnv)
|
||||
g := NewGlobalImpl(&GlobalOptions{})
|
||||
require.True(t, g.NoColor(), "NoColor() should be true when env is set")
|
||||
require.False(t, g.Color(), "Color() should be false when NoColor() is true via env")
|
||||
require.NoError(t, g.ValidateConfig(), "ValidateConfig() should not error from env-only no-color")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestColorFlagOverridesNoColorEnv guards against ValidateConfig() firing when
|
||||
// --color is explicitly passed but HELMFILE_NO_COLOR / NO_COLOR is set in the
|
||||
// environment. The flag must win over the env var.
|
||||
func TestColorFlagOverridesNoColorEnv(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
helmfileEnv string
|
||||
standardEnv string
|
||||
}{
|
||||
{name: "HELMFILE_NO_COLOR=true", helmfileEnv: "true"},
|
||||
{name: "NO_COLOR set", standardEnv: "1"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Setenv(envvar.NoColor, test.helmfileEnv)
|
||||
t.Setenv("NO_COLOR", test.standardEnv)
|
||||
g := NewGlobalImpl(&GlobalOptions{Color: true})
|
||||
require.True(t, g.Color(), "Color() should be true when --color is set")
|
||||
require.False(t, g.NoColor(), "NoColor() should be false when --color is set, even if env says otherwise")
|
||||
require.NoError(t, g.ValidateConfig(), "ValidateConfig() should not error when --color overrides env no-color")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,21 @@ type SyncOptions struct {
|
|||
TrackFailOnError bool
|
||||
// Description is the description that will be passed to helm upgrade --description
|
||||
Description string
|
||||
|
||||
// Diff-related options for --interactive mode
|
||||
SuppressOutputLineRegex []string
|
||||
IncludeTests bool
|
||||
Suppress []string
|
||||
SuppressSecrets bool
|
||||
ShowSecrets bool
|
||||
NoHooks bool
|
||||
SuppressDiff bool
|
||||
SkipDiffOnInstall bool
|
||||
DiffArgs string
|
||||
DetailedExitcode bool
|
||||
StripTrailingCR bool
|
||||
Context int
|
||||
DiffOutput string
|
||||
}
|
||||
|
||||
// NewSyncOptions creates a new Apply
|
||||
|
|
@ -228,6 +243,71 @@ func (t *SyncImpl) Description() string {
|
|||
return t.SyncOptions.Description
|
||||
}
|
||||
|
||||
// SuppressOutputLineRegex returns the SuppressOutputLineRegex.
|
||||
func (t *SyncImpl) SuppressOutputLineRegex() []string {
|
||||
return t.SyncOptions.SuppressOutputLineRegex
|
||||
}
|
||||
|
||||
// IncludeTests returns the IncludeTests.
|
||||
func (t *SyncImpl) IncludeTests() bool {
|
||||
return t.SyncOptions.IncludeTests
|
||||
}
|
||||
|
||||
// Suppress returns the Suppress.
|
||||
func (t *SyncImpl) Suppress() []string {
|
||||
return t.SyncOptions.Suppress
|
||||
}
|
||||
|
||||
// SuppressSecrets returns the SuppressSecrets.
|
||||
func (t *SyncImpl) SuppressSecrets() bool {
|
||||
return t.SyncOptions.SuppressSecrets
|
||||
}
|
||||
|
||||
// ShowSecrets returns the ShowSecrets.
|
||||
func (t *SyncImpl) ShowSecrets() bool {
|
||||
return t.SyncOptions.ShowSecrets
|
||||
}
|
||||
|
||||
// NoHooks returns the NoHooks.
|
||||
func (t *SyncImpl) NoHooks() bool {
|
||||
return t.SyncOptions.NoHooks
|
||||
}
|
||||
|
||||
// SuppressDiff returns the SuppressDiff.
|
||||
func (t *SyncImpl) SuppressDiff() bool {
|
||||
return t.SyncOptions.SuppressDiff
|
||||
}
|
||||
|
||||
// SkipDiffOnInstall returns the SkipDiffOnInstall.
|
||||
func (t *SyncImpl) SkipDiffOnInstall() bool {
|
||||
return t.SyncOptions.SkipDiffOnInstall
|
||||
}
|
||||
|
||||
// DiffArgs returns the DiffArgs.
|
||||
func (t *SyncImpl) DiffArgs() string {
|
||||
return t.SyncOptions.DiffArgs
|
||||
}
|
||||
|
||||
// DetailedExitcode returns the DetailedExitcode.
|
||||
func (t *SyncImpl) DetailedExitcode() bool {
|
||||
return t.SyncOptions.DetailedExitcode
|
||||
}
|
||||
|
||||
// StripTrailingCR returns the StripTrailingCR.
|
||||
func (t *SyncImpl) StripTrailingCR() bool {
|
||||
return t.SyncOptions.StripTrailingCR
|
||||
}
|
||||
|
||||
// Context returns the Context.
|
||||
func (t *SyncImpl) Context() int {
|
||||
return t.SyncOptions.Context
|
||||
}
|
||||
|
||||
// DiffOutput returns the DiffOutput.
|
||||
func (t *SyncImpl) DiffOutput() string {
|
||||
return t.SyncOptions.DiffOutput
|
||||
}
|
||||
|
||||
func (t *SyncImpl) ValidateConfig() error {
|
||||
validTrackModes := []string{"helm", "helm-legacy", "kubedog"}
|
||||
if t.SyncOptions.TrackMode != "" && !slices.Contains(validTrackModes, t.SyncOptions.TrackMode) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,14 @@ const (
|
|||
DisableRunnerUniqueID = "HELMFILE_DISABLE_RUNNER_UNIQUE_ID"
|
||||
Experimental = "HELMFILE_EXPERIMENTAL" // environment variable for experimental features, expecting "true" lower case
|
||||
Environment = "HELMFILE_ENVIRONMENT"
|
||||
KubeContext = "HELMFILE_KUBE_CONTEXT"
|
||||
Namespace = "HELMFILE_NAMESPACE"
|
||||
HelmBinary = "HELMFILE_HELM_BINARY"
|
||||
KustomizeBinary = "HELMFILE_KUSTOMIZE_BINARY"
|
||||
LogLevel = "HELMFILE_LOG_LEVEL"
|
||||
Debug = "HELMFILE_DEBUG"
|
||||
Quiet = "HELMFILE_QUIET"
|
||||
NoColor = "HELMFILE_NO_COLOR"
|
||||
FilePath = "HELMFILE_FILE_PATH"
|
||||
TempDir = "HELMFILE_TEMPDIR"
|
||||
UpgradeNoticeDisabled = "HELMFILE_UPGRADE_NOTICE_DISABLED"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,312 @@
|
|||
package kubedog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/werf/kubedog/pkg/tracker/daemonset"
|
||||
"github.com/werf/kubedog/pkg/tracker/deployment"
|
||||
"github.com/werf/kubedog/pkg/tracker/indicators"
|
||||
"github.com/werf/kubedog/pkg/tracker/job"
|
||||
"github.com/werf/kubedog/pkg/tracker/pod"
|
||||
"github.com/werf/kubedog/pkg/tracker/statefulset"
|
||||
"github.com/werf/kubedog/pkg/utils"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
var statusProgressTableRatio = []float64{.58, .11, .12, .19}
|
||||
var statusProgressSubTableRatio = []float64{.40, .15, .20, .25}
|
||||
|
||||
func writeOut(out io.Writer, s string) {
|
||||
_, _ = fmt.Fprint(out, s)
|
||||
}
|
||||
|
||||
func displayDeploymentStatusProgress(out io.Writer, resourceCaption string, status deployment.DeploymentStatus, prevStatus *deployment.DeploymentStatus) {
|
||||
t := utils.NewTable(statusProgressTableRatio...)
|
||||
t.SetWidth(termWidth())
|
||||
|
||||
showProgress := status.StatusGeneration > prevStatus.StatusGeneration
|
||||
|
||||
replicas := "-"
|
||||
if status.ReplicasIndicator != nil {
|
||||
replicas = status.ReplicasIndicator.FormatTableElem(prevStatus.ReplicasIndicator, indicators.FormatTableElemOptions{
|
||||
ShowProgress: showProgress,
|
||||
WithTargetValue: true,
|
||||
})
|
||||
}
|
||||
available := "-"
|
||||
if status.AvailableIndicator != nil {
|
||||
available = status.AvailableIndicator.FormatTableElem(prevStatus.AvailableIndicator, indicators.FormatTableElemOptions{
|
||||
ShowProgress: showProgress,
|
||||
})
|
||||
}
|
||||
uptodate := "-"
|
||||
if status.UpToDateIndicator != nil {
|
||||
uptodate = status.UpToDateIndicator.FormatTableElem(prevStatus.UpToDateIndicator, indicators.FormatTableElemOptions{
|
||||
ShowProgress: showProgress,
|
||||
})
|
||||
}
|
||||
|
||||
t.Header("DEPLOYMENT", "REPLICAS", "AVAILABLE", "UP-TO-DATE")
|
||||
|
||||
args := []interface{}{resourceCaption, replicas, available, uptodate}
|
||||
if status.IsFailed {
|
||||
args = append(args, formatResourceError(status.FailedReason))
|
||||
}
|
||||
t.Row(args...)
|
||||
|
||||
displayChildPodsAndWaiting(&t, prevStatus.Pods, status.Pods, status.NewPodsNames, status.WaitingForMessages)
|
||||
|
||||
writeOut(out, t.Render())
|
||||
}
|
||||
|
||||
func displayStatefulSetStatusProgress(out io.Writer, resourceCaption string, status statefulset.StatefulSetStatus, prevStatus *statefulset.StatefulSetStatus) {
|
||||
t := utils.NewTable(statusProgressTableRatio...)
|
||||
t.SetWidth(termWidth())
|
||||
|
||||
showProgress := status.StatusGeneration > prevStatus.StatusGeneration
|
||||
|
||||
replicas := "-"
|
||||
if status.ReplicasIndicator != nil {
|
||||
replicas = status.ReplicasIndicator.FormatTableElem(prevStatus.ReplicasIndicator, indicators.FormatTableElemOptions{
|
||||
ShowProgress: showProgress,
|
||||
WithTargetValue: true,
|
||||
})
|
||||
}
|
||||
ready := "-"
|
||||
if status.ReadyIndicator != nil {
|
||||
ready = status.ReadyIndicator.FormatTableElem(prevStatus.ReadyIndicator, indicators.FormatTableElemOptions{
|
||||
ShowProgress: showProgress,
|
||||
})
|
||||
}
|
||||
uptodate := "-"
|
||||
if status.UpToDateIndicator != nil {
|
||||
uptodate = status.UpToDateIndicator.FormatTableElem(prevStatus.UpToDateIndicator, indicators.FormatTableElemOptions{
|
||||
ShowProgress: showProgress,
|
||||
})
|
||||
}
|
||||
|
||||
t.Header("STATEFULSET", "REPLICAS", "READY", "UP-TO-DATE")
|
||||
|
||||
args := []interface{}{resourceCaption, replicas, ready, uptodate}
|
||||
if status.IsFailed {
|
||||
args = append(args, formatResourceError(status.FailedReason))
|
||||
} else {
|
||||
for _, w := range status.WarningMessages {
|
||||
args = append(args, formatResourceWarning(w))
|
||||
}
|
||||
}
|
||||
t.Row(args...)
|
||||
|
||||
displayChildPodsAndWaiting(&t, prevStatus.Pods, status.Pods, status.NewPodsNames, status.WaitingForMessages)
|
||||
|
||||
writeOut(out, t.Render())
|
||||
}
|
||||
|
||||
func displayDaemonSetStatusProgress(out io.Writer, resourceCaption string, status daemonset.DaemonSetStatus, prevStatus *daemonset.DaemonSetStatus) {
|
||||
t := utils.NewTable(statusProgressTableRatio...)
|
||||
t.SetWidth(termWidth())
|
||||
|
||||
showProgress := status.StatusGeneration > prevStatus.StatusGeneration
|
||||
|
||||
replicas := "-"
|
||||
if status.ReplicasIndicator != nil {
|
||||
replicas = status.ReplicasIndicator.FormatTableElem(prevStatus.ReplicasIndicator, indicators.FormatTableElemOptions{
|
||||
ShowProgress: showProgress,
|
||||
WithTargetValue: true,
|
||||
})
|
||||
}
|
||||
available := "-"
|
||||
if status.AvailableIndicator != nil {
|
||||
available = status.AvailableIndicator.FormatTableElem(prevStatus.AvailableIndicator, indicators.FormatTableElemOptions{
|
||||
ShowProgress: showProgress,
|
||||
})
|
||||
}
|
||||
uptodate := "-"
|
||||
if status.UpToDateIndicator != nil {
|
||||
uptodate = status.UpToDateIndicator.FormatTableElem(prevStatus.UpToDateIndicator, indicators.FormatTableElemOptions{
|
||||
ShowProgress: showProgress,
|
||||
})
|
||||
}
|
||||
|
||||
t.Header("DAEMONSET", "REPLICAS", "AVAILABLE", "UP-TO-DATE")
|
||||
|
||||
args := []interface{}{resourceCaption, replicas, available, uptodate}
|
||||
if status.IsFailed {
|
||||
args = append(args, formatResourceError(status.FailedReason))
|
||||
}
|
||||
t.Row(args...)
|
||||
|
||||
displayChildPodsAndWaiting(&t, prevStatus.Pods, status.Pods, status.NewPodsNames, status.WaitingForMessages)
|
||||
|
||||
writeOut(out, t.Render())
|
||||
}
|
||||
|
||||
func displayJobStatusProgress(out io.Writer, resourceCaption string, status job.JobStatus, prevStatus *job.JobStatus) {
|
||||
t := utils.NewTable(statusProgressTableRatio...)
|
||||
t.SetWidth(termWidth())
|
||||
|
||||
showProgress := status.StatusGeneration > prevStatus.StatusGeneration
|
||||
|
||||
succeeded := "-"
|
||||
if status.SucceededIndicator != nil {
|
||||
succeeded = status.SucceededIndicator.FormatTableElem(prevStatus.SucceededIndicator, indicators.FormatTableElemOptions{
|
||||
ShowProgress: showProgress,
|
||||
})
|
||||
}
|
||||
|
||||
t.Header("JOB", "ACTIVE", "DURATION", "SUCCEEDED/FAILED")
|
||||
|
||||
var active interface{} = "-"
|
||||
if status.Active != 0 {
|
||||
active = status.Active
|
||||
}
|
||||
failed := fmt.Sprintf("%d", status.Failed)
|
||||
|
||||
args := []interface{}{resourceCaption, active, status.Age, strings.Join([]string{succeeded, failed}, "/")}
|
||||
if status.IsFailed {
|
||||
args = append(args, formatResourceError(status.FailedReason))
|
||||
}
|
||||
t.Row(args...)
|
||||
|
||||
if len(status.Pods) > 0 {
|
||||
st := displayChildPodsStatusProgress(&t, prevStatus.Pods, status.Pods, nil, showProgress)
|
||||
extraMsg := ""
|
||||
if len(status.WaitingForMessages) > 0 {
|
||||
extraMsg += "---\n"
|
||||
extraMsg += utils.BlueF("Waiting for: %s", strings.Join(status.WaitingForMessages, ", "))
|
||||
}
|
||||
st.Commit(extraMsg)
|
||||
}
|
||||
|
||||
writeOut(out, t.Render())
|
||||
}
|
||||
|
||||
func displayChildPodsAndWaiting(t *utils.Table, prevPods, pods map[string]pod.PodStatus, newPodsNames []string, waitingForMessages []string) {
|
||||
if len(pods) > 0 {
|
||||
st := displayChildPodsStatusProgress(t, prevPods, pods, newPodsNames, true)
|
||||
extraMsg := ""
|
||||
if len(waitingForMessages) > 0 {
|
||||
extraMsg += "---\n"
|
||||
extraMsg += utils.BlueF("Waiting for: %s", strings.Join(waitingForMessages, ", "))
|
||||
}
|
||||
st.Commit(extraMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func displayChildPodsStatusProgress(t *utils.Table, prevPods, pods map[string]pod.PodStatus, newPodsNames []string, showProgress bool) *utils.Table {
|
||||
subT := t.SubTable(statusProgressSubTableRatio...)
|
||||
st := &subT
|
||||
|
||||
st.Header("POD", "READY", "RESTARTS", "STATUS")
|
||||
|
||||
podsNames := make([]string, 0, len(pods))
|
||||
for podName := range pods {
|
||||
podsNames = append(podsNames, podName)
|
||||
}
|
||||
sort.Strings(podsNames)
|
||||
|
||||
var podRows [][]interface{}
|
||||
|
||||
newPodSet := make(map[string]struct{}, len(newPodsNames))
|
||||
for _, name := range newPodsNames {
|
||||
newPodSet[name] = struct{}{}
|
||||
}
|
||||
|
||||
for _, podName := range podsNames {
|
||||
var podRow []interface{}
|
||||
|
||||
_, isPodNew := newPodSet[podName]
|
||||
|
||||
prevPodStatus := prevPods[podName]
|
||||
podStatus := pods[podName]
|
||||
|
||||
isReady := false
|
||||
if podStatus.StatusIndicator != nil {
|
||||
isReady = podStatus.StatusIndicator.IsReady()
|
||||
}
|
||||
|
||||
resource := formatPodResourceCaption(podName, isReady, podStatus.IsFailed, isPodNew)
|
||||
ready := fmt.Sprintf("%d/%d", podStatus.ReadyContainers, podStatus.TotalContainers)
|
||||
|
||||
status := "-"
|
||||
if podStatus.StatusIndicator != nil {
|
||||
status = podStatus.StatusIndicator.FormatTableElem(prevPodStatus.StatusIndicator, indicators.FormatTableElemOptions{
|
||||
ShowProgress: showProgress,
|
||||
IsResourceNew: isPodNew,
|
||||
})
|
||||
}
|
||||
|
||||
podRow = append(podRow, resource, ready, podStatus.Restarts, status)
|
||||
if podStatus.IsFailed {
|
||||
podRow = append(podRow, formatResourceError(podStatus.FailedReason))
|
||||
}
|
||||
|
||||
podRows = append(podRows, podRow)
|
||||
}
|
||||
|
||||
st.Rows(podRows...)
|
||||
|
||||
return st
|
||||
}
|
||||
|
||||
func formatResourceCaption(caption string, isReady, isFailed bool) string {
|
||||
switch {
|
||||
case isReady:
|
||||
return utils.GreenF("%s", caption)
|
||||
case isFailed:
|
||||
return utils.RedF("%s", caption)
|
||||
default:
|
||||
return utils.YellowF("%s", caption)
|
||||
}
|
||||
}
|
||||
|
||||
func formatPodResourceCaption(podName string, isReady, isFailed, isNew bool) string {
|
||||
if !isNew {
|
||||
return podName
|
||||
}
|
||||
return formatResourceCaption(podName, isReady, isFailed)
|
||||
}
|
||||
|
||||
func formatResourceError(reason string) string {
|
||||
return utils.RedF("error: %s", reason)
|
||||
}
|
||||
|
||||
func formatResourceWarning(reason string) string {
|
||||
return utils.YellowF("warning: %s", reason)
|
||||
}
|
||||
|
||||
func termWidth() int {
|
||||
if w, _, err := term.GetSize(int(os.Stderr.Fd())); err == nil && w > 0 {
|
||||
return w
|
||||
}
|
||||
return 140
|
||||
}
|
||||
|
||||
func displayCanaryStatus(out io.Writer, resourceCaption string, status CanaryStatusView) {
|
||||
var parts []string
|
||||
if status.Phase != "" {
|
||||
parts = append(parts, fmt.Sprintf("phase %s", status.Phase))
|
||||
}
|
||||
if status.Age != "" {
|
||||
parts = append(parts, fmt.Sprintf("age %s", status.Age))
|
||||
}
|
||||
msg := fmt.Sprintf("%s: %s", resourceCaption, strings.Join(parts, ", "))
|
||||
if status.IsFailed {
|
||||
msg = utils.RedF("%s", msg)
|
||||
}
|
||||
_, _ = fmt.Fprintln(out, msg)
|
||||
}
|
||||
|
||||
type CanaryStatusView struct {
|
||||
Phase string
|
||||
Age string
|
||||
IsFailed bool
|
||||
}
|
||||
|
||||
func statusOutput() io.Writer {
|
||||
return os.Stderr
|
||||
}
|
||||
|
|
@ -0,0 +1,453 @@
|
|||
package kubedog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gookit/color"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/werf/kubedog/pkg/tracker/daemonset"
|
||||
"github.com/werf/kubedog/pkg/tracker/deployment"
|
||||
"github.com/werf/kubedog/pkg/tracker/job"
|
||||
"github.com/werf/kubedog/pkg/tracker/pod"
|
||||
"github.com/werf/kubedog/pkg/tracker/statefulset"
|
||||
)
|
||||
|
||||
// TestMain forces ANSI color output so that tests asserting on escape codes
|
||||
// pass in non-TTY environments such as CI runners.
|
||||
func TestMain(m *testing.M) {
|
||||
color.ForceColor()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
// --- formatResourceCaption ---
|
||||
|
||||
func TestFormatResourceCaption_Ready(t *testing.T) {
|
||||
result := formatResourceCaption("deploy/myapp", true, false)
|
||||
assert.Contains(t, result, "deploy/myapp")
|
||||
// Green ANSI escape should be present
|
||||
assert.Contains(t, result, "\033[")
|
||||
}
|
||||
|
||||
func TestFormatResourceCaption_Failed(t *testing.T) {
|
||||
result := formatResourceCaption("deploy/myapp", false, true)
|
||||
assert.Contains(t, result, "deploy/myapp")
|
||||
assert.Contains(t, result, "\033[")
|
||||
}
|
||||
|
||||
func TestFormatResourceCaption_InProgress(t *testing.T) {
|
||||
result := formatResourceCaption("deploy/myapp", false, false)
|
||||
assert.Contains(t, result, "deploy/myapp")
|
||||
// Yellow for in-progress
|
||||
assert.Contains(t, result, "\033[")
|
||||
}
|
||||
|
||||
func TestFormatResourceCaption_ReadyTakesPrecedence(t *testing.T) {
|
||||
// isReady=true should win over isFailed=true
|
||||
resultReady := formatResourceCaption("x", true, false)
|
||||
resultFailed := formatResourceCaption("x", false, true)
|
||||
// Colors should differ
|
||||
assert.NotEqual(t, resultReady, resultFailed)
|
||||
}
|
||||
|
||||
// --- formatPodResourceCaption ---
|
||||
|
||||
func TestFormatPodResourceCaption_NotNew(t *testing.T) {
|
||||
result := formatPodResourceCaption("my-pod-abc", true, false, false)
|
||||
// Not a new pod: no coloring applied, just the plain name
|
||||
assert.Equal(t, "my-pod-abc", result)
|
||||
}
|
||||
|
||||
func TestFormatPodResourceCaption_NewAndReady(t *testing.T) {
|
||||
result := formatPodResourceCaption("my-pod-abc", true, false, true)
|
||||
assert.Contains(t, result, "my-pod-abc")
|
||||
assert.Contains(t, result, "\033[")
|
||||
}
|
||||
|
||||
func TestFormatPodResourceCaption_NewAndFailed(t *testing.T) {
|
||||
result := formatPodResourceCaption("my-pod-abc", false, true, true)
|
||||
assert.Contains(t, result, "my-pod-abc")
|
||||
assert.Contains(t, result, "\033[")
|
||||
}
|
||||
|
||||
func TestFormatPodResourceCaption_NewInProgress(t *testing.T) {
|
||||
result := formatPodResourceCaption("my-pod-abc", false, false, true)
|
||||
assert.Contains(t, result, "my-pod-abc")
|
||||
assert.Contains(t, result, "\033[")
|
||||
}
|
||||
|
||||
// --- formatResourceError / formatResourceWarning ---
|
||||
|
||||
func TestFormatResourceError(t *testing.T) {
|
||||
result := formatResourceError("CrashLoopBackOff")
|
||||
assert.Contains(t, result, "error:")
|
||||
assert.Contains(t, result, "CrashLoopBackOff")
|
||||
}
|
||||
|
||||
func TestFormatResourceWarning(t *testing.T) {
|
||||
result := formatResourceWarning("PodNotScheduled")
|
||||
assert.Contains(t, result, "warning:")
|
||||
assert.Contains(t, result, "PodNotScheduled")
|
||||
}
|
||||
|
||||
// --- termWidth ---
|
||||
|
||||
func TestTermWidth_ReturnsPositive(t *testing.T) {
|
||||
w := termWidth()
|
||||
assert.Greater(t, w, 0)
|
||||
}
|
||||
|
||||
// --- displayDeploymentStatusProgress ---
|
||||
|
||||
func TestDisplayDeploymentStatusProgress_ZeroStatus(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
caption := formatResourceCaption("deploy/myapp", false, false)
|
||||
var prev deployment.DeploymentStatus
|
||||
status := deployment.DeploymentStatus{}
|
||||
|
||||
// Must not panic and must produce some output
|
||||
assert.NotPanics(t, func() {
|
||||
displayDeploymentStatusProgress(&buf, caption, status, &prev)
|
||||
})
|
||||
out := buf.String()
|
||||
assert.NotEmpty(t, out)
|
||||
assert.Contains(t, out, "DEPLOYMENT")
|
||||
}
|
||||
|
||||
func TestDisplayDeploymentStatusProgress_Failed(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
caption := formatResourceCaption("deploy/myapp", false, true)
|
||||
var prev deployment.DeploymentStatus
|
||||
status := deployment.DeploymentStatus{
|
||||
IsFailed: true,
|
||||
FailedReason: "ImagePullBackOff",
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
displayDeploymentStatusProgress(&buf, caption, status, &prev)
|
||||
})
|
||||
out := buf.String()
|
||||
assert.Contains(t, out, "error:")
|
||||
assert.Contains(t, out, "ImagePullBackOff")
|
||||
}
|
||||
|
||||
func TestDisplayDeploymentStatusProgress_WithWaitingMessage(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
caption := formatResourceCaption("deploy/myapp", false, false)
|
||||
var prev deployment.DeploymentStatus
|
||||
// WaitingForMessages is only rendered when there are pods
|
||||
status := deployment.DeploymentStatus{
|
||||
StatusGeneration: 1,
|
||||
WaitingForMessages: []string{"up-to-date 1->3"},
|
||||
Pods: map[string]pod.PodStatus{
|
||||
"myapp-pod-abc": {ReadyContainers: 1, TotalContainers: 1},
|
||||
},
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
displayDeploymentStatusProgress(&buf, caption, status, &prev)
|
||||
})
|
||||
out := buf.String()
|
||||
assert.Contains(t, out, "Waiting for:")
|
||||
assert.Contains(t, out, "up-to-date 1->3")
|
||||
}
|
||||
|
||||
func TestDisplayDeploymentStatusProgress_WithPods(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
caption := formatResourceCaption("deploy/myapp", false, false)
|
||||
prev := deployment.DeploymentStatus{}
|
||||
status := deployment.DeploymentStatus{
|
||||
StatusGeneration: 1,
|
||||
Pods: map[string]pod.PodStatus{
|
||||
"myapp-abc-123": {ReadyContainers: 1, TotalContainers: 1},
|
||||
},
|
||||
NewPodsNames: []string{"myapp-abc-123"},
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
displayDeploymentStatusProgress(&buf, caption, status, &prev)
|
||||
})
|
||||
out := buf.String()
|
||||
assert.Contains(t, out, "POD")
|
||||
assert.Contains(t, out, "myapp-abc-123")
|
||||
}
|
||||
|
||||
// --- displayStatefulSetStatusProgress ---
|
||||
|
||||
func TestDisplayStatefulSetStatusProgress_ZeroStatus(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
caption := formatResourceCaption("sts/myapp", false, false)
|
||||
var prev statefulset.StatefulSetStatus
|
||||
status := statefulset.StatefulSetStatus{}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
displayStatefulSetStatusProgress(&buf, caption, status, &prev)
|
||||
})
|
||||
out := buf.String()
|
||||
assert.Contains(t, out, "STATEFULSET")
|
||||
}
|
||||
|
||||
func TestDisplayStatefulSetStatusProgress_WithWarnings(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
caption := formatResourceCaption("sts/myapp", false, false)
|
||||
var prev statefulset.StatefulSetStatus
|
||||
status := statefulset.StatefulSetStatus{
|
||||
WarningMessages: []string{"PodNotScheduled: insufficient resources"},
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
displayStatefulSetStatusProgress(&buf, caption, status, &prev)
|
||||
})
|
||||
out := buf.String()
|
||||
assert.Contains(t, out, "warning:")
|
||||
assert.Contains(t, out, "PodNotScheduled")
|
||||
}
|
||||
|
||||
func TestDisplayStatefulSetStatusProgress_Failed(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
caption := formatResourceCaption("sts/myapp", false, true)
|
||||
var prev statefulset.StatefulSetStatus
|
||||
status := statefulset.StatefulSetStatus{
|
||||
IsFailed: true,
|
||||
FailedReason: "timeout waiting for ready",
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
displayStatefulSetStatusProgress(&buf, caption, status, &prev)
|
||||
})
|
||||
out := buf.String()
|
||||
assert.Contains(t, out, "error:")
|
||||
assert.Contains(t, out, "timeout waiting for ready")
|
||||
}
|
||||
|
||||
// --- displayDaemonSetStatusProgress ---
|
||||
|
||||
func TestDisplayDaemonSetStatusProgress_ZeroStatus(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
caption := formatResourceCaption("ds/myapp", false, false)
|
||||
var prev daemonset.DaemonSetStatus
|
||||
status := daemonset.DaemonSetStatus{}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
displayDaemonSetStatusProgress(&buf, caption, status, &prev)
|
||||
})
|
||||
out := buf.String()
|
||||
assert.Contains(t, out, "DAEMONSET")
|
||||
}
|
||||
|
||||
func TestDisplayDaemonSetStatusProgress_Failed(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
caption := formatResourceCaption("ds/myapp", false, true)
|
||||
var prev daemonset.DaemonSetStatus
|
||||
status := daemonset.DaemonSetStatus{
|
||||
IsFailed: true,
|
||||
FailedReason: "node not ready",
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
displayDaemonSetStatusProgress(&buf, caption, status, &prev)
|
||||
})
|
||||
out := buf.String()
|
||||
assert.Contains(t, out, "error:")
|
||||
assert.Contains(t, out, "node not ready")
|
||||
}
|
||||
|
||||
// --- displayJobStatusProgress ---
|
||||
|
||||
func TestDisplayJobStatusProgress_ZeroStatus(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
caption := formatResourceCaption("job/myjob", false, false)
|
||||
var prev job.JobStatus
|
||||
status := job.JobStatus{}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
displayJobStatusProgress(&buf, caption, status, &prev)
|
||||
})
|
||||
out := buf.String()
|
||||
assert.Contains(t, out, "JOB")
|
||||
}
|
||||
|
||||
func TestDisplayJobStatusProgress_Active(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
caption := formatResourceCaption("job/myjob", false, false)
|
||||
var prev job.JobStatus
|
||||
status := job.JobStatus{
|
||||
StatusGeneration: 1,
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
displayJobStatusProgress(&buf, caption, status, &prev)
|
||||
})
|
||||
out := buf.String()
|
||||
assert.Contains(t, out, "ACTIVE")
|
||||
}
|
||||
|
||||
func TestDisplayJobStatusProgress_Failed(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
caption := formatResourceCaption("job/myjob", false, true)
|
||||
var prev job.JobStatus
|
||||
status := job.JobStatus{
|
||||
IsFailed: true,
|
||||
FailedReason: "BackoffLimitExceeded",
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
displayJobStatusProgress(&buf, caption, status, &prev)
|
||||
})
|
||||
out := buf.String()
|
||||
assert.Contains(t, out, "error:")
|
||||
assert.Contains(t, out, "BackoffLimitExceeded")
|
||||
}
|
||||
|
||||
func TestDisplayJobStatusProgress_WithWaitingMessage(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
caption := formatResourceCaption("job/myjob", false, false)
|
||||
var prev job.JobStatus
|
||||
status := job.JobStatus{
|
||||
WaitingForMessages: []string{"succeeded 0->1"},
|
||||
Pods: map[string]pod.PodStatus{
|
||||
"myjob-abc": {ReadyContainers: 0, TotalContainers: 1},
|
||||
},
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
displayJobStatusProgress(&buf, caption, status, &prev)
|
||||
})
|
||||
out := buf.String()
|
||||
assert.Contains(t, out, "Waiting for:")
|
||||
assert.Contains(t, out, "succeeded 0->1")
|
||||
}
|
||||
|
||||
// --- displayChildPodsStatusProgress ---
|
||||
|
||||
func TestDisplayChildPodsStatusProgress_Empty(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
caption := formatResourceCaption("deploy/myapp", false, false)
|
||||
// With no pods, only the header should be rendered
|
||||
prev := deployment.DeploymentStatus{}
|
||||
status := deployment.DeploymentStatus{
|
||||
Pods: map[string]pod.PodStatus{},
|
||||
}
|
||||
assert.NotPanics(t, func() {
|
||||
displayDeploymentStatusProgress(&buf, caption, status, &prev)
|
||||
})
|
||||
// No POD sub-table header when pods is empty
|
||||
out := buf.String()
|
||||
assert.NotContains(t, out, "POD")
|
||||
}
|
||||
|
||||
func TestDisplayChildPodsStatusProgress_NewPodSet(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
caption := formatResourceCaption("deploy/myapp", false, false)
|
||||
prev := deployment.DeploymentStatus{}
|
||||
// Two pods: one new, one old
|
||||
status := deployment.DeploymentStatus{
|
||||
StatusGeneration: 1,
|
||||
Pods: map[string]pod.PodStatus{
|
||||
"pod-new-abc": {ReadyContainers: 0, TotalContainers: 1},
|
||||
"pod-old-xyz": {ReadyContainers: 1, TotalContainers: 1},
|
||||
},
|
||||
NewPodsNames: []string{"pod-new-abc"},
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
displayDeploymentStatusProgress(&buf, caption, status, &prev)
|
||||
})
|
||||
out := buf.String()
|
||||
assert.Contains(t, out, "pod-new-abc")
|
||||
assert.Contains(t, out, "pod-old-xyz")
|
||||
}
|
||||
|
||||
func TestDisplayChildPodsStatusProgress_ManyPodsO1Check(t *testing.T) {
|
||||
// Verifies O(1) new-pod detection works correctly for many pods
|
||||
var buf bytes.Buffer
|
||||
caption := formatResourceCaption("deploy/myapp", false, false)
|
||||
prev := deployment.DeploymentStatus{}
|
||||
|
||||
pods := make(map[string]pod.PodStatus)
|
||||
newNames := make([]string, 0, 10)
|
||||
for i := 0; i < 20; i++ {
|
||||
name := strings.Repeat("a", i+1)
|
||||
pods[name] = pod.PodStatus{ReadyContainers: 1, TotalContainers: 1}
|
||||
if i%2 == 0 {
|
||||
newNames = append(newNames, name)
|
||||
}
|
||||
}
|
||||
status := deployment.DeploymentStatus{
|
||||
StatusGeneration: 1,
|
||||
Pods: pods,
|
||||
NewPodsNames: newNames,
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
displayDeploymentStatusProgress(&buf, caption, status, &prev)
|
||||
})
|
||||
assert.NotEmpty(t, buf.String())
|
||||
}
|
||||
|
||||
// --- displayCanaryStatus ---
|
||||
|
||||
func TestDisplayCanaryStatus_Normal(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
caption := formatResourceCaption("canary/myapp", false, false)
|
||||
view := CanaryStatusView{Phase: "Progressing", Age: "1m"}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
displayCanaryStatus(&buf, caption, view)
|
||||
})
|
||||
out := buf.String()
|
||||
assert.Contains(t, out, "Progressing")
|
||||
assert.Contains(t, out, "1m")
|
||||
}
|
||||
|
||||
func TestDisplayCanaryStatus_Failed(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
caption := formatResourceCaption("canary/myapp", false, true)
|
||||
view := CanaryStatusView{Phase: "Failed", IsFailed: true}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
displayCanaryStatus(&buf, caption, view)
|
||||
})
|
||||
out := buf.String()
|
||||
assert.Contains(t, out, "Failed")
|
||||
}
|
||||
|
||||
func TestDisplayCanaryStatus_Succeeded(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
caption := formatResourceCaption("canary/myapp", true, false)
|
||||
view := CanaryStatusView{Phase: "Succeeded"}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
displayCanaryStatus(&buf, caption, view)
|
||||
})
|
||||
out := buf.String()
|
||||
assert.Contains(t, out, "Succeeded")
|
||||
}
|
||||
|
||||
func TestDisplayCanaryStatus_EmptyPhaseAndAge(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
caption := formatResourceCaption("canary/myapp", false, false)
|
||||
view := CanaryStatusView{}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
displayCanaryStatus(&buf, caption, view)
|
||||
})
|
||||
// Should still produce output (at least the caption + newline)
|
||||
assert.NotEmpty(t, buf.String())
|
||||
}
|
||||
|
||||
// --- writeOut ---
|
||||
|
||||
func TestWriteOut(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
writeOut(&buf, "hello world")
|
||||
assert.Equal(t, "hello world", buf.String())
|
||||
}
|
||||
|
||||
func TestWriteOut_Empty(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
writeOut(&buf, "")
|
||||
assert.Equal(t, "", buf.String())
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/werf/kubedog/pkg/display"
|
||||
"github.com/werf/kubedog/pkg/informer"
|
||||
"github.com/werf/kubedog/pkg/tracker"
|
||||
"github.com/werf/kubedog/pkg/tracker/canary"
|
||||
|
|
@ -234,6 +235,7 @@ func (t *Tracker) TrackResources(ctx context.Context, resources []*resource.Reso
|
|||
ParentContext: ctx,
|
||||
Timeout: t.trackOptions.Timeout,
|
||||
LogsFromTime: time.Now().Add(-t.trackOptions.LogsSince),
|
||||
IgnoreLogs: !t.trackOptions.Logs,
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
|
@ -312,13 +314,35 @@ func (t *Tracker) runDeploymentTracker(ctx context.Context, tr *deployment.Track
|
|||
}
|
||||
|
||||
func (t *Tracker) waitDeploymentTracker(ctx context.Context, tr *deployment.Tracker, trackErrCh <-chan error, doneCh <-chan struct{}) error {
|
||||
var prevStatus deployment.DeploymentStatus
|
||||
out := statusOutput()
|
||||
resourceName := fmt.Sprintf("deploy/%s", tr.ResourceName)
|
||||
|
||||
for {
|
||||
select {
|
||||
case status := <-tr.Added:
|
||||
displayDeploymentStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
|
||||
prevStatus = status
|
||||
case <-tr.Ready:
|
||||
t.logger.Debugf("Deployment %s/%s is ready", tr.Namespace, tr.ResourceName)
|
||||
displayDeploymentStatusProgress(out, formatResourceCaption(resourceName, true, false), prevStatus, &prevStatus)
|
||||
t.logger.Infof("Deployment %s/%s is ready", tr.Namespace, tr.ResourceName)
|
||||
return nil
|
||||
case status := <-tr.Failed:
|
||||
displayDeploymentStatusProgress(out, formatResourceCaption(resourceName, false, true), status, &prevStatus)
|
||||
return fmt.Errorf("deployment %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason)
|
||||
case status := <-tr.Status:
|
||||
if status.StatusGeneration > prevStatus.StatusGeneration {
|
||||
displayDeploymentStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
|
||||
prevStatus = status
|
||||
}
|
||||
case msg := <-tr.EventMsg:
|
||||
t.logger.Infof("deploy/%s: %s", tr.ResourceName, msg)
|
||||
case chunk := <-tr.PodLogChunk:
|
||||
t.logPodLogChunk(chunk.PodName, chunk.LogLines)
|
||||
case report := <-tr.PodError:
|
||||
t.logger.Warnf("deploy/%s pod %s: %s: %s", tr.ResourceName, report.ReplicaSetPodError.PodName, report.ReplicaSetPodError.ContainerName, report.ReplicaSetPodError.Message)
|
||||
case <-tr.AddedReplicaSet:
|
||||
case <-tr.AddedPod:
|
||||
case err := <-trackErrCh:
|
||||
return err
|
||||
case <-doneCh:
|
||||
|
|
@ -338,13 +362,34 @@ func (t *Tracker) runStatefulSetTracker(ctx context.Context, tr *statefulset.Tra
|
|||
}
|
||||
|
||||
func (t *Tracker) waitStatefulSetTracker(ctx context.Context, tr *statefulset.Tracker, trackErrCh <-chan error, doneCh <-chan struct{}) error {
|
||||
var prevStatus statefulset.StatefulSetStatus
|
||||
out := statusOutput()
|
||||
resourceName := fmt.Sprintf("sts/%s", tr.ResourceName)
|
||||
|
||||
for {
|
||||
select {
|
||||
case status := <-tr.Added:
|
||||
displayStatefulSetStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
|
||||
prevStatus = status
|
||||
case <-tr.Ready:
|
||||
t.logger.Debugf("StatefulSet %s/%s is ready", tr.Namespace, tr.ResourceName)
|
||||
displayStatefulSetStatusProgress(out, formatResourceCaption(resourceName, true, false), prevStatus, &prevStatus)
|
||||
t.logger.Infof("StatefulSet %s/%s is ready", tr.Namespace, tr.ResourceName)
|
||||
return nil
|
||||
case status := <-tr.Failed:
|
||||
displayStatefulSetStatusProgress(out, formatResourceCaption(resourceName, false, true), status, &prevStatus)
|
||||
return fmt.Errorf("statefulset %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason)
|
||||
case status := <-tr.Status:
|
||||
if status.StatusGeneration > prevStatus.StatusGeneration {
|
||||
displayStatefulSetStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
|
||||
prevStatus = status
|
||||
}
|
||||
case msg := <-tr.EventMsg:
|
||||
t.logger.Infof("sts/%s: %s", tr.ResourceName, msg)
|
||||
case chunk := <-tr.PodLogChunk:
|
||||
t.logPodLogChunk(chunk.PodName, chunk.LogLines)
|
||||
case report := <-tr.PodError:
|
||||
t.logger.Warnf("sts/%s pod %s: %s: %s", tr.ResourceName, report.ReplicaSetPodError.PodName, report.ReplicaSetPodError.ContainerName, report.ReplicaSetPodError.Message)
|
||||
case <-tr.AddedPod:
|
||||
case err := <-trackErrCh:
|
||||
return err
|
||||
case <-doneCh:
|
||||
|
|
@ -364,13 +409,34 @@ func (t *Tracker) runDaemonSetTracker(ctx context.Context, tr *daemonset.Tracker
|
|||
}
|
||||
|
||||
func (t *Tracker) waitDaemonSetTracker(ctx context.Context, tr *daemonset.Tracker, trackErrCh <-chan error, doneCh <-chan struct{}) error {
|
||||
var prevStatus daemonset.DaemonSetStatus
|
||||
out := statusOutput()
|
||||
resourceName := fmt.Sprintf("ds/%s", tr.ResourceName)
|
||||
|
||||
for {
|
||||
select {
|
||||
case status := <-tr.Added:
|
||||
displayDaemonSetStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
|
||||
prevStatus = status
|
||||
case <-tr.Ready:
|
||||
t.logger.Debugf("DaemonSet %s/%s is ready", tr.Namespace, tr.ResourceName)
|
||||
displayDaemonSetStatusProgress(out, formatResourceCaption(resourceName, true, false), prevStatus, &prevStatus)
|
||||
t.logger.Infof("DaemonSet %s/%s is ready", tr.Namespace, tr.ResourceName)
|
||||
return nil
|
||||
case status := <-tr.Failed:
|
||||
displayDaemonSetStatusProgress(out, formatResourceCaption(resourceName, false, true), status, &prevStatus)
|
||||
return fmt.Errorf("daemonset %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason)
|
||||
case status := <-tr.Status:
|
||||
if status.StatusGeneration > prevStatus.StatusGeneration {
|
||||
displayDaemonSetStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
|
||||
prevStatus = status
|
||||
}
|
||||
case msg := <-tr.EventMsg:
|
||||
t.logger.Infof("ds/%s: %s", tr.ResourceName, msg)
|
||||
case chunk := <-tr.PodLogChunk:
|
||||
t.logPodLogChunk(chunk.PodName, chunk.LogLines)
|
||||
case report := <-tr.PodError:
|
||||
t.logger.Warnf("ds/%s pod %s: %s: %s", tr.ResourceName, report.PodError.PodName, report.PodError.ContainerName, report.PodError.Message)
|
||||
case <-tr.AddedPod:
|
||||
case err := <-trackErrCh:
|
||||
return err
|
||||
case <-doneCh:
|
||||
|
|
@ -390,13 +456,34 @@ func (t *Tracker) runJobTracker(ctx context.Context, tr *job.Tracker, errCh chan
|
|||
}
|
||||
|
||||
func (t *Tracker) waitJobTracker(ctx context.Context, tr *job.Tracker, trackErrCh <-chan error, doneCh <-chan struct{}) error {
|
||||
var prevStatus job.JobStatus
|
||||
out := statusOutput()
|
||||
resourceName := fmt.Sprintf("job/%s", tr.ResourceName)
|
||||
|
||||
for {
|
||||
select {
|
||||
case status := <-tr.Added:
|
||||
displayJobStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
|
||||
prevStatus = status
|
||||
case <-tr.Succeeded:
|
||||
t.logger.Debugf("Job %s/%s succeeded", tr.Namespace, tr.ResourceName)
|
||||
displayJobStatusProgress(out, formatResourceCaption(resourceName, true, false), prevStatus, &prevStatus)
|
||||
t.logger.Infof("Job %s/%s succeeded", tr.Namespace, tr.ResourceName)
|
||||
return nil
|
||||
case status := <-tr.Failed:
|
||||
displayJobStatusProgress(out, formatResourceCaption(resourceName, false, true), status, &prevStatus)
|
||||
return fmt.Errorf("job %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason)
|
||||
case status := <-tr.Status:
|
||||
if status.StatusGeneration > prevStatus.StatusGeneration {
|
||||
displayJobStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
|
||||
prevStatus = status
|
||||
}
|
||||
case msg := <-tr.EventMsg:
|
||||
t.logger.Infof("job/%s: %s", tr.ResourceName, msg)
|
||||
case chunk := <-tr.PodLogChunk:
|
||||
t.logPodLogChunk(chunk.PodName, chunk.LogLines)
|
||||
case report := <-tr.PodError:
|
||||
t.logger.Warnf("job/%s pod %s: %s: %s", tr.ResourceName, report.PodError.PodName, report.PodError.ContainerName, report.PodError.Message)
|
||||
case <-tr.AddedPod:
|
||||
case err := <-trackErrCh:
|
||||
return err
|
||||
case <-doneCh:
|
||||
|
|
@ -416,13 +503,44 @@ func (t *Tracker) runCanaryTracker(ctx context.Context, tr *canary.Tracker, errC
|
|||
}
|
||||
|
||||
func (t *Tracker) waitCanaryTracker(ctx context.Context, tr *canary.Tracker, trackErrCh <-chan error, doneCh <-chan struct{}) error {
|
||||
out := statusOutput()
|
||||
resourceName := fmt.Sprintf("canary/%s", tr.ResourceName)
|
||||
var lastView CanaryStatusView
|
||||
|
||||
for {
|
||||
select {
|
||||
case status := <-tr.Added:
|
||||
view := CanaryStatusView{
|
||||
Phase: string(status.CanaryStatus.Phase),
|
||||
IsFailed: status.IsFailed,
|
||||
}
|
||||
displayCanaryStatus(out, formatResourceCaption(resourceName, false, false), view)
|
||||
lastView = view
|
||||
case <-tr.Succeeded:
|
||||
t.logger.Debugf("Canary %s/%s succeeded", tr.Namespace, tr.ResourceName)
|
||||
displayCanaryStatus(out, formatResourceCaption(resourceName, true, false), CanaryStatusView{Phase: lastView.Phase})
|
||||
t.logger.Infof("Canary %s/%s succeeded", tr.Namespace, tr.ResourceName)
|
||||
return nil
|
||||
case status := <-tr.Failed:
|
||||
displayCanaryStatus(out, formatResourceCaption(resourceName, false, true), CanaryStatusView{
|
||||
Phase: status.FailedReason,
|
||||
IsFailed: true,
|
||||
})
|
||||
return fmt.Errorf("canary %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason)
|
||||
case status := <-tr.Status:
|
||||
view := CanaryStatusView{
|
||||
Phase: func() string {
|
||||
if status.StatusIndicator != nil {
|
||||
return status.StatusIndicator.Value
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
Age: status.Age,
|
||||
IsFailed: status.IsFailed,
|
||||
}
|
||||
displayCanaryStatus(out, formatResourceCaption(resourceName, false, false), view)
|
||||
lastView = view
|
||||
case msg := <-tr.EventMsg:
|
||||
t.logger.Infof("canary/%s: %s", tr.ResourceName, msg)
|
||||
case err := <-trackErrCh:
|
||||
return err
|
||||
case <-doneCh:
|
||||
|
|
@ -433,6 +551,12 @@ func (t *Tracker) waitCanaryTracker(ctx context.Context, tr *canary.Tracker, tra
|
|||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) logPodLogChunk(podName string, logLines []display.LogLine) {
|
||||
for _, line := range logLines {
|
||||
t.logger.Infof("po/%s [%s] %s", podName, line.Timestamp, line.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) buildTargets(resources []*resource.Resource) []trackTarget {
|
||||
var targets []trackTarget
|
||||
for _, res := range resources {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -9,8 +12,10 @@ import (
|
|||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
helmchart "helm.sh/helm/v3/pkg/chart"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/filesystem"
|
||||
"github.com/helmfile/helmfile/pkg/runtime"
|
||||
"github.com/helmfile/helmfile/pkg/yaml"
|
||||
)
|
||||
|
||||
|
|
@ -645,3 +650,462 @@ dependencies:
|
|||
t.Errorf("expected original dependency repository %q, got %q", wantRepository, chartMeta.Dependencies[0].Repository)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRewriteChartDependencies_RefreshesChartLock verifies that when Chart.yaml has
|
||||
// its file:// dependencies rewritten to absolute paths, an existing Chart.lock is
|
||||
// also updated in the temp copy: the digest is recomputed (otherwise `helm dep
|
||||
// build` would error with "lock out of sync") and matching file:// repository URLs
|
||||
// are mirrored over from the rewritten Chart.yaml (otherwise `helm dep build` would
|
||||
// resolve the lock's relative file:// path against the temp directory and fail).
|
||||
// Locked versions are preserved verbatim.
|
||||
func TestRewriteChartDependencies_RefreshesChartLock(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
chartYaml := `apiVersion: v2
|
||||
name: parent-chart
|
||||
version: 1.0.0
|
||||
dependencies:
|
||||
- name: local-dep
|
||||
repository: file://../local-dep
|
||||
version: 1.0.0
|
||||
- name: remote-dep
|
||||
repository: https://example.com/charts
|
||||
version: "*"
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "Chart.yaml"), []byte(chartYaml), 0644); err != nil {
|
||||
t.Fatalf("writing Chart.yaml: %v", err)
|
||||
}
|
||||
|
||||
const originalDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
||||
chartLock := `dependencies:
|
||||
- name: local-dep
|
||||
repository: file://../local-dep
|
||||
version: 1.0.0
|
||||
- name: remote-dep
|
||||
repository: https://example.com/charts
|
||||
version: 1.2.3
|
||||
digest: ` + originalDigest + `
|
||||
generated: "2024-01-01T00:00:00Z"
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "Chart.lock"), []byte(chartLock), 0644); err != nil {
|
||||
t.Fatalf("writing Chart.lock: %v", err)
|
||||
}
|
||||
|
||||
logger := zap.NewNop().Sugar()
|
||||
st := &HelmState{
|
||||
basePath: tempDir,
|
||||
fs: filesystem.DefaultFileSystem(),
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
|
||||
if err != nil {
|
||||
t.Fatalf("rewriteChartDependencies failed: %v", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
if rewrittenPath == tempDir {
|
||||
t.Fatalf("expected a temp copy to be created, got original path %q", rewrittenPath)
|
||||
}
|
||||
|
||||
lockData, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.lock"))
|
||||
if err != nil {
|
||||
t.Fatalf("reading rewritten Chart.lock: %v", err)
|
||||
}
|
||||
|
||||
var lock struct {
|
||||
Dependencies []struct {
|
||||
Name string `yaml:"name"`
|
||||
Repository string `yaml:"repository"`
|
||||
Version string `yaml:"version"`
|
||||
} `yaml:"dependencies"`
|
||||
Digest string `yaml:"digest"`
|
||||
Generated string `yaml:"generated"`
|
||||
}
|
||||
if err := yaml.Unmarshal(lockData, &lock); err != nil {
|
||||
t.Fatalf("parsing rewritten Chart.lock: %v", err)
|
||||
}
|
||||
|
||||
if lock.Digest == originalDigest {
|
||||
t.Errorf("expected digest to be recomputed; still %q", lock.Digest)
|
||||
}
|
||||
if !strings.HasPrefix(lock.Digest, "sha256:") {
|
||||
t.Errorf("expected sha256 digest, got %q", lock.Digest)
|
||||
}
|
||||
|
||||
if len(lock.Dependencies) != 2 {
|
||||
t.Fatalf("expected 2 lock dependencies, got %d", len(lock.Dependencies))
|
||||
}
|
||||
|
||||
// The local file:// dependency's repository must be mirrored to the absolute
|
||||
// path so `helm dep build` can resolve it from the temp chart directory.
|
||||
localDep := lock.Dependencies[0]
|
||||
if localDep.Name != "local-dep" {
|
||||
t.Fatalf("expected first lock dep name 'local-dep', got %q", localDep.Name)
|
||||
}
|
||||
if !filepath.IsAbs(strings.TrimPrefix(localDep.Repository, "file://")) {
|
||||
t.Errorf("expected local-dep repository to be an absolute file:// path, got %q", localDep.Repository)
|
||||
}
|
||||
if localDep.Version != "1.0.0" {
|
||||
t.Errorf("expected local-dep version preserved as 1.0.0, got %q", localDep.Version)
|
||||
}
|
||||
|
||||
// Remote (non-file://) deps must be untouched.
|
||||
remoteDep := lock.Dependencies[1]
|
||||
if remoteDep.Repository != "https://example.com/charts" {
|
||||
t.Errorf("expected remote dep repository unchanged, got %q", remoteDep.Repository)
|
||||
}
|
||||
if remoteDep.Version != "1.2.3" {
|
||||
t.Errorf("expected remote dep version preserved as 1.2.3, got %q", remoteDep.Version)
|
||||
}
|
||||
|
||||
// The original Chart.lock on disk must be untouched.
|
||||
originalLock, err := os.ReadFile(filepath.Join(tempDir, "Chart.lock"))
|
||||
if err != nil {
|
||||
t.Fatalf("reading original Chart.lock: %v", err)
|
||||
}
|
||||
if string(originalLock) != chartLock {
|
||||
t.Errorf("original Chart.lock was modified; expected unchanged content")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRewriteChartDependencies_RefreshesChartLockWithExtraFields verifies that
|
||||
// Chart.lock digest recomputation includes all dependency fields (alias, condition,
|
||||
// tags, import-values, enabled) — not just name/repository/version — so the digest
|
||||
// stays compatible with Helm's resolver.HashReq for charts using those fields.
|
||||
// It proves field coverage by running two chart variants under a shared root
|
||||
// (so file:// paths resolve to the same absolute location) and asserting the
|
||||
// digests differ only due to extra fields.
|
||||
func TestRewriteChartDependencies_RefreshesChartLockWithExtraFields(t *testing.T) {
|
||||
// Use a shared root so both chart variants resolve file://../local-dep to the
|
||||
// same absolute path — isolating the digest difference to field content only.
|
||||
sharedRoot := t.TempDir()
|
||||
chartDir := filepath.Join(sharedRoot, "parent")
|
||||
if err := os.MkdirAll(chartDir, 0755); err != nil {
|
||||
t.Fatalf("creating chart dir: %v", err)
|
||||
}
|
||||
|
||||
// Run rewriteChartDependencies for a given Chart.yaml and return the recomputed digest.
|
||||
getDigest := func(t *testing.T, chartYaml, chartLock string) string {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(filepath.Join(chartDir, "Chart.yaml"), []byte(chartYaml), 0644); err != nil {
|
||||
t.Fatalf("writing Chart.yaml: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(chartDir, "Chart.lock"), []byte(chartLock), 0644); err != nil {
|
||||
t.Fatalf("writing Chart.lock: %v", err)
|
||||
}
|
||||
logger := zap.NewNop().Sugar()
|
||||
st := &HelmState{
|
||||
basePath: chartDir,
|
||||
fs: filesystem.DefaultFileSystem(),
|
||||
logger: logger,
|
||||
}
|
||||
rewrittenPath, cleanup, err := st.rewriteChartDependencies(chartDir)
|
||||
if err != nil {
|
||||
t.Fatalf("rewriteChartDependencies failed: %v", err)
|
||||
}
|
||||
defer cleanup()
|
||||
lockData, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.lock"))
|
||||
if err != nil {
|
||||
t.Fatalf("reading rewritten Chart.lock: %v", err)
|
||||
}
|
||||
var lock struct {
|
||||
Digest string `yaml:"digest"`
|
||||
}
|
||||
if err := yaml.Unmarshal(lockData, &lock); err != nil {
|
||||
t.Fatalf("parsing rewritten Chart.lock: %v", err)
|
||||
}
|
||||
return lock.Digest
|
||||
}
|
||||
|
||||
const originalDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
||||
baseLock := `dependencies:
|
||||
- name: local-dep
|
||||
repository: file://../local-dep
|
||||
version: 1.0.0
|
||||
alias: my-local
|
||||
- name: local-dep
|
||||
repository: file://../local-dep-alt
|
||||
version: 2.0.0
|
||||
alias: my-local-alt
|
||||
digest: ` + originalDigest + `
|
||||
generated: "2024-01-01T00:00:00Z"
|
||||
`
|
||||
|
||||
// Chart.yaml with extra fields (alias, condition, tags, import-values).
|
||||
chartYamlWithExtras := `apiVersion: v2
|
||||
name: parent-chart
|
||||
version: 1.0.0
|
||||
dependencies:
|
||||
- name: local-dep
|
||||
repository: file://../local-dep
|
||||
version: 1.0.0
|
||||
alias: my-local
|
||||
condition: local-dep.enabled
|
||||
tags:
|
||||
- frontend
|
||||
- optional
|
||||
import-values:
|
||||
- child: config
|
||||
parent: global.config
|
||||
- name: local-dep
|
||||
repository: file://../local-dep-alt
|
||||
version: 2.0.0
|
||||
alias: my-local-alt
|
||||
`
|
||||
|
||||
// Same chart without condition/tags/import-values — only alias remains.
|
||||
chartYamlWithoutExtras := `apiVersion: v2
|
||||
name: parent-chart
|
||||
version: 1.0.0
|
||||
dependencies:
|
||||
- name: local-dep
|
||||
repository: file://../local-dep
|
||||
version: 1.0.0
|
||||
alias: my-local
|
||||
- name: local-dep
|
||||
repository: file://../local-dep-alt
|
||||
version: 2.0.0
|
||||
alias: my-local-alt
|
||||
`
|
||||
|
||||
digestWith := getDigest(t, chartYamlWithExtras, baseLock)
|
||||
digestWithout := getDigest(t, chartYamlWithoutExtras, baseLock)
|
||||
|
||||
if !strings.HasPrefix(digestWith, "sha256:") {
|
||||
t.Errorf("expected sha256 digest, got %q", digestWith)
|
||||
}
|
||||
if digestWith == originalDigest {
|
||||
t.Errorf("expected digest to be recomputed; still %q", digestWith)
|
||||
}
|
||||
if digestWith == digestWithout {
|
||||
t.Errorf("digest should differ when extra fields (condition, tags, import-values) are present, but both are %q", digestWith)
|
||||
}
|
||||
|
||||
// Also verify alias-based matching: both deps have name "local-dep" but
|
||||
// different aliases; both should get their file:// paths rewritten.
|
||||
if err := os.WriteFile(filepath.Join(chartDir, "Chart.yaml"), []byte(chartYamlWithExtras), 0644); err != nil {
|
||||
t.Fatalf("writing Chart.yaml: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(chartDir, "Chart.lock"), []byte(baseLock), 0644); err != nil {
|
||||
t.Fatalf("writing Chart.lock: %v", err)
|
||||
}
|
||||
logger := zap.NewNop().Sugar()
|
||||
st := &HelmState{
|
||||
basePath: chartDir,
|
||||
fs: filesystem.DefaultFileSystem(),
|
||||
logger: logger,
|
||||
}
|
||||
rewrittenPath, cleanup, err := st.rewriteChartDependencies(chartDir)
|
||||
if err != nil {
|
||||
t.Fatalf("rewriteChartDependencies failed: %v", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
lockData, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.lock"))
|
||||
if err != nil {
|
||||
t.Fatalf("reading rewritten Chart.lock: %v", err)
|
||||
}
|
||||
var lock struct {
|
||||
Dependencies []struct {
|
||||
Name string `yaml:"name"`
|
||||
Repository string `yaml:"repository"`
|
||||
Version string `yaml:"version"`
|
||||
Alias string `yaml:"alias"`
|
||||
} `yaml:"dependencies"`
|
||||
}
|
||||
if err := yaml.Unmarshal(lockData, &lock); err != nil {
|
||||
t.Fatalf("parsing rewritten Chart.lock: %v", err)
|
||||
}
|
||||
if len(lock.Dependencies) != 2 {
|
||||
t.Fatalf("expected 2 lock dependencies, got %d", len(lock.Dependencies))
|
||||
}
|
||||
|
||||
dep1 := lock.Dependencies[0]
|
||||
if dep1.Alias != "my-local" {
|
||||
t.Errorf("expected first lock dep alias 'my-local', got %q", dep1.Alias)
|
||||
}
|
||||
if !filepath.IsAbs(strings.TrimPrefix(dep1.Repository, "file://")) {
|
||||
t.Errorf("expected first dep repository to be an absolute file:// path, got %q", dep1.Repository)
|
||||
}
|
||||
|
||||
dep2 := lock.Dependencies[1]
|
||||
if dep2.Alias != "my-local-alt" {
|
||||
t.Errorf("expected second lock dep alias 'my-local-alt', got %q", dep2.Alias)
|
||||
}
|
||||
if !filepath.IsAbs(strings.TrimPrefix(dep2.Repository, "file://")) {
|
||||
t.Errorf("expected second dep repository to be an absolute file:// path, got %q", dep2.Repository)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRewriteChartDependencies_GoYamlV2ImportValues verifies that Chart.lock
|
||||
// refresh works under go-yaml v2 (HELMFILE_GO_YAML_V3=false), where nested
|
||||
// maps in import-values decode as map[interface{}]interface{} which json.Marshal
|
||||
// cannot handle without normalization.
|
||||
func TestRewriteChartDependencies_GoYamlV2ImportValues(t *testing.T) {
|
||||
prev := runtime.GoYamlV3
|
||||
runtime.GoYamlV3 = false
|
||||
t.Cleanup(func() {
|
||||
runtime.GoYamlV3 = prev
|
||||
})
|
||||
|
||||
tempDir := t.TempDir()
|
||||
|
||||
chartYaml := `apiVersion: v2
|
||||
name: parent-chart
|
||||
version: 1.0.0
|
||||
dependencies:
|
||||
- name: local-dep
|
||||
repository: file://../local-dep
|
||||
version: 1.0.0
|
||||
import-values:
|
||||
- child: config
|
||||
parent: global.config
|
||||
`
|
||||
chartLock := `dependencies:
|
||||
- name: local-dep
|
||||
repository: file://../local-dep
|
||||
version: 1.0.0
|
||||
import-values:
|
||||
- child: config
|
||||
parent: global.config
|
||||
digest: sha256:0000000000000000000000000000000000000000000000000000000000000000
|
||||
generated: "2024-01-01T00:00:00Z"
|
||||
`
|
||||
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "Chart.yaml"), []byte(chartYaml), 0644); err != nil {
|
||||
t.Fatalf("writing Chart.yaml: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "Chart.lock"), []byte(chartLock), 0644); err != nil {
|
||||
t.Fatalf("writing Chart.lock: %v", err)
|
||||
}
|
||||
|
||||
logger := zap.NewNop().Sugar()
|
||||
st := &HelmState{
|
||||
basePath: tempDir,
|
||||
fs: filesystem.DefaultFileSystem(),
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
|
||||
if err != nil {
|
||||
t.Fatalf("rewriteChartDependencies failed: %v", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
lockData, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.lock"))
|
||||
if err != nil {
|
||||
t.Fatalf("reading rewritten Chart.lock: %v", err)
|
||||
}
|
||||
|
||||
var lock struct {
|
||||
Digest string `yaml:"digest"`
|
||||
}
|
||||
if err := yaml.Unmarshal(lockData, &lock); err != nil {
|
||||
t.Fatalf("parsing rewritten Chart.lock: %v", err)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(lock.Digest, "sha256:") {
|
||||
t.Errorf("expected sha256 digest, got %q", lock.Digest)
|
||||
}
|
||||
const originalDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
||||
if lock.Digest == originalDigest {
|
||||
t.Errorf("expected digest to be recomputed; still %q", lock.Digest)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRewriteChartDependencies_DigestMatchesHelmHashReq verifies the recomputed
|
||||
// digest matches what Helm's resolver.HashReq would produce for a known input.
|
||||
// This guards against producing a digest that is "different" but still rejected
|
||||
// by `helm dependency build`.
|
||||
func TestRewriteChartDependencies_DigestMatchesHelmHashReq(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
chartYaml := `apiVersion: v2
|
||||
name: test-chart
|
||||
version: 1.0.0
|
||||
dependencies:
|
||||
- name: dep-a
|
||||
repository: file://../dep-a
|
||||
version: 2.0.0
|
||||
condition: dep-a.enabled
|
||||
tags:
|
||||
- backend
|
||||
`
|
||||
chartLock := `dependencies:
|
||||
- name: dep-a
|
||||
repository: file://../dep-a
|
||||
version: 2.0.0
|
||||
digest: sha256:0000000000000000000000000000000000000000000000000000000000000000
|
||||
generated: "2024-01-01T00:00:00Z"
|
||||
`
|
||||
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "Chart.yaml"), []byte(chartYaml), 0644); err != nil {
|
||||
t.Fatalf("writing Chart.yaml: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "Chart.lock"), []byte(chartLock), 0644); err != nil {
|
||||
t.Fatalf("writing Chart.lock: %v", err)
|
||||
}
|
||||
|
||||
logger := zap.NewNop().Sugar()
|
||||
st := &HelmState{
|
||||
basePath: tempDir,
|
||||
fs: filesystem.DefaultFileSystem(),
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
|
||||
if err != nil {
|
||||
t.Fatalf("rewriteChartDependencies failed: %v", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
lockData, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.lock"))
|
||||
if err != nil {
|
||||
t.Fatalf("reading rewritten Chart.lock: %v", err)
|
||||
}
|
||||
|
||||
var lock struct {
|
||||
Dependencies []*helmchart.Dependency `yaml:"dependencies"`
|
||||
Digest string `yaml:"digest"`
|
||||
}
|
||||
if err := yaml.Unmarshal(lockData, &lock); err != nil {
|
||||
t.Fatalf("parsing rewritten Chart.lock: %v", err)
|
||||
}
|
||||
|
||||
// Compute the expected digest independently using Helm's HashReq algorithm:
|
||||
// sha256(json.Marshal([2][]*chart.Dependency{req, lock}))
|
||||
// where req = rewritten Chart.yaml deps, lock = rewritten Chart.lock deps.
|
||||
absDepA, err := filepath.Abs(filepath.Join(tempDir, "../dep-a"))
|
||||
if err != nil {
|
||||
t.Fatalf("resolving absolute path: %v", err)
|
||||
}
|
||||
|
||||
req := []*helmchart.Dependency{
|
||||
{
|
||||
Name: "dep-a",
|
||||
Repository: "file://" + absDepA,
|
||||
Version: "2.0.0",
|
||||
Condition: "dep-a.enabled",
|
||||
Tags: []string{"backend"},
|
||||
},
|
||||
}
|
||||
lockDeps := []*helmchart.Dependency{
|
||||
{
|
||||
Name: "dep-a",
|
||||
Repository: "file://" + absDepA,
|
||||
Version: "2.0.0",
|
||||
},
|
||||
}
|
||||
|
||||
payload, err := json.Marshal([2][]*helmchart.Dependency{req, lockDeps})
|
||||
if err != nil {
|
||||
t.Fatalf("marshaling expected digest payload: %v", err)
|
||||
}
|
||||
sum := sha256.Sum256(payload)
|
||||
expectedDigest := "sha256:" + hex.EncodeToString(sum[:])
|
||||
|
||||
if lock.Digest != expectedDigest {
|
||||
t.Errorf("digest mismatch with Helm's HashReq algorithm:\n got: %s\n want: %s", lock.Digest, expectedDigest)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -448,7 +448,8 @@ func (st *HelmState) PrepareChartify(helm helmexec.Interface, release *ReleaseSp
|
|||
|
||||
for _, d := range release.Dependencies {
|
||||
chart := d.Chart
|
||||
if st.fs.DirectoryExistsAt(chart) {
|
||||
normalizedChart := normalizeChart(st.basePath, chart)
|
||||
if st.fs.DirectoryExistsAt(normalizedChart) {
|
||||
var err error
|
||||
|
||||
// Otherwise helm-dependency-up on the temporary chart generated by chartify ends up errors like:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/filesystem"
|
||||
)
|
||||
|
||||
// TestLocalDependencyChartPathNormalization tests that relative chart paths in
|
||||
// release dependencies (like "../chart") are normalized to absolute paths
|
||||
// relative to basePath before checking if the directory exists.
|
||||
// This is a regression test for issue #2596.
|
||||
//
|
||||
// Background: When helmfile.d/ contains multiple release files and one release
|
||||
// has a local chart dependency (chart: ../chart), the dependency chart path was
|
||||
// passed to DirectoryExistsAt without normalization, causing it to be resolved
|
||||
// relative to the CWD instead of basePath. This made helmfile fail to detect
|
||||
// the local chart and instead try to resolve it as a remote repo, resulting in
|
||||
// "failed reading adhoc dependencies: no helm list entry found for repository".
|
||||
func TestLocalDependencyChartPathNormalization(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
chartDir := filepath.Join(tempDir, "chart")
|
||||
require.NoError(t, os.MkdirAll(chartDir, 0755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(chartDir, "Chart.yaml"), []byte(`
|
||||
apiVersion: v2
|
||||
name: test-chart
|
||||
version: 0.1.0
|
||||
`), 0644))
|
||||
|
||||
helmfileDir := filepath.Join(tempDir, "helmfile.d")
|
||||
require.NoError(t, os.MkdirAll(helmfileDir, 0755))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
chartPath string
|
||||
basePath string
|
||||
expectLocal bool
|
||||
}{
|
||||
{
|
||||
name: "relative path ../chart normalized from helmfile.d",
|
||||
chartPath: "../chart",
|
||||
basePath: helmfileDir,
|
||||
expectLocal: true,
|
||||
},
|
||||
{
|
||||
name: "absolute path works unchanged",
|
||||
chartPath: chartDir,
|
||||
basePath: helmfileDir,
|
||||
expectLocal: true,
|
||||
},
|
||||
{
|
||||
name: "non-existent relative path not detected as local",
|
||||
chartPath: "../nonexistent",
|
||||
basePath: helmfileDir,
|
||||
expectLocal: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
normalizedChart := normalizeChart(tt.basePath, tt.chartPath)
|
||||
fs := filesystem.DefaultFileSystem()
|
||||
isLocal := fs.DirectoryExistsAt(normalizedChart)
|
||||
assert.Equal(t, tt.expectLocal, isLocal,
|
||||
"normalizeChart(%q, %q) = %q, DirectoryExistsAt = %v, want %v",
|
||||
tt.basePath, tt.chartPath, normalizedChart, isLocal, tt.expectLocal)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDependencyChartPathResolutionWithPrepareChartify verifies that the dependency
|
||||
// chart path is normalized using basePath before calling DirectoryExistsAt,
|
||||
// which is the core of the fix for issue #2596.
|
||||
func TestDependencyChartPathResolutionWithPrepareChartify(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
chartDir := filepath.Join(tempDir, "chart")
|
||||
require.NoError(t, os.MkdirAll(chartDir, 0755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(chartDir, "Chart.yaml"), []byte(`
|
||||
apiVersion: v2
|
||||
name: test-chart
|
||||
version: 0.1.0
|
||||
`), 0644))
|
||||
|
||||
helmfileDir := filepath.Join(tempDir, "helmfile.d")
|
||||
require.NoError(t, os.MkdirAll(helmfileDir, 0755))
|
||||
|
||||
fs := filesystem.DefaultFileSystem()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
depChartPath string
|
||||
basePath string
|
||||
expectDetected bool
|
||||
}{
|
||||
{
|
||||
name: "relative ../chart from helmfile.d detected as local",
|
||||
depChartPath: "../chart",
|
||||
basePath: helmfileDir,
|
||||
expectDetected: true,
|
||||
},
|
||||
{
|
||||
name: "absolute path detected as local",
|
||||
depChartPath: chartDir,
|
||||
basePath: helmfileDir,
|
||||
expectDetected: true,
|
||||
},
|
||||
{
|
||||
name: "non-existent relative path not detected",
|
||||
depChartPath: "../nonexistent",
|
||||
basePath: helmfileDir,
|
||||
expectDetected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
normalizedChart := normalizeChart(tt.basePath, tt.depChartPath)
|
||||
isLocal := fs.DirectoryExistsAt(normalizedChart)
|
||||
assert.Equal(t, tt.expectDetected, isLocal,
|
||||
"normalizeChart(%q, %q) = %q, DirectoryExistsAt = %v, want %v",
|
||||
tt.basePath, tt.depChartPath, normalizedChart, isLocal, tt.expectDetected)
|
||||
|
||||
if tt.expectDetected && !filepath.IsAbs(tt.depChartPath) {
|
||||
absChart, err := filepath.Abs(filepath.Join(tt.basePath, tt.depChartPath))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, absChart, normalizedChart,
|
||||
"normalized path should match expected absolute path")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -193,13 +193,18 @@ func (r ReleaseSpec) ExecuteTemplateExpressions(renderer *tmpl.FileRenderer) (*R
|
|||
}
|
||||
result.SetValuesTemplate[i].File = s.String()
|
||||
}
|
||||
for j, ts := range val.Values {
|
||||
for j, tv := range val.Values {
|
||||
// values
|
||||
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".set[%d].values[%d] = \"%s\": %v", r.Name, i, j, ts, err)
|
||||
switch ts := tv.(type) {
|
||||
case string:
|
||||
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".set[%d].values[%d] = \"%s\": %v", r.Name, i, j, ts, err)
|
||||
}
|
||||
result.SetValuesTemplate[i].Values[j] = s.String()
|
||||
default:
|
||||
result.SetValuesTemplate[i].Values[j] = ts
|
||||
}
|
||||
result.SetValuesTemplate[i].Values[j] = s.String()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import (
|
|||
"bytes"
|
||||
gocontext "context"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
|
@ -27,6 +29,7 @@ import (
|
|||
"github.com/helmfile/vals"
|
||||
"github.com/tatsushid/go-prettytable"
|
||||
"go.uber.org/zap"
|
||||
helmchart "helm.sh/helm/v3/pkg/chart"
|
||||
cliv3 "helm.sh/helm/v3/pkg/cli"
|
||||
cliv4 "helm.sh/helm/v4/pkg/cli"
|
||||
|
||||
|
|
@ -92,6 +95,11 @@ type ReleaseSetSpec struct {
|
|||
|
||||
Templates map[string]TemplateSpec `yaml:"templates"`
|
||||
|
||||
// DefaultInherit is a list of template names that all releases inherit by default.
|
||||
// Each release will automatically inherit these templates unless it already explicitly
|
||||
// inherits from the same template.
|
||||
DefaultInherit DefaultInherits `yaml:"defaultInherit,omitempty"`
|
||||
|
||||
Env environment.Environment `yaml:"-"`
|
||||
|
||||
// If set to "Error", return an error when a subhelmfile points to a
|
||||
|
|
@ -511,6 +519,45 @@ func (r *Inherits) UnmarshalYAML(unmarshal func(any) error) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type DefaultInherits []string
|
||||
|
||||
func (r *DefaultInherits) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
var list []string
|
||||
if err := unmarshal(&list); err == nil {
|
||||
*r = normalizeDefaultInherits(list)
|
||||
return nil
|
||||
}
|
||||
|
||||
var single string
|
||||
if err := unmarshal(&single); err != nil {
|
||||
return err
|
||||
}
|
||||
*r = normalizeDefaultInherits([]string{single})
|
||||
return nil
|
||||
}
|
||||
|
||||
// normalizeDefaultInherits trims names, drops empty entries, and returns nil for an empty result.
|
||||
func normalizeDefaultInherits(in []string) []string {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(in))
|
||||
for _, name := range in {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, name)
|
||||
}
|
||||
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// ChartPathOrName returns ChartPath if it is non-empty, and returns Chart otherwise.
|
||||
// This is useful to redirect helm commands like `helm template`, `helm dependency update`, `helm diff`, and `helm upgrade --install` to
|
||||
// our modified version of the chart, in case the user configured Helmfile to do modify the chart before being passed to Helm.
|
||||
|
|
@ -529,10 +576,10 @@ type Release struct {
|
|||
|
||||
// SetValue are the key values to set on a helm release
|
||||
type SetValue struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Value string `yaml:"value,omitempty"`
|
||||
File string `yaml:"file,omitempty"`
|
||||
Values []string `yaml:"values,omitempty"`
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Value string `yaml:"value,omitempty"`
|
||||
File string `yaml:"file,omitempty"`
|
||||
Values []any `yaml:"values,omitempty"`
|
||||
}
|
||||
|
||||
// AffectedReleases hold the list of released that where updated, deleted, or in error
|
||||
|
|
@ -1528,6 +1575,121 @@ func (st *HelmState) rewriteChartDependencies(chartPath string) (string, func(),
|
|||
|
||||
st.logger.Debugf("Rewrote Chart.yaml with absolute dependency paths at %s", tempChartYamlPath)
|
||||
|
||||
// Rewriting Chart.yaml invalidates Chart.lock's digest, since helm computes the
|
||||
// digest over the JSON-marshaled dependencies block. If the lock isn't refreshed,
|
||||
// downstream `helm dependency build` errors with "lock file is out of sync with
|
||||
// the dependencies file" and falls back to `dependency update`, which re-resolves
|
||||
// version constraints (e.g. `version: "*"`) against the chart repo and silently
|
||||
// pulls newer dependency versions. The version pins in the lock are still the
|
||||
// intended truth — only the rewritten file:// repository URL changed. Mirror the
|
||||
// rewrite into the lock and recompute the digest so `dep build` accepts it.
|
||||
tempChartLockPath := filepath.Join(tempDir, "Chart.lock")
|
||||
lockData, lockErr := st.fs.ReadFile(tempChartLockPath)
|
||||
if lockErr != nil && !os.IsNotExist(lockErr) {
|
||||
st.logger.Warnf("Failed to read Chart.lock at %s: %v", tempChartLockPath, lockErr)
|
||||
}
|
||||
if lockErr == nil {
|
||||
var lock struct {
|
||||
Dependencies []*helmchart.Dependency `yaml:"dependencies,omitempty"`
|
||||
Digest string `yaml:"digest,omitempty"`
|
||||
Generated string `yaml:"generated,omitempty"`
|
||||
}
|
||||
if err := yaml.Unmarshal(lockData, &lock); err != nil {
|
||||
st.logger.Warnf("Failed to parse Chart.lock at %s: %v", tempChartLockPath, err)
|
||||
} else {
|
||||
// Build the request slice (rewritten Chart.yaml dependencies) using helm's
|
||||
// own chart.Dependency type so the JSON used for hashing matches helm's
|
||||
// exactly. All supported fields must be mapped, not just name/repository/
|
||||
// version, because helm's digest algorithm hashes the full Dependency struct.
|
||||
req := make([]*helmchart.Dependency, 0, len(chartMeta.Dependencies))
|
||||
for _, d := range chartMeta.Dependencies {
|
||||
dep := &helmchart.Dependency{
|
||||
Name: d.Name,
|
||||
Repository: d.Repository,
|
||||
}
|
||||
if v, ok := d.Data["version"].(string); ok {
|
||||
dep.Version = v
|
||||
}
|
||||
if v, ok := d.Data["condition"].(string); ok {
|
||||
dep.Condition = v
|
||||
}
|
||||
if v, ok := d.Data["alias"].(string); ok {
|
||||
dep.Alias = v
|
||||
}
|
||||
if v, ok := d.Data["enabled"].(bool); ok {
|
||||
dep.Enabled = v
|
||||
}
|
||||
if v, ok := d.Data["tags"].([]interface{}); ok {
|
||||
tags := make([]string, 0, len(v))
|
||||
for _, t := range v {
|
||||
if s, ok := t.(string); ok {
|
||||
tags = append(tags, s)
|
||||
}
|
||||
}
|
||||
dep.Tags = tags
|
||||
}
|
||||
if v, ok := d.Data["import-values"].([]interface{}); ok {
|
||||
normalized, err := maputil.RecursivelyStringifyMapKey(v)
|
||||
if err != nil {
|
||||
st.logger.Warnf("Failed to normalize import-values for dependency %s: %v", d.Name, err)
|
||||
} else {
|
||||
dep.ImportValues = normalized.([]interface{})
|
||||
}
|
||||
}
|
||||
req = append(req, dep)
|
||||
}
|
||||
|
||||
// Mirror the rewritten file:// repository URLs onto matching lock entries.
|
||||
// Without this, `helm dependency build` would resolve the lock's relative
|
||||
// file:// paths against the (moved) chart directory and fail with
|
||||
// "directory ... not found". Versions in the lock are left untouched.
|
||||
// Match on Name + Alias to handle charts with duplicate dependency names
|
||||
// distinguished by alias.
|
||||
for _, ld := range lock.Dependencies {
|
||||
if !strings.HasPrefix(ld.Repository, "file://") {
|
||||
continue
|
||||
}
|
||||
for _, rd := range req {
|
||||
if rd.Name == ld.Name && rd.Alias == ld.Alias && strings.HasPrefix(rd.Repository, "file://") {
|
||||
ld.Repository = rd.Repository
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize lock.Dependencies ImportValues to avoid json.Marshal failures
|
||||
// when go-yaml v2 decodes nested maps as map[interface{}]interface{}.
|
||||
for _, ld := range lock.Dependencies {
|
||||
if ld.ImportValues != nil {
|
||||
normalized, err := maputil.RecursivelyStringifyMapKey(ld.ImportValues)
|
||||
if err != nil {
|
||||
st.logger.Warnf("Failed to normalize import-values in Chart.lock for dependency %s: %v", ld.Name, err)
|
||||
} else {
|
||||
ld.ImportValues = normalized.([]interface{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replicates helm's resolver.HashReq:
|
||||
// json.Marshal([2][]*chart.Dependency{req, lock}) → sha256 hex.
|
||||
// resolver.HashReq lives in helm.sh/helm/v3/internal/resolver, so we
|
||||
// inline the (small, stable) algorithm rather than importing it.
|
||||
if payload, err := json.Marshal([2][]*helmchart.Dependency{req, lock.Dependencies}); err != nil {
|
||||
st.logger.Warnf("Failed to marshal deps for Chart.lock digest at %s: %v", tempChartLockPath, err)
|
||||
} else {
|
||||
sum := sha256.Sum256(payload)
|
||||
lock.Digest = "sha256:" + hex.EncodeToString(sum[:])
|
||||
if updated, err := yaml.Marshal(&lock); err != nil {
|
||||
st.logger.Warnf("Failed to marshal Chart.lock at %s: %v", tempChartLockPath, err)
|
||||
} else if err := st.fs.WriteFile(tempChartLockPath, updated, 0644); err != nil {
|
||||
st.logger.Warnf("Failed to write Chart.lock at %s: %v", tempChartLockPath, err)
|
||||
} else {
|
||||
st.logger.Debugf("Refreshed Chart.lock digest at %s after Chart.yaml rewrite", tempChartLockPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
if removeErr := st.fs.RemoveAll(tempDir); removeErr != nil {
|
||||
st.logger.Warnf("Failed to remove temp chart directory %s: %v", tempDir, removeErr)
|
||||
|
|
@ -4548,7 +4710,7 @@ func (st *HelmState) setFlags(setValues []SetValue) ([]string, error) {
|
|||
} else if set.File != "" {
|
||||
flags = append(flags, "--set-file", fmt.Sprintf("%s=%s", escape(set.Name), st.storage().normalizeSetFilePath(set.File, runtime.GOOS)))
|
||||
} else if len(set.Values) > 0 {
|
||||
renderedValues, err := renderValsSecrets(st.valsRuntime, set.Values...)
|
||||
renderedValues, err := renderValsSecretsAny(st.valsRuntime, set.Values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -4576,7 +4738,7 @@ func (st *HelmState) setStringFlags(setValues []SetValue) ([]string, error) {
|
|||
}
|
||||
flags = append(flags, "--set-string", fmt.Sprintf("%s=%s", escape(set.Name), escape(renderedValue[0])))
|
||||
} else if len(set.Values) > 0 {
|
||||
renderedValues, err := renderValsSecrets(st.valsRuntime, set.Values...)
|
||||
renderedValues, err := renderValsSecretsAny(st.valsRuntime, set.Values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -4613,6 +4775,43 @@ func renderValsSecrets(e vals.Evaluator, input ...string) ([]string, error) {
|
|||
return output, nil
|
||||
}
|
||||
|
||||
// renderValsSecretsAny renders 'ref+.*' secrets in a slice of any-typed values.
|
||||
// Map values are serialized to JSON; string values are rendered via vals.
|
||||
func renderValsSecretsAny(e vals.Evaluator, input []any) ([]string, error) {
|
||||
output := make([]string, len(input))
|
||||
if len(input) == 0 {
|
||||
return output, nil
|
||||
}
|
||||
|
||||
strInputs := make([]string, 0, len(input))
|
||||
strIndexMap := make([]int, 0, len(input))
|
||||
for i, v := range input {
|
||||
switch tv := v.(type) {
|
||||
case string:
|
||||
strInputs = append(strInputs, tv)
|
||||
strIndexMap = append(strIndexMap, i)
|
||||
default:
|
||||
jsonBytes, err := json.Marshal(tv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal set value at index %d: %w", i, err)
|
||||
}
|
||||
output[i] = string(jsonBytes)
|
||||
}
|
||||
}
|
||||
|
||||
if len(strInputs) > 0 {
|
||||
rendered, err := renderValsSecrets(e, strInputs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for idx, renderedIdx := range strIndexMap {
|
||||
output[renderedIdx] = rendered[idx]
|
||||
}
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func hideChartCredentials(chartCredentials string) (string, error) {
|
||||
u, err := url.Parse(chartCredentials)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -85,7 +85,10 @@ func (st *HelmState) ExecuteTemplates() (*HelmState, error) {
|
|||
vals := st.Values()
|
||||
|
||||
for i, rt := range st.Releases {
|
||||
release, err := st.releaseWithInheritedTemplate(&rt, nil)
|
||||
rtWithDefaults := rt
|
||||
rtWithDefaults.Inherit = st.applyDefaultInherit(rt.Inherit)
|
||||
|
||||
release, err := st.releaseWithInheritedTemplate(&rtWithDefaults, nil)
|
||||
if err != nil {
|
||||
var cyclicInheritanceErr CyclicReleaseTemplateInheritanceError
|
||||
if errors.As(err, &cyclicInheritanceErr) {
|
||||
|
|
@ -224,3 +227,36 @@ func (st *HelmState) releaseWithInheritedTemplate(r *ReleaseSpec, inheritancePat
|
|||
|
||||
return &merged, nil
|
||||
}
|
||||
|
||||
// applyDefaultInherit prepends default inherit templates to the release's inherit list.
|
||||
// Templates that are already explicitly referenced by the release are not duplicated.
|
||||
func (st *HelmState) applyDefaultInherit(releaseInherit Inherits) Inherits {
|
||||
if len(st.DefaultInherit) == 0 {
|
||||
return releaseInherit
|
||||
}
|
||||
|
||||
// Build the deduplication set and filter out blank entries in one pass.
|
||||
existing := make(map[string]bool, len(releaseInherit))
|
||||
filtered := make(Inherits, 0, len(releaseInherit))
|
||||
for _, inh := range releaseInherit {
|
||||
if name := strings.TrimSpace(inh.Template); name != "" {
|
||||
existing[name] = true
|
||||
filtered = append(filtered, inh)
|
||||
}
|
||||
}
|
||||
|
||||
result := make(Inherits, 0, len(st.DefaultInherit)+len(filtered))
|
||||
for _, name := range st.DefaultInherit {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !existing[name] {
|
||||
result = append(result, Inherit{Template: name})
|
||||
existing[name] = true
|
||||
}
|
||||
}
|
||||
result = append(result, filtered...)
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,12 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/environment"
|
||||
"github.com/helmfile/helmfile/pkg/filesystem"
|
||||
"github.com/helmfile/helmfile/pkg/runtime"
|
||||
"github.com/helmfile/helmfile/pkg/yaml"
|
||||
)
|
||||
|
||||
func boolPtrToString(ptr *bool) string {
|
||||
|
|
@ -93,7 +96,7 @@ func TestHelmState_executeTemplates(t *testing.T) {
|
|||
SetValuesTemplate: []SetValue{
|
||||
{Name: "val1", Value: "{{ .Release.Name }}-val1"},
|
||||
{Name: "val2", File: "{{ .Release.Name }}.yml"},
|
||||
{Name: "val3", Values: []string{"{{ .Release.Name }}-val2", "{{ .Release.Name }}-val3"}},
|
||||
{Name: "val3", Values: []any{"{{ .Release.Name }}-val2", "{{ .Release.Name }}-val3"}},
|
||||
{Name: "val4", Value: "{{ .Release.Chart }}-{{ .Release.ChartVersion}}"},
|
||||
},
|
||||
},
|
||||
|
|
@ -105,7 +108,7 @@ func TestHelmState_executeTemplates(t *testing.T) {
|
|||
SetValues: []SetValue{
|
||||
{Name: "val1", Value: "test-app-val1"},
|
||||
{Name: "val2", File: "test-app.yml"},
|
||||
{Name: "val3", Values: []string{"test-app-val2", "test-app-val3"}},
|
||||
{Name: "val3", Values: []any{"test-app-val2", "test-app-val3"}},
|
||||
{Name: "val4", Value: "test-charts/chart-1.5"},
|
||||
},
|
||||
},
|
||||
|
|
@ -294,3 +297,210 @@ func TestHelmState_recursiveRefsTemplates(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyDefaultInherit(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
defaultInherit DefaultInherits
|
||||
releaseInherit Inherits
|
||||
want Inherits
|
||||
}{
|
||||
{
|
||||
name: "no default inherit",
|
||||
defaultInherit: nil,
|
||||
releaseInherit: Inherits{{Template: "foo"}},
|
||||
want: Inherits{{Template: "foo"}},
|
||||
},
|
||||
{
|
||||
name: "default inherit prepended",
|
||||
defaultInherit: DefaultInherits{"default"},
|
||||
releaseInherit: Inherits{{Template: "foo"}},
|
||||
want: Inherits{{Template: "default"}, {Template: "foo"}},
|
||||
},
|
||||
{
|
||||
name: "default inherit already in release inherit is not duplicated",
|
||||
defaultInherit: DefaultInherits{"default"},
|
||||
releaseInherit: Inherits{{Template: "default"}, {Template: "foo"}},
|
||||
want: Inherits{{Template: "default"}, {Template: "foo"}},
|
||||
},
|
||||
{
|
||||
name: "multiple default inherits",
|
||||
defaultInherit: DefaultInherits{"a", "b"},
|
||||
releaseInherit: Inherits{{Template: "c"}},
|
||||
want: Inherits{{Template: "a"}, {Template: "b"}, {Template: "c"}},
|
||||
},
|
||||
{
|
||||
name: "release inherit empty with defaults",
|
||||
defaultInherit: DefaultInherits{"default"},
|
||||
releaseInherit: nil,
|
||||
want: Inherits{{Template: "default"}},
|
||||
},
|
||||
{
|
||||
name: "default inherit deduplicates and skips empty values",
|
||||
defaultInherit: DefaultInherits{"default", " ", "default", "ops"},
|
||||
releaseInherit: Inherits{{Template: "foo"}},
|
||||
want: Inherits{{Template: "default"}, {Template: "ops"}, {Template: "foo"}},
|
||||
},
|
||||
{
|
||||
// Whitespace-only template names in releaseInherit are used verbatim for dedup
|
||||
// (trimmed for map lookup), so the user's explicit entry is preserved in the output
|
||||
// and the default is not prepended again.
|
||||
name: "release inherit with whitespace template is deduplicated correctly",
|
||||
defaultInherit: DefaultInherits{"default"},
|
||||
releaseInherit: Inherits{{Template: " default "}, {Template: "foo"}},
|
||||
want: Inherits{{Template: " default "}, {Template: "foo"}},
|
||||
},
|
||||
{
|
||||
name: "release inherit with blank template is skipped",
|
||||
defaultInherit: DefaultInherits{"default"},
|
||||
releaseInherit: Inherits{{Template: ""}, {Template: "foo"}},
|
||||
want: Inherits{{Template: "default"}, {Template: "foo"}},
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
tt := tests[i]
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
st := &HelmState{
|
||||
ReleaseSetSpec: ReleaseSetSpec{
|
||||
DefaultInherit: tt.defaultInherit,
|
||||
},
|
||||
}
|
||||
got := st.applyDefaultInherit(tt.releaseInherit)
|
||||
if len(got) != len(tt.want) {
|
||||
t.Fatalf("expected %d inherits, got %d", len(tt.want), len(got))
|
||||
}
|
||||
for j := range got {
|
||||
if got[j].Template != tt.want[j].Template {
|
||||
t.Errorf("inherit[%d]: expected template %q, got %q", j, tt.want[j].Template, got[j].Template)
|
||||
}
|
||||
if len(got[j].Except) != len(tt.want[j].Except) {
|
||||
t.Errorf("inherit[%d]: expected %d except, got %d", j, len(tt.want[j].Except), len(got[j].Except))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelmState_executeTemplatesWithDefaultTemplates(t *testing.T) {
|
||||
logger := zap.NewNop().Sugar()
|
||||
state := &HelmState{
|
||||
logger: logger,
|
||||
fs: &filesystem.FileSystem{
|
||||
Glob: func(s string) ([]string, error) { return nil, nil },
|
||||
},
|
||||
basePath: ".",
|
||||
ReleaseSetSpec: ReleaseSetSpec{
|
||||
HelmDefaults: HelmSpec{
|
||||
KubeContext: "test_context",
|
||||
},
|
||||
Env: environment.Environment{Name: "test_env"},
|
||||
Templates: map[string]TemplateSpec{
|
||||
"default": {
|
||||
ReleaseSpec: ReleaseSpec{
|
||||
Namespace: "default-ns",
|
||||
Labels: map[string]string{"managed": "true"},
|
||||
},
|
||||
},
|
||||
},
|
||||
DefaultInherit: DefaultInherits{"default"},
|
||||
Releases: []ReleaseSpec{
|
||||
{
|
||||
Name: "app1",
|
||||
Chart: "test-chart",
|
||||
},
|
||||
{
|
||||
Name: "app2",
|
||||
Chart: "test-chart-2",
|
||||
Inherit: Inherits{
|
||||
{Template: "default", Except: []string{"labels"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RenderedValues: map[string]any{},
|
||||
}
|
||||
|
||||
r, err := state.ExecuteTemplates()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
app1 := r.Releases[0]
|
||||
if app1.Namespace != "default-ns" {
|
||||
t.Errorf("app1: expected namespace %q, got %q", "default-ns", app1.Namespace)
|
||||
}
|
||||
if app1.Labels["managed"] != "true" {
|
||||
t.Errorf("app1: expected label managed=true, got %v", app1.Labels)
|
||||
}
|
||||
|
||||
app2 := r.Releases[1]
|
||||
if app2.Namespace != "default-ns" {
|
||||
t.Errorf("app2: expected namespace %q, got %q", "default-ns", app2.Namespace)
|
||||
}
|
||||
if _, ok := app2.Labels["managed"]; ok {
|
||||
t.Errorf("app2: expected labels to be excluded, but got %v", app2.Labels)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultInherits_UnmarshalYAML(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want DefaultInherits
|
||||
}{
|
||||
{
|
||||
name: "single string",
|
||||
input: `default`,
|
||||
want: DefaultInherits{"default"},
|
||||
},
|
||||
{
|
||||
name: "list of strings",
|
||||
input: `["a", "b"]`,
|
||||
want: DefaultInherits{"a", "b"},
|
||||
},
|
||||
{
|
||||
name: "null value",
|
||||
input: `null`,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "empty string value",
|
||||
input: `""`,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "list trims and drops empty names",
|
||||
input: `[" a ", "", " ", "b"]`,
|
||||
want: DefaultInherits{"a", "b"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, enableGoYamlV3 := range []bool{true, false} {
|
||||
t.Run(fmt.Sprintf("GoYamlV3=%t", enableGoYamlV3), func(t *testing.T) {
|
||||
prev := runtime.GoYamlV3
|
||||
runtime.GoYamlV3 = enableGoYamlV3
|
||||
defer func() {
|
||||
runtime.GoYamlV3 = prev
|
||||
}()
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var got DefaultInherits
|
||||
err := yaml.Unmarshal([]byte(tt.input), &got)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(got) != len(tt.want) {
|
||||
t.Fatalf("expected %d items, got %d", len(tt.want), len(got))
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tt.want[i] {
|
||||
t.Errorf("item[%d]: expected %q, got %q", i, tt.want[i], got[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2096,7 +2096,7 @@ func TestHelmState_SyncReleases(t *testing.T) {
|
|||
SetValues: []SetValue{
|
||||
{
|
||||
Name: "foo.bar[0]",
|
||||
Values: []string{
|
||||
Values: []any{
|
||||
"A",
|
||||
"B",
|
||||
},
|
||||
|
|
@ -2107,6 +2107,26 @@ func TestHelmState_SyncReleases(t *testing.T) {
|
|||
helm: &exectest.Helm{},
|
||||
wantReleases: []exectest.Release{{Name: "releaseName", Flags: []string{"--set", "foo.bar[0]={A,B}", "--reset-values"}}},
|
||||
},
|
||||
{
|
||||
name: "set array of map values",
|
||||
releases: []ReleaseSpec{
|
||||
{
|
||||
Name: "releaseName",
|
||||
Chart: "foo",
|
||||
SetValues: []SetValue{
|
||||
{
|
||||
Name: "source.helm.parameters",
|
||||
Values: []any{
|
||||
map[string]any{"name": "demo"},
|
||||
map[string]any{"version": "v2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
helm: &exectest.Helm{},
|
||||
wantReleases: []exectest.Release{{Name: "releaseName", Flags: []string{"--set", "source.helm.parameters={\\{\"name\":\"demo\"\\},\\{\"version\":\"v2\"\\}}", "--reset-values"}}},
|
||||
},
|
||||
{
|
||||
name: "post renderer helm 3",
|
||||
releases: []ReleaseSpec{
|
||||
|
|
@ -2709,7 +2729,7 @@ func TestHelmState_DiffReleases(t *testing.T) {
|
|||
SetValues: []SetValue{
|
||||
{
|
||||
Name: "foo.bar[0]",
|
||||
Values: []string{
|
||||
Values: []any{
|
||||
"A",
|
||||
"B",
|
||||
},
|
||||
|
|
@ -5698,7 +5718,7 @@ func TestHelmState_setStringFlags(t *testing.T) {
|
|||
setStringValues: []SetValue{
|
||||
{
|
||||
Name: "key",
|
||||
Values: []string{"value1", "value2"},
|
||||
Values: []any{"value1", "value2"},
|
||||
},
|
||||
},
|
||||
want: []string{"--set-string", "key={value1,value2}"},
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export HELM_DATA_HOME="${helm_dir}/data"
|
|||
export HELM_HOME="${HELM_DATA_HOME}"
|
||||
export HELM_PLUGINS="${HELM_DATA_HOME}/plugins"
|
||||
export HELM_CONFIG_HOME="${helm_dir}/config"
|
||||
HELM_DIFF_VERSION="${HELM_DIFF_VERSION:-3.15.3}"
|
||||
HELM_DIFF_VERSION="${HELM_DIFF_VERSION:-3.15.7}"
|
||||
HELM_GIT_VERSION="${HELM_GIT_VERSION:-1.4.1}"
|
||||
HELM_SECRETS_VERSION="${HELM_SECRETS_VERSION:-4.7.4}"
|
||||
export GNUPGHOME="${PWD}/${dir}/.gnupg"
|
||||
|
|
@ -143,6 +143,8 @@ ${kubectl} create namespace ${test_ns} || fail "Could not create namespace ${tes
|
|||
. ${dir}/test-cases/issue-2424-sequential-values-paths.sh
|
||||
. ${dir}/test-cases/issue-2431.sh
|
||||
. ${dir}/test-cases/issue-2544.sh
|
||||
. ${dir}/test-cases/issue-2596-local-deps-multiple-files.sh
|
||||
. ${dir}/test-cases/issue-2599-default-inherit.sh
|
||||
. ${dir}/test-cases/kubedog-tracking.sh
|
||||
|
||||
# ALL DONE -----------------------------------------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Test for issue #2271: lookup function should work with strategicMergePatches
|
||||
# Test for issue #2271: lookup function should work with strategicMergePatches and jsonPatches
|
||||
# Without this fix, helm template runs client-side and lookup() returns empty values
|
||||
|
||||
issue_2271_input_dir="${cases_dir}/issue-2271/input"
|
||||
|
|
@ -8,7 +8,7 @@ issue_2271_tmp_dir=$(mktemp -d)
|
|||
|
||||
cd "${issue_2271_input_dir}"
|
||||
|
||||
test_start "issue-2271: lookup function with strategicMergePatches"
|
||||
test_start "issue-2271: lookup function with strategicMergePatches and jsonPatches"
|
||||
|
||||
# Test 1: Install chart without kustomize patches
|
||||
info "Installing chart without kustomize patches"
|
||||
|
|
@ -36,26 +36,44 @@ fi
|
|||
|
||||
info "ConfigMap value updated to: $current_value"
|
||||
|
||||
assert_lookup_preserved() {
|
||||
local label="$1"
|
||||
local helmfile_path="$2"
|
||||
local output_path="$3"
|
||||
local code
|
||||
|
||||
info "Testing diff with ${label} - lookup should preserve value"
|
||||
|
||||
${helmfile} -f "${helmfile_path}" diff > "${output_path}" 2>&1
|
||||
code=$?
|
||||
|
||||
if [ $code -ne 0 ] && [ $code -ne 2 ]; then
|
||||
cat "${output_path}"
|
||||
rm -rf "${issue_2271_tmp_dir}"
|
||||
fail "Unexpected error during diff with ${label}"
|
||||
fi
|
||||
|
||||
# Check if the diff contains the preserved value (not "initial-value")
|
||||
if grep -q "preserved-value.*test-preserved-value" "${output_path}"; then
|
||||
info "SUCCESS: lookup function preserved the value with ${label}"
|
||||
elif grep -q "preserved-value.*initial-value" "${output_path}"; then
|
||||
cat "${output_path}"
|
||||
rm -rf "${issue_2271_tmp_dir}"
|
||||
fail "Issue #2271 regression: lookup function returned empty value with ${label}"
|
||||
else
|
||||
# No diff for ConfigMap means value is perfectly preserved
|
||||
info "SUCCESS: No ConfigMap changes detected for ${label} (value perfectly preserved)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 3: Diff with strategicMergePatches should preserve the lookup value
|
||||
info "Testing diff with strategicMergePatches - lookup should preserve value"
|
||||
assert_lookup_preserved "strategicMergePatches" "helmfile.yaml" "${issue_2271_tmp_dir}/test-2271-strategic-diff.txt"
|
||||
|
||||
${helmfile} -f helmfile.yaml diff > "${issue_2271_tmp_dir}/test-2271-diff.txt" 2>&1
|
||||
code=$?
|
||||
|
||||
# Check if the diff contains the preserved value (not "initial-value")
|
||||
if grep -q "preserved-value.*test-preserved-value" "${issue_2271_tmp_dir}/test-2271-diff.txt"; then
|
||||
info "SUCCESS: lookup function preserved the value with kustomize patches"
|
||||
elif grep -q "preserved-value.*initial-value" "${issue_2271_tmp_dir}/test-2271-diff.txt"; then
|
||||
cat "${issue_2271_tmp_dir}/test-2271-diff.txt"
|
||||
rm -rf "${issue_2271_tmp_dir}"
|
||||
fail "Issue #2271 regression: lookup function returned empty value with kustomize"
|
||||
else
|
||||
# No diff for ConfigMap means value is perfectly preserved
|
||||
info "SUCCESS: No ConfigMap changes detected (value perfectly preserved)"
|
||||
fi
|
||||
# Test 4: Diff with jsonPatches should preserve the lookup value
|
||||
assert_lookup_preserved "jsonPatches" "helmfile-jsonpatch.yaml" "${issue_2271_tmp_dir}/test-2271-json-diff.txt"
|
||||
|
||||
# Cleanup
|
||||
${helm} uninstall test-release-2271 --namespace default 2>/dev/null || true
|
||||
rm -rf "${issue_2271_tmp_dir}"
|
||||
|
||||
test_pass "issue-2271: lookup function with strategicMergePatches"
|
||||
test_pass "issue-2271: lookup function with strategicMergePatches and jsonPatches"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
releases:
|
||||
- name: test-release-2271
|
||||
namespace: default
|
||||
chart: ./test-chart
|
||||
installed: true
|
||||
jsonPatches:
|
||||
- target:
|
||||
group: apps
|
||||
version: v1
|
||||
kind: Deployment
|
||||
name: test-release-2271-app
|
||||
namespace: default
|
||||
patch:
|
||||
- op: add
|
||||
path: /spec/template/metadata/labels/hello
|
||||
value: world
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
# Integration test for issue #2596: Local dependencies with multiple release files
|
||||
# https://github.com/helmfile/helmfile/issues/2596
|
||||
# Reproduction: https://github.com/vgivanov/helmfile-deps-local-chart
|
||||
#
|
||||
# This test uses the exact same structure as the reproduction repo:
|
||||
# chart/Chart.yaml
|
||||
# helmfile.d/release1.yaml (chart: ../chart with dependencies)
|
||||
# helmfile.d/release2.yaml (chart: ../chart without dependencies)
|
||||
#
|
||||
# Before the fix, running `helmfile template` from this directory would fail with:
|
||||
# "failed reading adhoc dependencies: no helm list entry found for repository"
|
||||
# because the relative dependency chart path "../chart" was not normalized against
|
||||
# basePath before calling DirectoryExistsAt.
|
||||
|
||||
issue_2596_input_dir="${cases_dir}/issue-2596-local-deps-multiple-files/input"
|
||||
issue_2596_tmp=""
|
||||
|
||||
cleanup_issue_2596() {
|
||||
if [ -n "${issue_2596_tmp}" ] && [ -d "${issue_2596_tmp}" ]; then
|
||||
rm -rf "${issue_2596_tmp}"
|
||||
fi
|
||||
}
|
||||
trap cleanup_issue_2596 EXIT
|
||||
|
||||
issue_2596_tmp=$(mktemp -d)
|
||||
helmfile_real="$(pwd)/${helmfile}"
|
||||
|
||||
test_start "issue #2596: local deps with multiple release files"
|
||||
|
||||
info "Testing helmfile template with local chart dependencies across multiple release files"
|
||||
|
||||
cd "${issue_2596_input_dir}"
|
||||
|
||||
${helmfile_real} template > "${issue_2596_tmp}/output.yaml" 2>&1
|
||||
result=$?
|
||||
|
||||
cd - > /dev/null
|
||||
|
||||
if [ $result -ne 0 ]; then
|
||||
cat "${issue_2596_tmp}/output.yaml"
|
||||
fail "helmfile template with local chart dependencies should not fail"
|
||||
fi
|
||||
|
||||
info "Local chart dependencies with multiple release files works correctly"
|
||||
|
||||
cleanup_issue_2596
|
||||
trap - EXIT
|
||||
|
||||
test_pass "issue #2596: local deps with multiple release files"
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
apiVersion: v2
|
||||
name: chart
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "1.16.0"
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
releases:
|
||||
- name: release1
|
||||
chart: ../chart
|
||||
version: "*"
|
||||
dependencies:
|
||||
- chart: ../chart
|
||||
version: "*"
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
releases:
|
||||
- name: release2
|
||||
chart: ../chart
|
||||
version: "*"
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
# Issue #2599: Test that defaultInherit applies template inheritance to all releases
|
||||
# https://github.com/helmfile/helmfile/issues/2599
|
||||
#
|
||||
# This test verifies that:
|
||||
# - defaultInherit as a single string applies the template to all releases
|
||||
# - Releases without explicit inherit still get the template
|
||||
# - Releases with explicit inherit + except are not duplicated
|
||||
# - Non-existent template in defaultInherit produces a clear error
|
||||
|
||||
issue_2599_input_dir="${cases_dir}/issue-2599-default-inherit/input"
|
||||
issue_2599_tmp=""
|
||||
|
||||
cleanup_issue_2599() {
|
||||
if [ -n "${issue_2599_tmp}" ] && [ -d "${issue_2599_tmp}" ]; then
|
||||
rm -rf "${issue_2599_tmp}"
|
||||
fi
|
||||
}
|
||||
trap cleanup_issue_2599 EXIT
|
||||
|
||||
issue_2599_tmp=$(mktemp -d)
|
||||
|
||||
test_start "issue 2599 default inherit"
|
||||
|
||||
# Test 1: defaultInherit applies template to all releases
|
||||
info "Running helmfile build with defaultInherit"
|
||||
${helmfile} -f "${issue_2599_input_dir}/helmfile.yaml" build \
|
||||
> "${issue_2599_tmp}/output.log" 2>&1 \
|
||||
|| { cat "${issue_2599_tmp}/output.log"; fail "helmfile build with defaultInherit shouldn't fail"; }
|
||||
|
||||
# Verify namespace from template is applied to both releases
|
||||
grep -q "namespace: default-ns" "${issue_2599_tmp}/output.log" \
|
||||
|| fail "namespace from default template should be applied"
|
||||
|
||||
# Verify both releases are processed
|
||||
grep -q "app1" "${issue_2599_tmp}/output.log" \
|
||||
|| fail "release app1 should be in output"
|
||||
|
||||
grep -q "app2" "${issue_2599_tmp}/output.log" \
|
||||
|| fail "release app2 should be in output"
|
||||
grep -q "^templates:" "${issue_2599_tmp}/output.log" \
|
||||
|| fail "templates section should be in build output"
|
||||
|
||||
# Verify inherited values and labels per release
|
||||
sed -n '/name: app1/,/name: app2/p' "${issue_2599_tmp}/output.log" > "${issue_2599_tmp}/app1.log"
|
||||
sed -n '/name: app2/,/^templates:/{/^templates:/!p}' "${issue_2599_tmp}/output.log" > "${issue_2599_tmp}/app2.log"
|
||||
[ -s "${issue_2599_tmp}/app1.log" ] || fail "failed to extract release app1 section from build output"
|
||||
[ -s "${issue_2599_tmp}/app2.log" ] || fail "failed to extract release app2 section from build output"
|
||||
|
||||
grep -Eq 'managed:[[:space:]]*"?true"?([[:space:]]|$)' "${issue_2599_tmp}/app1.log" \
|
||||
|| fail "release app1 should inherit managed label from default template"
|
||||
grep -q "common.yaml" "${issue_2599_tmp}/app1.log" \
|
||||
|| fail "release app1 should inherit values from common.yaml"
|
||||
grep -q "common.yaml" "${issue_2599_tmp}/app2.log" \
|
||||
|| fail "release app2 should inherit values from common.yaml"
|
||||
if grep -Eq 'managed:[[:space:]]*"?true"?([[:space:]]|$)' "${issue_2599_tmp}/app2.log"; then
|
||||
fail "release app2 should not inherit managed label due to except"
|
||||
fi
|
||||
|
||||
# Test 2: non-existent template in defaultInherit should fail
|
||||
info "Running helmfile build with non-existent defaultInherit template"
|
||||
cat > "${issue_2599_tmp}/bad-helmfile.yaml" <<EOF
|
||||
defaultInherit: nonexistent
|
||||
releases:
|
||||
- name: app1
|
||||
chart: ${dir}/charts/raw
|
||||
EOF
|
||||
|
||||
${helmfile} -f "${issue_2599_tmp}/bad-helmfile.yaml" build \
|
||||
> "${issue_2599_tmp}/error.log" 2>&1 \
|
||||
&& fail "helmfile build with non-existent defaultInherit template should fail"
|
||||
|
||||
grep -q "inexistent release template" "${issue_2599_tmp}/error.log" \
|
||||
|| fail "error message should mention inexistent release template"
|
||||
|
||||
test_pass "issue 2599 default inherit"
|
||||
|
|
@ -0,0 +1 @@
|
|||
testKey: testValue
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
templates:
|
||||
default:
|
||||
namespace: default-ns
|
||||
labels:
|
||||
managed: "true"
|
||||
values:
|
||||
- common.yaml
|
||||
|
||||
defaultInherit: default
|
||||
|
||||
releases:
|
||||
- name: app1
|
||||
chart: ../../../charts/raw
|
||||
- name: app2
|
||||
chart: ../../../charts/raw
|
||||
inherit:
|
||||
- template: default
|
||||
except:
|
||||
- labels
|
||||
Loading…
Reference in New Issue