feat: add defaultInherit for automatic release template inheritance (#2600)

* feat: add defaultInherit for automatic release template inheritance

Add a top-level defaultInherit field to helmfile.yaml that automatically
applies template inheritance to all releases without requiring explicit
inherit on each release.

The field accepts a single template name as a string or a list of
template names. Releases that already explicitly inherit from the same
template are not duplicated.

Fixes #2599

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

* style: fix gci formatting in app_template_test.go

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

* fix: correct relative chart path in integration test

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

* fix: use absolute chart path in bad-helmfile test

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

* fix: use clean chart path in bad-helmfile test

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

* fix: use dir variable for chart path

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

* test: fix flaky defaultInherit integration assertions

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/d0884e8e-8b1b-456d-8250-dec1566b8a37

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* test: tighten defaultInherit integration assertions

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/d0884e8e-8b1b-456d-8250-dec1566b8a37

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* test: harden release block parsing in issue-2599 case

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/d0884e8e-8b1b-456d-8250-dec1566b8a37

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* test: make issue-2599 assertions format-tolerant

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/d0884e8e-8b1b-456d-8250-dec1566b8a37

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* test: fix section extraction and regex matching in issue-2599 case

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/d0884e8e-8b1b-456d-8250-dec1566b8a37

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* fix: sanitize defaultInherit values and dedupe applied templates

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/85a8e815-3701-4b48-a28d-6bb2d50a3b40

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* chore: address validation feedback on defaultInherit fixes

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/85a8e815-3701-4b48-a28d-6bb2d50a3b40

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* fix: sanitize releaseInherit entries in applyDefaultInherit; add cleanup trap and quote vars in integration test

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/1fbf62d5-7ce2-42e5-898b-30151c0c1ef9

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* refactor: combine releaseInherit loops in applyDefaultInherit to avoid double TrimSpace; clarify test comment

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/1fbf62d5-7ce2-42e5-898b-30151c0c1ef9

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* test: align default inherit tests with yaml wrapper and assertions

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/3ea9b8e4-633f-43c4-899f-e063ec576486

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* test: address review feedback on defaultInherit tests

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/3ea9b8e4-633f-43c4-899f-e063ec576486

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* test: fix issue-2599 integration script helmfile invocation

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/9452bb65-7086-459f-b5ae-0b00c1e021eb

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
yxxhero
2026-05-20 18:21:03 +08:00
committed by GitHub
co-authored by copilot-swe-agent[bot]
parent 2f2e8617ad
commit 781d28a47a
10 changed files with 666 additions and 1 deletions
+1
View File
@@ -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 |
+42
View File
@@ -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`.
+236
View File
@@ -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"`,
})
})
}
+44
View File
@@ -95,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
@@ -514,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.
+37 -1
View File
@@ -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
}
+210
View File
@@ -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 {
@@ -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])
}
}
})
}
})
}
}
+1
View File
@@ -144,6 +144,7 @@ ${kubectl} create namespace ${test_ns} || fail "Could not create namespace ${tes
. ${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 -----------------------------------------------------------------------------------------------------------
@@ -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