fix: helmBinary setting ignored in multi-document YAML files (#2414)

* fix: helmBinary setting ignored in multi-document YAML files

The helmBinary setting in helmfile.yaml was being ignored when using
multi-document YAML files (files with --- separators).

Root Cause:
When processing multi-document YAML files, the load() function splits
the file into parts and processes each part separately. Each part was
calling applyDefaultsAndOverrides() which would set an empty helmBinary
to the default 'helm'. When merging parts, the default value from a
later part would override the correct value from an earlier part.

Fix:
- Added a new applyDefaults parameter to ParseAndLoad() to control when
  defaults are applied
- Modified rawLoad() to pass applyDefaults=false when processing
  individual parts
- Added a call to ApplyDefaultsAndOverrides() after all parts are merged
  to apply defaults once on the final merged state
- Exported ApplyDefaultsAndOverrides() method for use by the app package

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

* fix: update comment per PR review

Change 're-apply' to 'apply' since defaults are never applied during
part processing (applyDefaults=false is passed), so this is the first
and only time defaults are applied to the merged state.

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

* fix: clarify applyDefaults logic in test LoadFile callbacks

Add explicit applyDefaults variable with comment explaining why it
equals evaluateBases: base files shouldn't apply defaults, only the
main file should after all parts/bases are merged.

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

* fix: address PR review comments

- Remove applyDefaults parameter from rawLoad() since it's always false
- Add regression test for multi-document YAML with helmBinary (issue #2319)

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

* test: add integration test for helmBinary in multi-document YAML

Add TestHelmBinaryPreservedInMultiDocumentYAML that exercises the full
loadDesiredStateFromYaml path to ensure helmBinary from the first
document is preserved when merging multi-document YAML files.

This is a regression test at the load() orchestration level for issue #2319.

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

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
This commit is contained in:
yxxhero
2026-02-20 22:12:38 +08:00
committed by GitHub
parent cd918b79d1
commit ca8fc293e9
4 changed files with 127 additions and 12 deletions
+44
View File
@@ -4618,3 +4618,47 @@ func TestRenderYamlEnvVar(t *testing.T) {
})
}
}
// TestHelmBinaryPreservedInMultiDocumentYAML tests that helmBinary is preserved
// when processing multi-document YAML files at the loadDesiredStateFromYaml level.
// This is a regression test for issue #2319.
func TestHelmBinaryPreservedInMultiDocumentYAML(t *testing.T) {
statePath := "/path/to/helmfile.yaml"
stateContent := `helmBinary: /usr/bin/werf
helmDefaults:
createNamespace: true
---
releases:
- name: myapp
chart: stable/nginx
`
testFs := testhelper.NewTestFs(map[string]string{
statePath: stateContent,
})
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Namespace: "",
Env: "default",
FileOrDir: statePath,
}
app = injectFs(app, testFs)
app.remote = remote.NewRemote(app.Logger, testFs.Cwd, app.fs)
expectNoCallsToHelm(app)
st, err := app.loadDesiredStateFromYaml(statePath, LoadOpts{})
require.NoError(t, err, "unexpected error loading helmfile")
require.NotNil(t, st, "expected state to be loaded")
// The key assertion: helmBinary from document 1 should be preserved
// even though document 2 has no helmBinary setting
assert.Equal(t, "/usr/bin/werf", st.DefaultHelmBinary,
"helmBinary from first document should be preserved, not reset to default 'helm'")
}
+10 -1
View File
@@ -185,7 +185,8 @@ func (a *desiredStateLoader) rawLoad(yaml []byte, baseDir, file string, evaluate
return nil, err
}
st, err = a.underlying().ParseAndLoad(yaml, baseDir, file, a.env, false, evaluateBases, merged, nil)
// applyDefaults is always false here - defaults are applied after all parts are merged
st, err = a.underlying().ParseAndLoad(yaml, baseDir, file, a.env, false, evaluateBases, false, merged, nil)
if err != nil {
return nil, err
}
@@ -297,6 +298,14 @@ func (ld *desiredStateLoader) load(env, overrodeEnv *environment.Environment, ba
}
}
// After all parts are merged, apply defaults and overrides to ensure
// that values from earlier parts (like helmBinary) are preserved correctly
// in the merged state.
// See https://github.com/helmfile/helmfile/issues/2319
if evaluateBases {
ld.underlying().ApplyDefaultsAndOverrides(finalState)
}
// If environments are not defined in the helmfile at all although the env is specified,
// it's a missing env situation. Let's fail.
if len(finalState.Environments) == 0 && evaluateBases && !hasEnv && env.Name != state.DefaultEnv {