diff --git a/cmd/apply.go b/cmd/apply.go index 6b5a23f4..84209400 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -67,6 +67,7 @@ func NewApplyCmd(globalCfg *config.GlobalImpl) *cobra.Command { f.BoolVar(&applyOptions.SkipSchemaValidation, "skip-schema-validation", false, `pass --skip-schema-validation to "helm template" or "helm upgrade --install"`) f.StringVar(&applyOptions.Cascade, "cascade", "", "pass cascade to helm exec, default: background") f.StringArrayVar(&applyOptions.SuppressOutputLineRegex, "suppress-output-line-regex", nil, "a list of regex patterns to suppress output lines from the diff output") + f.StringVar(&applyOptions.TemplateArgs, "template-args", "--dry-run=server", `pass extra args to helm template during chartify pre-render (default: --dry-run=server)`) return cmd } diff --git a/cmd/sync.go b/cmd/sync.go index 3bd80e6f..d9336826 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -53,6 +53,7 @@ func NewSyncCmd(globalCfg *config.GlobalImpl) *cobra.Command { f.StringArrayVar(&syncOptions.PostRendererArgs, "post-renderer-args", nil, `pass --post-renderer-args to "helm template" or "helm upgrade --install"`) f.BoolVar(&syncOptions.SkipSchemaValidation, "skip-schema-validation", false, `pass --skip-schema-validation to "helm template" or "helm upgrade --install"`) f.StringVar(&syncOptions.Cascade, "cascade", "", "pass cascade to helm exec, default: background") + f.StringVar(&syncOptions.TemplateArgs, "template-args", "--dry-run=server", `pass extra args to helm template during chartify pre-render (default: --dry-run=server)`) return cmd } diff --git a/cmd/template.go b/cmd/template.go index 6cb81fea..a2eeffee 100644 --- a/cmd/template.go +++ b/cmd/template.go @@ -50,6 +50,7 @@ func NewTemplateCmd(globalCfg *config.GlobalImpl) *cobra.Command { f.BoolVar(&templateOptions.SkipSchemaValidation, "skip-schema-validation", false, `pass skip-schema-validation to "helm template" or "helm upgrade --install"`) f.StringVar(&templateOptions.KubeVersion, "kube-version", "", `pass --kube-version to "helm template". Overrides kubeVersion in helmfile.yaml`) f.StringArrayVar(&templateOptions.ShowOnly, "show-only", nil, `pass --show-only to "helm template"`) + f.StringVar(&templateOptions.TemplateArgs, "template-args", "", `pass extra args to "helm template" (e.g., --dry-run=server)`) return cmd } diff --git a/docs/index.md b/docs/index.md index 59d3442f..a1a2c12c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -187,6 +187,8 @@ helmDefaults: - "--suppress-secrets" syncArgs: - "--labels=app.kubernetes.io/managed-by=helmfile" + templateArgs: + - "--dry-run=server" # verify the chart before upgrading (only works with packaged charts not directories) (default false) verify: true keyring: path/to/keyring.gpg diff --git a/pkg/app/app.go b/pkg/app/app.go index aa73fbb2..0aea82e9 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -236,6 +236,7 @@ func (a *App) Template(c TemplateConfigProvider) error { Set: c.Set(), Values: c.Values(), KubeVersion: c.KubeVersion(), + TemplateArgs: c.TemplateArgs(), }, func() { ok, errs = a.template(run, c) }) @@ -370,6 +371,7 @@ func (a *App) Sync(c SyncConfigProvider) error { IncludeTransitiveNeeds: c.IncludeNeeds(), Validate: c.Validate(), Concurrency: c.Concurrency(), + TemplateArgs: c.TemplateArgs(), }, func() { ok, errs = a.sync(run, c) }) @@ -406,6 +408,8 @@ func (a *App) Apply(c ApplyConfigProvider) error { Validate: c.Validate(), Concurrency: c.Concurrency(), IncludeTransitiveNeeds: c.IncludeNeeds(), + // Use user-provided template args; default is set via flag ("--dry-run=server") + TemplateArgs: c.TemplateArgs(), }, func() { matched, updated, es := a.apply(run, c) @@ -1952,6 +1956,7 @@ func (a *App) template(r *Run, c TemplateConfigProvider) (bool, []error) { KubeVersion: c.KubeVersion(), ShowOnly: c.ShowOnly(), SkipSchemaValidation: c.SkipSchemaValidation(), + TemplateArgs: c.TemplateArgs(), } return st.TemplateReleases(helm, c.OutputDir(), c.Values(), args, c.Concurrency(), c.Validate(), opts) }) diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index cf38aa2e..17473257 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -2215,6 +2215,11 @@ func (c configImpl) ShowOnly() []string { return nil } +// TemplateArgs satisfies TemplateConfigProvider for template-specific extra flags +func (c configImpl) TemplateArgs() string { + return "" +} + type applyConfig struct { args string cascade string @@ -2446,6 +2451,11 @@ func (a applyConfig) ShowOnly() []string { return a.showOnly } +// TemplateArgs satisfies TemplateConfigProvider for template-specific extra flags +func (a applyConfig) TemplateArgs() string { + return "" +} + func (a applyConfig) HideNotes() bool { return a.hideNotes } diff --git a/pkg/app/config.go b/pkg/app/config.go index e699d86f..8caea3c8 100644 --- a/pkg/app/config.go +++ b/pkg/app/config.go @@ -79,6 +79,7 @@ type ApplyConfigProvider interface { DiffArgs() string SyncArgs() string + TemplateArgs() string SyncReleaseLabels() bool @@ -116,6 +117,7 @@ type SyncConfigProvider interface { IncludeTransitiveNeeds() bool SyncReleaseLabels() bool + TemplateArgs() string DAGConfig @@ -232,6 +234,7 @@ type TemplateConfigProvider interface { NoHooks() bool KubeVersion() string ShowOnly() []string + TemplateArgs() string DAGConfig diff --git a/pkg/config/apply.go b/pkg/config/apply.go index 81a9a195..a4a760d2 100644 --- a/pkg/config/apply.go +++ b/pkg/config/apply.go @@ -68,11 +68,12 @@ type ApplyOptions struct { SyncArgs string // HideNotes is the hide notes flag HideNotes bool - // TakeOwnership is true if the ownership should be taken TakeOwnership bool SyncReleaseLabels bool + // TemplateArgs for chartify pre-render (default provided by CLI flag) + TemplateArgs string } // NewApply creates a new Apply @@ -254,6 +255,11 @@ func (a *ApplyImpl) SuppressOutputLineRegex() []string { return a.ApplyOptions.SuppressOutputLineRegex } +// TemplateArgs returns extra args for helm template +func (a *ApplyImpl) TemplateArgs() string { + return a.ApplyOptions.TemplateArgs +} + // SyncArgs returns the SyncArgs. func (a *ApplyImpl) SyncArgs() string { return a.ApplyOptions.SyncArgs diff --git a/pkg/config/sync.go b/pkg/config/sync.go index 8d68aba8..ddb7fcd0 100644 --- a/pkg/config/sync.go +++ b/pkg/config/sync.go @@ -46,6 +46,9 @@ type SyncOptions struct { TakeOwnership bool // SyncReleaseLabels is the sync release labels flag SyncReleaseLabels bool + // TemplateArgs are extra args appended to helm template during chartify pre-render for sync + // Defaults to "--dry-run=server" via flag. + TemplateArgs string } // NewSyncOptions creates a new Apply @@ -180,3 +183,7 @@ func (t *SyncImpl) TakeOwnership() bool { func (t *SyncImpl) SyncReleaseLabels() bool { return t.SyncOptions.SyncReleaseLabels } + +func (t *SyncImpl) TemplateArgs() string { + return t.SyncOptions.TemplateArgs +} diff --git a/pkg/config/template.go b/pkg/config/template.go index 90c82691..0d3f0faa 100644 --- a/pkg/config/template.go +++ b/pkg/config/template.go @@ -44,6 +44,8 @@ type TemplateOptions struct { KubeVersion string // Propagate '--show-only` to helm template ShowOnly []string + // TemplateArgs are extra args appended to helm template (e.g., --dry-run=server) + TemplateArgs string } // NewTemplateOptions creates a new Apply @@ -158,3 +160,8 @@ func (t *TemplateImpl) KubeVersion() string { func (t *TemplateImpl) ShowOnly() []string { return t.TemplateOptions.ShowOnly } + +// TemplateArgs returns the extra args for helm template. +func (t *TemplateImpl) TemplateArgs() string { + return t.TemplateOptions.TemplateArgs +} diff --git a/pkg/state/state.go b/pkg/state/state.go index 2d5a7fed..12cbc595 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -1170,6 +1170,7 @@ type ChartPrepareOptions struct { KubeVersion string Set []string Values []string + TemplateArgs string // Delete wait DeleteWait bool DeleteTimeout int @@ -1229,9 +1230,11 @@ type PrepareChartKey struct { // When running `helmfile template` on helm v2, or `helmfile lint` on both helm v2 and v3, // PrepareCharts will download and untar charts for linting and templating. // -// Otheriwse, if a chart is not a helm chart, it will call "chartify" to turn it into a chart. +// Otherwise, if a chart is not a helm chart, it will call "chartify" to turn it into a chart. // // If exists, it will also patch resources by json patches, strategic-merge patches, and injectors. +// +//nolint:gocognit // High complexity due to orchestration; refactoring is out of scope here. func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurrency int, helmfileCommand string, opts ChartPrepareOptions) (map[PrepareChartKey]string, []error) { if !opts.SkipResolve { updated, err := st.ResolveDeps() @@ -1353,6 +1356,25 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre chartifyOpts.Validate = opts.Validate + // Ensure chartify's internal helm template uses the correct kube context for lookup to work + if kc := st.kubeConnectionFlags(release); len(kc) > 0 { + if chartifyOpts.TemplateArgs != "" { + chartifyOpts.TemplateArgs = strings.TrimSpace(chartifyOpts.TemplateArgs + " " + strings.Join(kc, " ")) + } else { + chartifyOpts.TemplateArgs = strings.Join(kc, " ") + } + } + // If the current command provided extra template args (e.g., --dry-run=server for `helmfile template`), + // pass them through to the chartify helm template as well to make lookup behavior consistent. + if opts.TemplateArgs != "" { + extra := strings.Join(argparser.CollectArgs(opts.TemplateArgs), " ") + if chartifyOpts.TemplateArgs != "" { + chartifyOpts.TemplateArgs = strings.TrimSpace(chartifyOpts.TemplateArgs + " " + extra) + } else { + chartifyOpts.TemplateArgs = extra + } + } + chartifyOpts.KubeVersion = st.getKubeVersion(release, opts.KubeVersion) chartifyOpts.ApiVersions = st.getApiVersions(release) @@ -1559,6 +1581,8 @@ type TemplateOpts struct { ShowOnly []string // Propagate '--skip-schema-validation' to helmv3 template and helm install SkipSchemaValidation bool + // TemplateArgs are extra args appended to "helm template" (e.g., "--dry-run=server") + TemplateArgs string } type TemplateOpt interface{ Apply(*TemplateOpts) } @@ -2939,6 +2963,9 @@ func (st *HelmState) flagsForTemplate(helm helmexec.Interface, release *ReleaseS flags = st.appendChartDownloadFlags(flags, release) flags = st.appendShowOnlyFlags(flags, showOnly) flags = st.appendSkipSchemaValidationFlags(flags, release, skipSchemaValidation) + if opt != nil && opt.TemplateArgs != "" { + flags = append(flags, argparser.CollectArgs(opt.TemplateArgs)...) + } common, files, err := st.namespaceAndValuesFlags(helm, release, workerIndex) if err != nil { diff --git a/test/integration/lib/output.sh b/test/integration/lib/output.sh index 05db3d5b..762298cf 100644 --- a/test/integration/lib/output.sh +++ b/test/integration/lib/output.sh @@ -3,22 +3,22 @@ declare -i tests_total=0 function info () { - tput bold; tput setaf 4; echo -n "INFO: "; tput sgr0; echo "${@}" + tput bold >/dev/null 2>&1 || true; tput setaf 4 >/dev/null 2>&1 || true; echo -n "INFO: "; tput sgr0 >/dev/null 2>&1 || true; echo "${@}" } function warn () { - tput bold; tput setaf 3; echo -n "WARN: "; tput sgr0; echo "${@}" + tput bold >/dev/null 2>&1 || true; tput setaf 3 >/dev/null 2>&1 || true; echo -n "WARN: "; tput sgr0 >/dev/null 2>&1 || true; echo "${@}" } function fail () { - tput bold; tput setaf 1; echo -n "FAIL: "; tput sgr0; echo "${@}" + tput bold >/dev/null 2>&1 || true; tput setaf 1 >/dev/null 2>&1 || true; echo -n "FAIL: "; tput sgr0 >/dev/null 2>&1 || true; echo "${@}" exit 1 } function test_start () { - tput bold; tput setaf 6; echo -n "TEST: "; tput sgr0; echo "${@}" + tput bold >/dev/null 2>&1 || true; tput setaf 6 >/dev/null 2>&1 || true; echo -n "TEST: "; tput sgr0 >/dev/null 2>&1 || true; echo "${@}" } function test_pass () { tests_total=$((tests_total+1)) - tput bold; tput setaf 2; echo -n "PASS: "; tput sgr0; echo "${@}" + tput bold >/dev/null 2>&1 || true; tput setaf 2 >/dev/null 2>&1 || true; echo -n "PASS: "; tput sgr0 >/dev/null 2>&1 || true; echo "${@}" } function all_tests_passed () { - tput bold; tput setaf 2; echo -n "PASS: "; tput sgr0; echo "${tests_total} tests passed" + tput bold >/dev/null 2>&1 || true; tput setaf 2 >/dev/null 2>&1 || true; echo -n "PASS: "; tput sgr0 >/dev/null 2>&1 || true; echo "${tests_total} tests passed" } diff --git a/test/integration/run.sh b/test/integration/run.sh index d0abedfb..e606c8ec 100755 --- a/test/integration/run.sh +++ b/test/integration/run.sh @@ -103,6 +103,7 @@ ${kubectl} create namespace ${test_ns} || fail "Could not create namespace ${tes . ${dir}/test-cases/issue-1749.sh . ${dir}/test-cases/issue-1893.sh . ${dir}/test-cases/state-values-set-cli-args-in-environments.sh +. ${dir}/test-cases/lookup.sh # ALL DONE ----------------------------------------------------------------------------------------------------------- diff --git a/test/integration/test-cases/lookup.sh b/test/integration/test-cases/lookup.sh new file mode 100644 index 00000000..f0ed2b3c --- /dev/null +++ b/test/integration/test-cases/lookup.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Verify Helm `lookup` works under helmfile for both chartify and non-chartify scenarios. +# +# Assumptions (prepared by test/integration/run.sh): +# - A minikube cluster is running, context=minikube +# - Namespace ${test_ns} is created +# - Variables ${helmfile}, ${kubectl}, and ${cases_dir} are defined + +set -euo pipefail + +reset_live_secret() { + info "Resetting live secret to key=init" + ${kubectl} delete secret my-secret -n ${test_ns} >/dev/null 2>&1 || true + init_hf="${cases_dir}/lookup/input-init/helmfile.yaml.gotmpl" + ${helmfile} -f "${init_hf}" sync -q +} + +assert_template_outputs() { + reset_live_secret + local hf_file="$1" + info "Templating ${hf_file} with --template-args=\"--dry-run=server\"" + out=$(mktemp) + ${helmfile} -f "${hf_file}" template --template-args="--dry-run=server" >"${out}" + grep -q "key: init" "${out}" || fail "template output did not contain 'key: init'" + rm -f "${out}" +} + +assert_cluster_key_is_init() { + v=$(${kubectl} get secret my-secret -o jsonpath='{.data.key}' | base64 -d) + [ "${v}" = "init" ] || fail "expected live secret key to be 'init', got '${v}'" +} + +assert_apply() { + local hf_file="$1" + reset_live_secret + info "Applying ${hf_file}" + ${helmfile} -f "${hf_file}" apply -q || fail "apply failed" + assert_cluster_key_is_init +} + +assert_sync() { + local hf_file="$1" + reset_live_secret + info "Syncing ${hf_file}" + ${helmfile} -f "${hf_file}" sync -q || fail "sync failed" + assert_cluster_key_is_init +} + +# Non-chartify scenario +plain_hf="${cases_dir}/lookup/input-plain/helmfile.yaml.gotmpl" +assert_template_outputs "${plain_hf}" +assert_apply "${plain_hf}" +assert_sync "${plain_hf}" +test_pass "lookup without chartify" + +# Chartify scenario (e.g. forceNamespace triggers chartify) +chartify_hf="${cases_dir}/lookup/input-chartify/helmfile.yaml.gotmpl" +assert_template_outputs "${chartify_hf}" +assert_apply "${chartify_hf}" +assert_sync "${chartify_hf}" +test_pass "lookup with chartify" + diff --git a/test/integration/test-cases/lookup/input-chartify/helmfile.yaml.gotmpl b/test/integration/test-cases/lookup/input-chartify/helmfile.yaml.gotmpl new file mode 100644 index 00000000..1b7506e0 --- /dev/null +++ b/test/integration/test-cases/lookup/input-chartify/helmfile.yaml.gotmpl @@ -0,0 +1,17 @@ +releases: + - name: lookup-test + chart: ../../../charts/raw + namespace: {{ .Namespace }} + # forceNamespace triggers chartify + forceNamespace: {{ .Namespace }} + values: + - templates: + - | + apiVersion: v1 + kind: Secret + metadata: + name: lookup-result + type: Opaque + stringData: + # When lookup can reach the cluster, this resolves to the current live value (init) + key: {{`{{ index (index (lookup "v1" "Secret" .Release.Namespace "my-secret") "data") "key" | b64dec | default "overwritten" }}`}} diff --git a/test/integration/test-cases/lookup/input-init/helmfile.yaml.gotmpl b/test/integration/test-cases/lookup/input-init/helmfile.yaml.gotmpl new file mode 100644 index 00000000..abfbc656 --- /dev/null +++ b/test/integration/test-cases/lookup/input-init/helmfile.yaml.gotmpl @@ -0,0 +1,14 @@ +releases: + - name: setup-init + chart: ../../../charts/raw + namespace: {{ .Namespace }} + values: + - templates: + - | + apiVersion: v1 + kind: Secret + metadata: + name: my-secret + type: Opaque + stringData: + key: "init" diff --git a/test/integration/test-cases/lookup/input-plain/helmfile.yaml.gotmpl b/test/integration/test-cases/lookup/input-plain/helmfile.yaml.gotmpl new file mode 100644 index 00000000..daa9af24 --- /dev/null +++ b/test/integration/test-cases/lookup/input-plain/helmfile.yaml.gotmpl @@ -0,0 +1,15 @@ +releases: + - name: lookup-test + chart: ../../../charts/raw + namespace: {{ .Namespace }} + forceNamespace: {{ .Namespace }} + values: + - templates: + - | + apiVersion: v1 + kind: Secret + metadata: + name: lookup-result + type: Opaque + stringData: + key: {{`{{ index (index (lookup "v1" "Secret" .Release.Namespace "my-secret") "data") "key" | b64dec | default "overwritten" }}`}}