fix: do not merge and import values from disabled dependencies

This is the 2nd PR splitted from #6876, and should be merged after #8677

Prior to this PR, values from disabled dependencies would still be merged and imported to the parent values. This didn't have much consequences, but is incompatible with an implemention of values templating.

This PR makes the following changes:
- `pkg/chartutil/coalesce.go` and `pkg/chartutil/dependencies.go` are no longer recursive, recursivity is handled by `pkg/engine/engine.go`
- `chartutils.ToRenderValues` does not merge chart values anymore, this is done by `pkg/engine/engine.go`
- `pkg/engine/engine.go` now uses a recursive `Engine.updateRenderValues` function that:
	- parses and merge `values.yaml`
	- validates the values along the schema
	- evaluates if sub-charts are enabled
	- recursively treat the enabled subcharts
	- import the values of the enabled subcharts
- some `pkg/actions` have been addapted to the new way of merging values

Signed-off-by: Aurélien Lambert <aure@olli-ai.com>
pull/8679/head
Aurélien Lambert 5 years ago
parent 16a89f136d
commit e5f6e39ad3
No known key found for this signature in database
GPG Key ID: 676BC3D8C0C1071B

@ -206,10 +206,6 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
i.cfg.Log("API Version list given outside of client only mode, this list will be ignored")
}
if err := chartutil.ProcessDependencies(chrt, vals); err != nil {
return nil, err
}
// Make sure if Atomic is set, that wait is set as well. This makes it so
// the user doesn't have to specify both
i.Wait = i.Wait || i.Atomic

@ -60,6 +60,12 @@ func (p *Package) Run(path string, vals map[string]interface{}) (string, error)
return "", err
}
combinedVals, err := chartutil.CoalesceRoot(ch, vals)
if err != nil {
return "", err
}
ch.Values = combinedVals
// If version is set, modify the version.
if p.Version != "" {
if err := setVersion(ch, p.Version); err != nil {

@ -198,10 +198,6 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
return nil, nil, err
}
if err := chartutil.ProcessDependencies(chart, vals); err != nil {
return nil, nil, err
}
// Increment revision count. This is passed to templates, and also stored on
// the release object.
revision := lastRelease.Version + 1
@ -445,7 +441,7 @@ func (u *Upgrade) reuseValues(chart *chart.Chart, current *release.Release, newV
u.cfg.Log("reusing the old release's values")
// We have to regenerate the old coalesced values:
oldVals, err := chartutil.CoalesceValues(current.Chart, current.Config)
oldVals, err := chartutil.CoalesceRoot(current.Chart, current.Config)
if err != nil {
return nil, errors.Wrap(err, "failed to rebuild old values")
}

@ -35,6 +35,8 @@ import (
// - A chart has access to all of the variables for it, as well as all of
// the values destined for its dependencies.
func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (map[string]interface{}, error) {
// create a copy of vals and then pass it to coalesce
// and coalesceDeps, as both will mutate the passed values
v, err := copystructure.Copy(vals)
if err != nil {
return vals, err
@ -186,6 +188,9 @@ func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} {
// values.
for key, val := range src {
if dv, ok := dst[key]; ok && dv == nil {
// When the YAML value is null, we remove the value's key.
// This allows Helm's various sources of values (value files or --set) to
// remove incompatible keys from any previous chart, file, or set values.
delete(dst, key)
} else if !ok {
dst[key] = val
@ -201,3 +206,58 @@ func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} {
}
return dst
}
// CoalesceTablesUpdate merges a source map into a destination map.
//
// src is considered authoritative.
func CoalesceTablesUpdate(dst, src map[string]interface{}) map[string]interface{} {
if dst == nil || src == nil {
return dst
}
// src values override dest values.
for key, val := range src {
// We do not remove the null values, to let value templates delete values of sub-charts
if dv, ok := dst[key]; !ok {
} else if istable(val) {
if istable(dv) {
CoalesceTablesUpdate(dv.(map[string]interface{}),
val.(map[string]interface{}))
continue
} else {
log.Printf("warning: overwriting not table with table for %s (%v)", key, dv)
}
} else if istable(dv) {
log.Printf("warning: overwriting table with non table for %s (%v)", key, dv)
}
dst[key] = val
}
return dst
}
// CoalesceDep returns the render values for subchart,
// merged with subchart values and dest global
func CoalesceDep(subchart *chart.Chart, dest map[string]interface{}) (map[string]interface{}, error) {
dv, ok := dest[subchart.Name()]
if !ok {
// If dest doesn't already have the key, create it.
dv = map[string]interface{}{}
dest[subchart.Name()] = dv
} else if !istable(dv) {
return dest, errors.Errorf("type mismatch on %s: %t", subchart.Name(), dv)
}
dvmap := dv.(map[string]interface{})
// Get globals out of dest and merge them into dvmap.
coalesceGlobals(dvmap, dest)
// Now coalesce the rest of the values.
coalesceValues(subchart, dvmap)
return dvmap, nil
}
// CoalesceRoot merges dest with chrt values,
// it returns dest for a similar behavior with CoalesceDep
func CoalesceRoot(chrt *chart.Chart, dest map[string]interface{}) (map[string]interface{}, error) {
coalesceValues(chrt, dest)
return dest, nil
}

@ -18,6 +18,7 @@ package chartutil
import (
"encoding/json"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
@ -179,8 +180,9 @@ func TestCoalesceValues(t *testing.T) {
is.Equal(valsCopy, vals)
}
func TestCoalesceTables(t *testing.T) {
dst := map[string]interface{}{
// Returns authoritative values
func getMainData() map[string]interface{} {
return map[string]interface{}{
"name": "Ishmael",
"address": map[string]interface{}{
"street": "123 Spouter Inn Ct.",
@ -193,7 +195,11 @@ func TestCoalesceTables(t *testing.T) {
"boat": "pequod",
"hole": nil,
}
src := map[string]interface{}{
}
// Returns non-authoritative values
func getSecondaryData() map[string]interface{} {
return map[string]interface{}{
"occupation": "whaler",
"address": map[string]interface{}{
"state": "MA",
@ -206,11 +212,12 @@ func TestCoalesceTables(t *testing.T) {
},
"hole": "black",
}
}
// What we expect is that anything in dst overrides anything in src, but that
// otherwise the values are coalesced.
CoalesceTables(dst, src)
// Tests the coalessing of getMainData() and getSecondaryData()
func testCoalescedData(t *testing.T, dst map[string]interface{}, cleanNil bool) {
// What we expect is that anything in getMainData() overrides anything in
// getSecondaryData(), but that otherwise the values are coalesced.
if dst["name"] != "Ishmael" {
t.Errorf("Unexpected name: %s", dst["name"])
}
@ -235,8 +242,10 @@ func TestCoalesceTables(t *testing.T) {
t.Errorf("Unexpected state: %v", addr["state"])
}
if _, ok = addr["country"]; ok {
if n, ok := addr["country"]; cleanNil && ok {
t.Error("The country is not left out.")
} else if !cleanNil && (!ok || n != nil) {
t.Error("The country is not nil.")
}
if det, ok := dst["details"].(map[string]interface{}); !ok {
@ -245,14 +254,103 @@ func TestCoalesceTables(t *testing.T) {
t.Error("Could not find your friends. Maybe you don't have any. :-(")
}
if dst["boat"].(string) != "pequod" {
if bo, ok := dst["boat"].(string); !ok {
t.Fatalf("boat is the wrong type: %v", dst["boat"])
} else if bo != "pequod" {
t.Errorf("Expected boat string, got %v", dst["boat"])
}
if _, ok = dst["hole"]; ok {
if n, ok := dst["hole"]; cleanNil && ok {
t.Error("The hole still exists.")
} else if !cleanNil && (!ok || n != nil) {
t.Error("The hole is not nil.")
}
}
func TestCoalesceTables(t *testing.T) {
dst := getMainData()
src := getSecondaryData()
CoalesceTables(dst, src)
testCoalescedData(t, dst, true)
}
func TestCoalesceTablesUpdate(t *testing.T) {
src := getMainData()
dst := getSecondaryData()
CoalesceTablesUpdate(dst, src)
testCoalescedData(t, dst, false)
}
func TestCoalesceDep(t *testing.T) {
src := map[string]interface{}{
// global object should be transferred to subchart
"global": map[string]interface{}{
"IP": "192.168.0.1",
"port": 8080,
},
// subchart object should be coallesced with chart values and returned
"subchart": getMainData(),
// any other field should be ignored
"other": map[string]interface{}{
"type": "car",
},
}
subchart := &chart.Chart{
Metadata: &chart.Metadata{
Name: "subchart",
},
Values: getSecondaryData(),
}
subchart.Values["global"] = map[string]interface{}{
"port": 80,
"service": "users",
}
dst, err := CoalesceDep(subchart, src)
if err != nil {
t.Fatal(err)
}
if d, ok := src["subchart"]; !ok {
t.Fatal("subchart went away.")
} else if dm, ok := d.(map[string]interface{}); !ok {
t.Fatalf("subchart has now wrong type: %t", d)
} else if reflect.ValueOf(dst).Pointer() != reflect.ValueOf(dm).Pointer() {
t.Error("CoalesceDep must return subchart map.")
}
testCoalescedData(t, dst, true)
glob, ok := dst["global"].(map[string]interface{})
if !ok {
t.Fatal("global went away.")
}
if glob["IP"].(string) != "192.168.0.1" {
t.Errorf("Unexpected IP: %v", glob["IP"])
}
if glob["port"].(int) != 8080 {
t.Errorf("Unexpected port: %v", glob["port"])
}
if glob["service"].(string) != "users" {
t.Errorf("Unexpected service: %v", glob["service"])
}
if _, ok := dst["other"]; ok {
t.Error("Unexpected field other.")
}
if _, ok := dst["type"]; ok {
t.Error("Unexpected field type.")
}
}
func TestCoalesceNil(t *testing.T) {
dst2 := map[string]interface{}{
"name": "Ishmael",
"address": map[string]interface{}{
@ -306,3 +404,17 @@ func TestCoalesceTables(t *testing.T) {
t.Errorf("Expected hole string, got %v", dst2["boat"])
}
}
func TestCoalesceRoot(t *testing.T) {
dst := getMainData()
chart := &chart.Chart{
Metadata: &chart.Metadata{
Name: "root",
},
Values: getSecondaryData(),
}
CoalesceRoot(chart, dst)
testCoalescedData(t, dst, true)
}

@ -22,16 +22,8 @@ import (
"helm.sh/helm/v3/pkg/chart"
)
// ProcessDependencies checks through this chart's dependencies, processing accordingly.
func ProcessDependencies(c *chart.Chart, v Values) error {
if err := processDependencyEnabled(c, v, ""); err != nil {
return err
}
return processDependencyImportValues(c)
}
// processDependencyConditions disables charts based on condition path value in values
func processDependencyConditions(reqs []*chart.Dependency, cvals Values, cpath string) {
func processDependencyConditions(reqs []*chart.Dependency, cvals Values) {
if reqs == nil {
return
}
@ -39,7 +31,7 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals Values, cpath s
for _, c := range strings.Split(strings.TrimSpace(r.Condition), ",") {
if len(c) > 0 {
// retrieve value
vv, err := cvals.PathValue(cpath + c)
vv, err := cvals.PathValue(c)
if err == nil {
// if not bool, warn
if bv, ok := vv.(bool); ok {
@ -58,18 +50,14 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals Values, cpath s
}
// processDependencyTags disables charts based on tags in values
func processDependencyTags(reqs []*chart.Dependency, cvals Values) {
if reqs == nil {
return
}
vt, err := cvals.Table("tags")
if err != nil {
func processDependencyTags(reqs []*chart.Dependency, tags map[string]interface{}) {
if reqs == nil || tags == nil {
return
}
for _, r := range reqs {
var hasTrue, hasFalse bool
for _, k := range r.Tags {
if b, ok := vt[k]; ok {
if b, ok := tags[k]; ok {
// if not bool, warn
if bv, ok := b.(bool); ok {
if bv {
@ -90,6 +78,14 @@ func processDependencyTags(reqs []*chart.Dependency, cvals Values) {
}
}
func GetTags(cvals Values) map[string]interface{} {
vt, err := cvals.Table("tags")
if err != nil {
return nil
}
return vt
}
func getAliasDependency(charts []*chart.Chart, dep *chart.Dependency) *chart.Chart {
for _, c := range charts {
if c == nil {
@ -114,8 +110,8 @@ func getAliasDependency(charts []*chart.Chart, dep *chart.Dependency) *chart.Cha
return nil
}
// processDependencyEnabled removes disabled charts from dependencies
func processDependencyEnabled(c *chart.Chart, v map[string]interface{}, path string) error {
// ProcessDependencyEnabled removes disabled charts from dependencies
func ProcessDependencyEnabled(c *chart.Chart, v map[string]interface{}, tags map[string]interface{}) error {
if c.Metadata.Dependencies == nil {
return nil
}
@ -150,13 +146,9 @@ Loop:
for _, lr := range c.Metadata.Dependencies {
lr.Enabled = true
}
cvals, err := CoalesceValues(c, v)
if err != nil {
return err
}
// flag dependencies as enabled/disabled
processDependencyTags(c.Metadata.Dependencies, cvals)
processDependencyConditions(c.Metadata.Dependencies, cvals, path)
processDependencyTags(c.Metadata.Dependencies, tags)
processDependencyConditions(c.Metadata.Dependencies, v)
// make a map of charts to remove
rm := map[string]struct{}{}
for _, r := range c.Metadata.Dependencies {
@ -181,14 +173,6 @@ Loop:
cdMetadata = append(cdMetadata, n)
}
}
// recursively call self to process sub dependencies
for _, t := range cd {
subpath := path + t.Metadata.Name + "."
if err := processDependencyEnabled(t, cvals, subpath); err != nil {
return err
}
}
// set the correct dependencies in metadata
c.Metadata.Dependencies = nil
c.Metadata.Dependencies = append(c.Metadata.Dependencies, cdMetadata...)
@ -217,70 +201,47 @@ func set(path []string, data map[string]interface{}) map[string]interface{} {
}
// processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field.
func processImportValues(c *chart.Chart) error {
func processImportValues(c *chart.Chart, cvals Values) error {
if c.Metadata.Dependencies == nil {
return nil
}
// combine chart values and empty config to get Values
var cvals Values
cvals, err := CoalesceValues(c, nil)
if err != nil {
return err
}
b := make(map[string]interface{})
// import values from each dependency if specified in import-values
for _, r := range c.Metadata.Dependencies {
var outiv []interface{}
for _, riv := range r.ImportValues {
var child, parent string
switch iv := riv.(type) {
case map[string]interface{}:
child := iv["child"].(string)
parent := iv["parent"].(string)
outiv = append(outiv, map[string]string{
"child": child,
"parent": parent,
})
// get child table
vv, err := cvals.Table(r.Name + "." + child)
if err != nil {
log.Printf("Warning: ImportValues missing table from chart %s: %v", r.Name, err)
continue
}
// create value map from child to be merged into parent
b = CoalesceTables(cvals, pathToMap(parent, vv))
child = iv["child"].(string)
parent = iv["parent"].(string)
case string:
child := "exports." + iv
outiv = append(outiv, map[string]string{
"child": child,
"parent": ".",
})
vm, err := cvals.Table(r.Name + "." + child)
if err != nil {
log.Printf("Warning: ImportValues missing table: %v", err)
continue
}
b = CoalesceTables(b, vm)
child = "exports." + iv
parent = "."
}
// get child table
vv, err := cvals.Table(r.Name + "." + child)
if err != nil {
log.Printf("Warning: ImportValues missing table %s from chart %s: %v", child, r.Name, err)
continue
}
// create value map from child to be merged into parent
CoalesceTables(cvals, pathToMap(parent, vv))
}
// set our formatted import values
r.ImportValues = outiv
}
// set the new values
c.Values = CoalesceTables(b, cvals)
return nil
}
// processDependencyImportValues imports specified chart values from child to parent.
func processDependencyImportValues(c *chart.Chart) error {
// ProcessDependencyImportValues imports specified chart values from child to parent.
//
// v is expected to have existing path for every sub chart
func ProcessDependencyImportValues(c *chart.Chart, v map[string]interface{}) error {
for _, d := range c.Dependencies() {
// recurse
if err := processDependencyImportValues(d); err != nil {
dv := v[d.Name()].(map[string]interface{})
if err := ProcessDependencyImportValues(d, dv); err != nil {
return err
}
}
return processImportValues(c)
return processImportValues(c, v)
}

@ -17,6 +17,7 @@ package chartutil
import (
"os"
"path/filepath"
"reflect"
"sort"
"strconv"
"testing"
@ -61,6 +62,35 @@ func TestLoadDependency(t *testing.T) {
check(c.Lock.Dependencies)
}
// recProcessDependencyEnabled is mostly a simplified version of
// Engine.recUpdateRenderValues, for testing only dependencies
func recProcessDependencyEnabled(c *chart.Chart, v map[string]interface{}, tags map[string]interface{}) error {
// get the local values
var err error
if c.IsRoot() {
v, err = CoalesceRoot(c, v)
tags = GetTags(v)
} else {
v, err = CoalesceDep(c, v)
}
if err != nil {
return err
}
// Remove all disabled dependencies
err = ProcessDependencyEnabled(c, v, tags)
if err != nil {
return err
}
// Recursive upudate on enabled dependencies
for _, child := range c.Dependencies() {
err = recProcessDependencyEnabled(child, v, tags)
if err != nil {
return err
}
}
return nil
}
func TestDependencyEnabled(t *testing.T) {
type M = map[string]interface{}
tests := []struct {
@ -116,7 +146,7 @@ func TestDependencyEnabled(t *testing.T) {
for _, tc := range tests {
c := loadChart(t, "testdata/subpop")
t.Run(tc.name, func(t *testing.T) {
if err := processDependencyEnabled(c, tc.v, ""); err != nil {
if err := recProcessDependencyEnabled(c, tc.v, nil); err != nil {
t.Fatalf("error processing enabled dependencies %v", err)
}
@ -212,12 +242,16 @@ func TestProcessDependencyImportValues(t *testing.T) {
e["SCBexported2A"] = "blaster"
e["global.SC1exported2.all.SC1exported3"] = "SC1expstr"
if err := processDependencyImportValues(c); err != nil {
var cvals Values
cvals, err := CoalesceValues(c, nil)
if err != nil {
t.Fatalf("coalescing values %v", err)
}
if err := ProcessDependencyImportValues(c, cvals); err != nil {
t.Fatalf("processing import values dependencies %v", err)
}
cc := Values(c.Values)
for kk, vv := range e {
pv, err := cc.PathValue(kk)
pv, err := cvals.PathValue(kk)
if err != nil {
t.Fatalf("retrieving import values table %v %v", kk, err)
}
@ -243,7 +277,12 @@ func TestProcessDependencyImportValuesForEnabledCharts(t *testing.T) {
c := loadChart(t, "testdata/import-values-from-enabled-subchart/parent-chart")
nameOverride := "parent-chart-prod"
if err := processDependencyImportValues(c); err != nil {
cvals, err := CoalesceValues(c, nil)
if err != nil {
t.Fatalf("coalescing values %v", err)
}
if err := ProcessDependencyImportValues(c, cvals); err != nil {
t.Fatalf("processing import values dependencies %v", err)
}
@ -251,7 +290,7 @@ func TestProcessDependencyImportValuesForEnabledCharts(t *testing.T) {
t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
}
if err := processDependencyEnabled(c, c.Values, ""); err != nil {
if err := recProcessDependencyEnabled(c, c.Values, nil); err != nil {
t.Fatalf("expected no errors but got %q", err)
}
@ -315,7 +354,7 @@ func TestDependentChartAliases(t *testing.T) {
t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
}
if err := processDependencyEnabled(c, c.Values, ""); err != nil {
if err := ProcessDependencyEnabled(c, c.Values, GetTags(c.Values)); err != nil {
t.Fatalf("expected no errors but got %q", err)
}
@ -336,7 +375,7 @@ func TestDependentChartWithSubChartsAbsentInDependency(t *testing.T) {
t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
}
if err := processDependencyEnabled(c, c.Values, ""); err != nil {
if err := ProcessDependencyEnabled(c, c.Values, GetTags(c.Values)); err != nil {
t.Fatalf("expected no errors but got %q", err)
}
@ -373,7 +412,7 @@ func TestDependentChartsWithSubchartsAllSpecifiedInDependency(t *testing.T) {
t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
}
if err := processDependencyEnabled(c, c.Values, ""); err != nil {
if err := ProcessDependencyEnabled(c, c.Values, GetTags(c.Values)); err != nil {
t.Fatalf("expected no errors but got %q", err)
}
@ -393,7 +432,7 @@ func TestDependentChartsWithSomeSubchartsSpecifiedInDependency(t *testing.T) {
t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
}
if err := processDependencyEnabled(c, c.Values, ""); err != nil {
if err := ProcessDependencyEnabled(c, c.Values, GetTags(c.Values)); err != nil {
t.Fatalf("expected no errors but got %q", err)
}
@ -405,3 +444,33 @@ func TestDependentChartsWithSomeSubchartsSpecifiedInDependency(t *testing.T) {
t.Fatalf("expected 1 dependency specified in Chart.yaml, got %d", len(c.Metadata.Dependencies))
}
}
func TestGetTags(t *testing.T) {
type M = map[string]interface{}
tests := []struct {
name string
vals M
tags M
}{{
"normal tags",
M{"tags": M{"a": true, "b": false}},
M{"a": true, "b": false},
}, {
"not an object tags",
M{"tags": []interface{}{"a", "b"}},
nil,
}, {
"no tags",
M{"no_tags": M{"a": true, "b": false}},
nil,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tags := GetTags(tt.vals)
if !reflect.DeepEqual(tags, tt.tags) {
t.Fatalf("tags map do not match got %v, expected %v", tags, tt.tags)
}
})
}
}

@ -17,11 +17,11 @@ limitations under the License.
package chartutil
import (
"fmt"
"io"
"io/ioutil"
"strings"
"github.com/mitchellh/copystructure"
"github.com/pkg/errors"
"sigs.k8s.io/yaml"
@ -134,6 +134,7 @@ func ToRenderValues(chrt *chart.Chart, chrtVals map[string]interface{}, options
top := map[string]interface{}{
"Chart": chrt.Metadata,
"Capabilities": caps,
"Values": nil,
"Release": map[string]interface{}{
"Name": options.Name,
"Namespace": options.Namespace,
@ -144,17 +145,17 @@ func ToRenderValues(chrt *chart.Chart, chrtVals map[string]interface{}, options
},
}
vals, err := CoalesceValues(chrt, chrtVals)
if err != nil {
return top, err
}
if err := ValidateAgainstSchema(chrt, vals); err != nil {
errFmt := "values don't meet the specifications of the schema(s) in the following chart(s):\n%s"
return top, fmt.Errorf(errFmt, err.Error())
// if we have an empty map, make sure it is initialized
if chrtVals == nil {
top["Values"] = map[string]interface{}{}
} else {
vals, err := copystructure.Copy(chrtVals)
if err != nil {
return top, err
}
top["Values"] = vals.(map[string]interface{})
}
top["Values"] = vals
return top, nil
}

@ -141,9 +141,10 @@ func TestToRenderValues(t *testing.T) {
}
where := vals["where"].(map[string]interface{})
expects := map[string]string{
"city": "Baghdad",
"date": "809 CE",
"title": "caliph",
"city": "Baghdad",
"date": "809 CE",
// ToRenderValues no longer coallesce chart values
// "title": "caliph",
}
for field, expect := range expects {
if got := where[field]; got != expect {

@ -64,6 +64,11 @@ type Engine struct {
// section contains a value named "bar", that value will be passed on to the
// bar chart during render time.
func (e Engine) Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) {
// update values and dependencies
if err := e.updateRenderValues(chrt, values); err != nil {
return nil, err
}
// parse templates with the updated values
tmap := allTemplates(chrt, values)
return e.render(tmap)
}
@ -297,6 +302,75 @@ func cleanupExecError(filename string, err error) error {
return err
}
// updateRenderValues update render values with chart values.
func (e Engine) updateRenderValues(c *chart.Chart, vals chartutil.Values) error {
var sb strings.Builder
// update values and dependencies
if err := e.recUpdateRenderValues(c, vals, nil, &sb); err != nil {
return err
}
// Check for values validation errors
if sb.Len() > 0 {
errFmt := "values don't meet the specifications of the schema(s) in the following chart(s):\n%s"
return fmt.Errorf(errFmt, sb.String())
}
// import values from dependenvies
if err := chartutil.ProcessDependencyImportValues(c, vals["Values"].(map[string]interface{})); err != nil {
return err
}
return nil
}
func (e Engine) recUpdateRenderValues(c *chart.Chart, vals chartutil.Values, tags map[string]interface{}, sb *strings.Builder) error {
next := map[string]interface{}{
"Chart": c.Metadata,
"Files": newFiles(c.Files),
"Release": vals["Release"],
"Capabilities": vals["Capabilities"],
"Values": nil,
}
// If there is a {{.Values.ThisChart}} in the parent metadata,
// copy that into the {{.Values}} for this template.
var nvals map[string]interface{}
var err error
if c.IsRoot() {
nvals, err = chartutil.CoalesceRoot(c, vals["Values"].(map[string]interface{}))
} else {
nvals, err = chartutil.CoalesceDep(c, vals["Values"].(map[string]interface{}))
}
if err != nil {
return err
}
next["Values"] = nvals
// Get validations errors of chart values
if c.Schema != nil {
err = chartutil.ValidateAgainstSingleSchema(nvals, c.Schema)
if err != nil {
sb.WriteString(fmt.Sprintf("%s:\n", c.Name()))
sb.WriteString(err.Error())
}
}
// Get tags of the root
if c.IsRoot() {
tags = chartutil.GetTags(nvals)
}
// Remove all disabled dependencies
err = chartutil.ProcessDependencyEnabled(c, nvals, tags)
if err != nil {
return err
}
// Recursive upudate on enabled dependencies
for _, child := range c.Dependencies() {
err = e.recUpdateRenderValues(child, next, tags, sb)
if err != nil {
return err
}
}
return nil
}
func sortTemplates(tpls map[string]renderable) []string {
keys := make([]string, len(tpls))
i := 0

@ -18,11 +18,13 @@ package engine
import (
"fmt"
"sort"
"strings"
"sync"
"testing"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil"
)
@ -153,7 +155,9 @@ func TestRenderRefsOrdering(t *testing.T) {
}
for i := 0; i < 100; i++ {
out, err := Render(parentChart, chartutil.Values{})
out, err := Render(parentChart, chartutil.Values{
"Values": map[string]interface{}{},
})
if err != nil {
t.Fatalf("Failed to render templates: %s", err)
}
@ -333,7 +337,9 @@ func TestRenderDependency(t *testing.T) {
},
})
out, err := Render(ch, map[string]interface{}{})
out, err := Render(ch, map[string]interface{}{
"Values": map[string]interface{}{},
})
if err != nil {
t.Fatalf("failed to render chart: %s", err)
}
@ -738,3 +744,311 @@ func TestRenderRecursionLimit(t *testing.T) {
}
}
func TestUpdateRenderValues_dependencies(t *testing.T) {
values := map[string]interface{}{}
rv := map[string]interface{}{
"Release": map[string]interface{}{
"Name": "Test Name",
},
"Values": values,
}
c := loadChart(t, "testdata/dependencies")
if err := new(Engine).updateRenderValues(c, rv); err != nil {
t.Fatal(err)
}
// check for conditions
if vm, ok := values["condition_true"]; !ok {
t.Errorf("chart 'condition_true' not evaluated")
} else {
m := vm.(map[string]interface{})
if v, ok := m["evaluated"]; !ok || !v.(bool) {
t.Errorf("chart 'condition_true' not evaluated")
}
}
if _, ok := values["condition_false"]; ok {
t.Errorf("chart 'condition_false' evaluated")
}
if vm, ok := values["condition_null"]; !ok {
t.Errorf("chart 'condition_null' not evaluated")
} else {
m := vm.(map[string]interface{})
if v, ok := m["evaluated"]; !ok || !v.(bool) {
t.Errorf("chart 'condition_null' not evaluated")
}
}
// check for tags
if vm, ok := values["tags_true"]; !ok {
t.Errorf("chart 'tags_true' not evaluated")
} else {
m := vm.(map[string]interface{})
if v, ok := m["evaluated"]; !ok || !v.(bool) {
t.Errorf("chart 'tags_true' not evaluated")
}
}
if _, ok := values["tags_false"]; ok {
t.Errorf("chart 'tags_false' evaluated")
}
// check for sub tags
if vm, ok := values["tags_sub"]; !ok {
t.Errorf("chart 'tags_sub' not evaluated")
} else {
m := vm.(map[string]interface{})
if v, ok := m["evaluated"]; !ok || !v.(bool) {
t.Errorf("chart 'tags_sub' not evaluated")
}
if vm, ok := m["tags_sub_true"]; !ok {
t.Errorf("chart 'tags_sub/tags_sub_true' not evaluated")
} else {
m := vm.(map[string]interface{})
if v, ok := m["evaluated"]; !ok || !v.(bool) {
t.Errorf("chart 'tags_sub/tags_sub_true' not evaluated")
}
}
if _, ok := m["tags_sub/tags_sub_false"]; ok {
t.Errorf("chart 'tags_sub/tags_sub_false' evaluated")
}
}
// check for import-values
if vm, ok := values["import_values"]; !ok {
t.Errorf("chart 'import_values' not evaluated")
} else {
m := vm.(map[string]interface{})
if v, ok := m["evaluated"]; !ok || !v.(bool) {
t.Errorf("chart 'import_values' not evaluated")
}
}
if vm, ok := values["importValues"]; !ok {
t.Errorf("value 'importValues' not imported")
} else {
m := vm.(map[string]interface{})
if v, ok := m["imported"]; !ok || !v.(bool) {
t.Errorf("value 'importValues.imported' not imported")
}
}
if vm, ok := values["subImport"]; !ok {
t.Errorf("value 'subImport' not imported")
} else {
m := vm.(map[string]interface{})
if v, ok := m["old"]; !ok {
t.Errorf("value 'subImport.old' not imported")
} else if vs, ok := v.(string); !ok || vs != "values.yaml" {
t.Errorf("wrong 'subImport.old' imported: %v", v)
}
}
names := extractChartNames(c)
except := []string{
"parentchart",
"parentchart.condition_null",
"parentchart.condition_true",
"parentchart.import_values",
"parentchart.tags_sub",
"parentchart.tags_sub.tags_sub_true",
"parentchart.tags_true",
}
if len(names) != len(except) {
t.Errorf("dependencies values do not match got %v, expected %v", names, except)
} else {
for i := range names {
if names[i] != except[i] {
t.Errorf("dependencies values do not match got %v, expected %v", names, except)
break
}
}
}
}
// copied from chartutil/values_test.go:TestToRenderValues
// because ToRenderValues no longer coalesces chart values
func TestUpdateRenderValues_ToRenderValues(t *testing.T) {
chartValues := map[string]interface{}{
"name": "al Rashid",
"where": map[string]interface{}{
"city": "Basrah",
"title": "caliph",
},
}
overideValues := map[string]interface{}{
"name": "Haroun",
"where": map[string]interface{}{
"city": "Baghdad",
"date": "809 CE",
"title": "caliph",
},
}
c := &chart.Chart{
Metadata: &chart.Metadata{Name: "test"},
Templates: []*chart.File{},
Values: chartValues,
Files: []*chart.File{
{Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")},
},
}
c.AddDependency(&chart.Chart{
Metadata: &chart.Metadata{Name: "where"},
})
o := chartutil.ReleaseOptions{
Name: "Seven Voyages",
Namespace: "default",
Revision: 1,
IsInstall: true,
}
res, err := chartutil.ToRenderValues(c, overideValues, o, nil)
if err != nil {
t.Fatal(err)
}
if err = new(Engine).updateRenderValues(c, res); err != nil {
t.Fatal(err)
}
// Ensure that the top-level values are all set.
if name := res["Chart"].(*chart.Metadata).Name; name != "test" {
t.Errorf("Expected chart name 'test', got %q", name)
}
relmap := res["Release"].(map[string]interface{})
if name := relmap["Name"]; name.(string) != "Seven Voyages" {
t.Errorf("Expected release name 'Seven Voyages', got %q", name)
}
if namespace := relmap["Namespace"]; namespace.(string) != "default" {
t.Errorf("Expected namespace 'default', got %q", namespace)
}
if revision := relmap["Revision"]; revision.(int) != 1 {
t.Errorf("Expected revision '1', got %d", revision)
}
if relmap["IsUpgrade"].(bool) {
t.Error("Expected upgrade to be false.")
}
if !relmap["IsInstall"].(bool) {
t.Errorf("Expected install to be true.")
}
if !res["Capabilities"].(*chartutil.Capabilities).APIVersions.Has("v1") {
t.Error("Expected Capabilities to have v1 as an API")
}
if res["Capabilities"].(*chartutil.Capabilities).KubeVersion.Major != "1" {
t.Error("Expected Capabilities to have a Kube version")
}
vals := res["Values"].(map[string]interface{})
if vals["name"] != "Haroun" {
t.Errorf("Expected 'Haroun', got %q (%v)", vals["name"], vals)
}
where := vals["where"].(map[string]interface{})
expects := map[string]string{
"city": "Baghdad",
"date": "809 CE",
"title": "caliph",
}
for field, expect := range expects {
if got := where[field]; got != expect {
t.Errorf("Expected %q, got %q (%v)", expect, got, where)
}
}
}
// copied from chartutil/dependencies_test.go:TestDependencyEnabled
// because ProcessDependencyEnabled is no longer recursive
func TestUpdateRenderValues_TestDependencyEnabled(t *testing.T) {
type M = map[string]interface{}
tests := []struct {
name string
v M
e []string // expected charts including duplicates in alphanumeric order
}{{
"tags with no effect",
M{"tags": M{"nothinguseful": false}},
[]string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb"},
}, {
"tags disabling a group",
M{"tags": M{"front-end": false}},
[]string{"parentchart"},
}, {
"tags disabling a group and enabling a different group",
M{"tags": M{"front-end": false, "back-end": true}},
[]string{"parentchart", "parentchart.subchart2", "parentchart.subchart2.subchartb", "parentchart.subchart2.subchartc"},
}, {
"tags disabling only children, children still enabled since tag front-end=true in values.yaml",
M{"tags": M{"subcharta": false, "subchartb": false}},
[]string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb"},
}, {
"tags disabling all parents/children with additional tag re-enabling a parent",
M{"tags": M{"front-end": false, "subchart1": true, "back-end": false}},
[]string{"parentchart", "parentchart.subchart1"},
}, {
"conditions enabling the parent charts, but back-end (b, c) is still disabled via values.yaml",
M{"subchart1": M{"enabled": true}, "subchart2": M{"enabled": true}},
[]string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb", "parentchart.subchart2"},
}, {
"conditions disabling the parent charts, effectively disabling children",
M{"subchart1": M{"enabled": false}, "subchart2": M{"enabled": false}},
[]string{"parentchart"},
}, {
"conditions a child using the second condition path of child's condition",
M{"subchart1": M{"subcharta": M{"enabled": false}}},
[]string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subchartb"},
}, {
"tags enabling a parent/child group with condition disabling one child",
M{"subchart2": M{"subchartc": M{"enabled": false}}, "tags": M{"back-end": true}},
[]string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb", "parentchart.subchart2", "parentchart.subchart2.subchartb"},
}, {
"tags will not enable a child if parent is explicitly disabled with condition",
M{"subchart1": M{"enabled": false}, "tags": M{"front-end": true}},
[]string{"parentchart"},
}, {
"subcharts with alias also respect conditions",
M{"subchart1": M{"enabled": false}, "subchart2alias": M{"enabled": true, "subchartb": M{"enabled": true}}},
[]string{"parentchart", "parentchart.subchart2alias", "parentchart.subchart2alias.subchartb"},
}}
for _, tc := range tests {
c := loadChart(t, "../chartutil/testdata/subpop")
vals := map[string]interface{}{"Values": tc.v}
t.Run(tc.name, func(t *testing.T) {
if err := new(Engine).updateRenderValues(c, vals); err != nil {
t.Fatalf("error processing enabled dependencies %v", err)
}
names := extractChartNames(c)
if len(names) != len(tc.e) {
t.Fatalf("slice lengths do not match got %v, expected %v", len(names), len(tc.e))
}
for i := range names {
if names[i] != tc.e[i] {
t.Fatalf("slice values do not match got %v, expected %v", names, tc.e)
}
}
})
}
}
// copied from chartutil/dependencies_test.go:loadChart
func loadChart(t *testing.T, path string) *chart.Chart {
t.Helper()
c, err := loader.Load(path)
if err != nil {
t.Fatalf("failed to load testdata: %s", err)
}
return c
}
// copied from chartutil/dependencies_test.go:extractChartNames
// extractCharts recursively searches chart dependencies returning all charts found
func extractChartNames(c *chart.Chart) []string {
var out []string
var fn func(c *chart.Chart)
fn = func(c *chart.Chart) {
out = append(out, c.ChartPath())
for _, d := range c.Dependencies() {
fn(d)
}
}
fn(c)
sort.Strings(out)
return out
}

@ -0,0 +1,37 @@
apiVersion: v2
description: A Helm chart for Kubernetes
name: parentchart
version: 0.1.0
dependencies:
- name: condition_true
repository: http://localhost:10191
version: 0.1.0
condition: condition.true
- name: condition_false
repository: http://localhost:10191
version: 0.1.0
condition: condition.false
- name: condition_null
repository: http://localhost:10191
version: 0.1.0
condition: condition.null
- name: tags_true
repository: http://localhost:10191
version: 0.1.0
tags:
- true_tag
- name: tags_false
repository: http://localhost:10191
version: 0.1.0
tags:
- false_tag
- name: import_values
repository: http://localhost:10191
version: 0.1.0
import-values:
- child: importValues
parent: importValues
- child: importTemplate
parent: importTemplate
- child: import
parent: subImport

@ -0,0 +1,4 @@
apiVersion: v2
description: A Helm chart for Kubernetes
name: condition_false
version: 0.1.0

@ -0,0 +1,4 @@
apiVersion: v2
description: A Helm chart for Kubernetes
name: condition_null
version: 0.1.0

@ -0,0 +1,4 @@
apiVersion: v2
description: A Helm chart for Kubernetes
name: condition_true
version: 0.1.0

@ -0,0 +1,4 @@
apiVersion: v2
description: A Helm chart for Kubernetes
name: import_values
version: 0.1.0

@ -0,0 +1,6 @@
evaluated: true
import:
old: "values.yaml"
common: "values.yaml"
importValues:
imported: true

@ -0,0 +1,4 @@
apiVersion: v2
description: A Helm chart for Kubernetes
name: tags_false
version: 0.1.0

@ -0,0 +1,15 @@
apiVersion: v2
description: A Helm chart for Kubernetes
name: tags_sub
version: 0.1.0
dependencies:
- name: tags_sub_true
repository: http://localhost:10191
version: 0.1.0
tags:
- true_tag
- name: tags_sub_false
repository: http://localhost:10191
version: 0.1.0
tags:
- false_tag

@ -0,0 +1,4 @@
apiVersion: v2
description: A Helm chart for Kubernetes
name: tags_sub_false
version: 0.1.0

@ -0,0 +1,4 @@
apiVersion: v2
description: A Helm chart for Kubernetes
name: tags_sub_true
version: 0.1.0

@ -0,0 +1,4 @@
apiVersion: v2
description: A Helm chart for Kubernetes
name: tags_true
version: 0.1.0

@ -0,0 +1,7 @@
condition:
"true": true
"false": false
"null": null
tags:
true_tag: true
false_tag: false
Loading…
Cancel
Save