mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-10 14:46:15 +02:00
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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user