feat: inherit field for release template inheritance (#606)

* feat: `inherit` field for release template inheritance

Ref https://github.com/helmfile/helmfile/issues/435#issuecomment-1364749414

Signed-off-by: Yusuke Kuoka <ykuoka@gmail.com>

* Fix wording

Signed-off-by: Yusuke Kuoka <ykuoka@gmail.com>

* Comment on releaseWithInheritedTemplate

Signed-off-by: Yusuke Kuoka <ykuoka@gmail.com>

* Update Release Template doc with the new `inherit` feature

Signed-off-by: Yusuke Kuoka <ykuoka@gmail.com>

* Fix a typo in code comment

Signed-off-by: Yusuke Kuoka <ykuoka@gmail.com>

Signed-off-by: Yusuke Kuoka <ykuoka@gmail.com>
This commit is contained in:
Yusuke Kuoka
2022-12-28 10:01:04 +09:00
committed by GitHub
parent 6b19577987
commit 490bb5d147
9 changed files with 324 additions and 14 deletions
+91
View File
@@ -392,3 +392,94 @@ releases:
})
})
}
func TestTemplate_CyclicInheritance(t *testing.T) {
type testcase struct {
ns string
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": `
templates:
a:
inherit:
template: b
values:
- a.yaml
b:
inherit:
template: c
values:
- b.yaml
c:
inherit:
template: a
values:
- c.yaml
releases:
- name: app1
inherit:
template: a
chart: incubator/raw
`,
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: &ffs.FileSystem{Glob: filepath.Glob},
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},
valsRuntime: valsRuntime,
}, files)
if tc.ns != "" {
app.Namespace = tc.ns
}
tmplErr := app.Template(applyConfig{
// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
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 cyclic inheritance", func(t *testing.T) {
check(t, testcase{
error: `in ./helmfile.yaml: failed executing release templates in "helmfile.yaml": unable to load release "app1" with template: cyclic inheritance detected: a->b->c->a`,
})
})
}
+8
View File
@@ -198,6 +198,11 @@ type RepositorySpec struct {
SkipTLSVerify string `yaml:"skipTLSVerify,omitempty"`
}
type Inherit struct {
Template string `yaml:"template,omitempty"`
Except []string `yaml:"except,omitempty"`
}
// ReleaseSpec defines the structure of a helm release
type ReleaseSpec struct {
// Chart is the name of the chart being installed to create this release
@@ -340,6 +345,9 @@ type ReleaseSpec struct {
// Propagate '--post-renderer' to helmv3 template and helm install
PostRenderer *string `yaml:"postRenderer,omitempty"`
// Inherit is used to inherit a release template from a release or another release template
Inherit Inherit `yaml:"inherit,omitempty"`
}
// ChartPathOrName returns ChartPath if it is non-empty, and returns Chart otherwise.
+92 -2
View File
@@ -1,8 +1,12 @@
package state
import (
"errors"
"fmt"
"reflect"
"strings"
"github.com/imdario/mergo"
"github.com/helmfile/helmfile/pkg/tmpl"
"github.com/helmfile/helmfile/pkg/yaml"
@@ -89,7 +93,15 @@ func (st *HelmState) ExecuteTemplates() (*HelmState, error) {
vals := st.Values()
for i, rt := range st.Releases {
release := rt
release, err := st.releaseWithInheritedTemplate(&rt, nil)
if err != nil {
var cyclicInheritanceErr CyclicReleaseTemplateInheritanceError
if errors.As(err, &cyclicInheritanceErr) {
return nil, fmt.Errorf("unable to load release %q with template: %w", rt.Name, cyclicInheritanceErr)
}
return nil, err
}
if release.KubeContext == "" {
release.KubeContext = r.HelmDefaults.KubeContext
}
@@ -107,7 +119,7 @@ func (st *HelmState) ExecuteTemplates() (*HelmState, error) {
}
successFlag := false
for it, prev := 0, &release; it < 6; it++ {
for it, prev := 0, release; it < 6; it++ {
tmplData := st.createReleaseTemplateData(prev, vals)
renderer := tmpl.NewFileRenderer(st.fs, st.basePath, tmplData)
r, err := release.ExecuteTemplateExpressions(renderer)
@@ -132,3 +144,81 @@ func (st *HelmState) ExecuteTemplates() (*HelmState, error) {
return &r, nil
}
type CyclicReleaseTemplateInheritanceError struct {
Message string
}
func (e CyclicReleaseTemplateInheritanceError) Error() string {
return e.Message
}
// releaseWithInheritedTemplate generates a new ReleaseSpec from a ReleaseSpec, by recursively inheriting
// release templates referenced by the spec's `inherit` field.
// The third parameter retains the current state of the recursive call, to detect a cyclic dependency a.k.a
// a cyclic relese template inheritance.
// This functions fails with a CyclicReleaseTemplateInheritanceError if it finds a cyclic inheritance.
func (st *HelmState) releaseWithInheritedTemplate(r *ReleaseSpec, inheritancePath []string) (*ReleaseSpec, error) {
templateName := r.Inherit.Template
if templateName == "" {
return r, nil
}
path := append([]string{}, inheritancePath...)
path = append(path, templateName)
var cycleFound bool
for _, t := range inheritancePath {
if t == templateName {
cycleFound = true
break
}
}
if cycleFound {
return nil, CyclicReleaseTemplateInheritanceError{Message: fmt.Sprintf("cyclic inheritance detected: %s", strings.Join(path, "->"))}
}
template, defined := st.Templates[templateName]
if !defined {
return nil, fmt.Errorf("release %q tried to inherit inexistent release template %q", r.Name, templateName)
}
src, err := st.releaseWithInheritedTemplate(&template.ReleaseSpec, path)
if err != nil {
return nil, fmt.Errorf("unable to load release template %q: %w", templateName, err)
}
for _, k := range r.Inherit.Except {
switch k {
case "labels":
src.Labels = map[string]string{}
case "values":
src.Values = nil
case "valuesTemplate":
src.ValuesTemplate = nil
case "setTemplate":
src.SetValuesTemplate = nil
case "set":
src.SetValues = nil
default:
return nil, fmt.Errorf("%q is not allowed under `inherit`. Allowed values are \"set\", \"setTemplate\", \"values\", \"valuesTemplate\", and \"labels\"", k)
}
st.logger.Debugf("excluded field %q when inheriting template %q to release %q", k, templateName, r.Name)
}
var merged ReleaseSpec
if err := mergo.Merge(&merged, src, mergo.WithAppendSlice, mergo.WithSliceDeepCopy); err != nil {
return nil, fmt.Errorf("unable to inherit release template %q: %w", templateName, err)
}
if err := mergo.Merge(&merged, r, mergo.WithAppendSlice, mergo.WithSliceDeepCopy); err != nil {
return nil, fmt.Errorf("unable to load release %q: %w", r.Name, err)
}
merged.Inherit = Inherit{}
return &merged, nil
}
+6 -6
View File
@@ -38,39 +38,39 @@ func TestGenerateID(t *testing.T) {
run(testcase{
subject: "baseline",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
want: "foo-values-8496665478",
want: "foo-values-648b77cdd4",
})
run(testcase{
subject: "different bytes content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: []byte(`{"k":"v"}`),
want: "foo-values-5c4468ff65",
want: "foo-values-5dfbf8fdb7",
})
run(testcase{
subject: "different map content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: map[string]interface{}{"k": "v"},
want: "foo-values-7b656f7c67",
want: "foo-values-7565d47dd9",
})
run(testcase{
subject: "different chart",
release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"},
want: "foo-values-675b4dffc9",
want: "foo-values-7c4f76c445",
})
run(testcase{
subject: "different name",
release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"},
want: "bar-values-5fb8b9599",
want: "bar-values-644fb47865",
})
run(testcase{
subject: "specific ns",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"},
want: "myns-foo-values-64948d6f45",
want: "myns-foo-values-c5ddcc795",
})
for id, n := range ids {