mirror of
https://github.com/helmfile/helmfile.git
synced 2026-09-30 09:33:41 +02:00
* feat: add `inherits:` for sub-helmfile config inheritance
Add an opt-in `inherits:` field to `helmfiles:` entries so a sub-helmfile
can inherit specific configuration categories from its parent:
helmfiles:
- path: myapp.yaml
inherits: [repositories, environments]
Allowed values: repositories, helmDefaults, commonLabels, apiVersions,
kubeVersion, templates, environments. Child values win; parent fills gaps
(consistent with `bases:`). This directly fixes #1495, where a repository
declared in the parent was unavailable to sub-helmfiles, producing a
confusing "repo not found" error.
Implementation notes:
- The 6 pure fields (repositories, helmDefaults, commonLabels, apiVersions,
kubeVersion, templates) are merged post-load via MergeInherited; verified
all are consumed post-load (ExecuteTemplates/converge), never at parse.
- environments is injected pre-load as ctxEnv, because RenderedValues is
baked at load time; the parent's resolved values become the base and the
child's own environments: block overrides per key.
- helmDefaults uses a *HelmSpec pointer (value type is non-comparable) with
a no-override mergo merge, so a child that omits helmDefaults inherits the
parent's fully.
- A footgun warning (WarnUninheritedRepos) suggests
`inherits: [repositories]` when a release references a repo the parent
declares but the child lacks.
- Unknown inherits keys are rejected at parse time with the allowed set.
Inheritance is opt-in and fully backward compatible: empty (the default)
preserves the historical independent-sub-helmfile behavior.
Fixes #1495
Signed-off-by: yxxhero <aiopsclub@163.com>
* fix: address review — deep-copy inherited config and fix bases: doc link
- BuildInheritedConfig now deep-copies the pure fields via a YAML round-trip
(Env via environment.DeepCopy) so the returned config never aliases the
parent state's slices/maps, matching its doc comment. Now returns an error
to surface round-trip failures; the call site in processNestedHelmfiles is
updated. Added TestBuildInheritedConfig_PureFieldsAreDeepCopied to lock
in the no-aliasing guarantee.
- Fix the broken `bases:` anchor (#) in shared-configuration-across-teams.md
to point to writing-helmfile.md#layering-state-files.
Signed-off-by: yxxhero <aiopsclub@163.com>
* fix: address review — reject inherits without path and document helmDefaults caveat
Signed-off-by: yxxhero <aiopsclub@163.com>
* fix: address review — make AllowedInherits immutable and clarify effective-repo wording
Signed-off-by: yxxhero <aiopsclub@163.com>
---------
Signed-off-by: yxxhero <aiopsclub@163.com>
100 lines
2.7 KiB
Go
100 lines
2.7 KiB
Go
package state
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/helmfile/helmfile/pkg/yaml"
|
|
)
|
|
|
|
func TestSubHelmfileSpec_UnmarshalInherits(t *testing.T) {
|
|
t.Run("map form parses inherits", func(t *testing.T) {
|
|
var hf SubHelmfileSpec
|
|
require.NoError(t, yaml.Unmarshal([]byte(`
|
|
path: myapp.yaml
|
|
inherits:
|
|
- repositories
|
|
- helmDefaults
|
|
`), &hf))
|
|
assert.Equal(t, "myapp.yaml", hf.Path)
|
|
assert.Equal(t, []string{"repositories", "helmDefaults"}, hf.Inherits)
|
|
})
|
|
|
|
t.Run("string shorthand leaves inherits nil", func(t *testing.T) {
|
|
var hf SubHelmfileSpec
|
|
require.NoError(t, yaml.Unmarshal([]byte(`myapp.yaml`), &hf))
|
|
assert.Equal(t, "myapp.yaml", hf.Path)
|
|
assert.Nil(t, hf.Inherits)
|
|
})
|
|
|
|
t.Run("no inherits leaves it nil", func(t *testing.T) {
|
|
var hf SubHelmfileSpec
|
|
require.NoError(t, yaml.Unmarshal([]byte("path: myapp.yaml\n"), &hf))
|
|
assert.Nil(t, hf.Inherits)
|
|
})
|
|
}
|
|
|
|
func TestSubHelmfileSpec_RejectsUnknownInheritsKey(t *testing.T) {
|
|
var hf SubHelmfileSpec
|
|
err := yaml.Unmarshal([]byte(`
|
|
path: myapp.yaml
|
|
inherits:
|
|
- repositories
|
|
- bunkKey
|
|
`), &hf)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "invalid inherits entry")
|
|
assert.Contains(t, err.Error(), "bunkKey")
|
|
}
|
|
|
|
func TestSubHelmfileSpec_RejectsInheritsWithoutPath(t *testing.T) {
|
|
var hf SubHelmfileSpec
|
|
err := yaml.Unmarshal([]byte(`
|
|
inherits:
|
|
- repositories
|
|
`), &hf)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "found 'inherits' definition without path")
|
|
}
|
|
|
|
func TestSubHelmfileSpec_AllAllowedKeysAccepted(t *testing.T) {
|
|
for _, key := range AllowedInherits() {
|
|
var hf SubHelmfileSpec
|
|
err := yaml.Unmarshal([]byte("path: x.yaml\ninherits:\n- "+key+"\n"), &hf)
|
|
require.NoErrorf(t, err, "key %q should be valid", key)
|
|
assert.Equal(t, []string{key}, hf.Inherits)
|
|
}
|
|
}
|
|
|
|
func TestAllowedInherits_DefensiveCopy(t *testing.T) {
|
|
orig := AllowedInherits()
|
|
require.NotEmpty(t, orig)
|
|
|
|
// Mutating the returned slice must not affect validation, which backs onto
|
|
// the unexported allowedInherits.
|
|
mutated := AllowedInherits()
|
|
mutated[0] = "tampered"
|
|
|
|
// A genuinely valid key is still valid, the tampered value is not, and a fresh
|
|
// call still returns the pristine set.
|
|
assert.True(t, IsValidInherit("repositories"))
|
|
assert.False(t, IsValidInherit("tampered"))
|
|
assert.Equal(t, orig, AllowedInherits())
|
|
}
|
|
|
|
func TestSubHelmfileSpec_MarshalRoundTripInherits(t *testing.T) {
|
|
hf := SubHelmfileSpec{
|
|
Path: "myapp.yaml",
|
|
Inherits: []string{"repositories", "environments"},
|
|
}
|
|
out, err := yaml.Marshal(hf)
|
|
require.NoError(t, err)
|
|
|
|
var got SubHelmfileSpec
|
|
require.NoError(t, yaml.Unmarshal(out, &got))
|
|
assert.Equal(t, hf.Inherits, got.Inherits)
|
|
assert.Equal(t, hf.Path, got.Path)
|
|
}
|