diff --git a/pkg/action/install.go b/pkg/action/install.go index 00fb208b0..b9efc243f 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -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 diff --git a/pkg/action/package.go b/pkg/action/package.go index 0a927cd41..69845d23f 100644 --- a/pkg/action/package.go +++ b/pkg/action/package.go @@ -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 { diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index e7c2aec25..e46844246 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -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") } diff --git a/pkg/chartutil/coalesce.go b/pkg/chartutil/coalesce.go index 4ff957ea0..3423693f2 100644 --- a/pkg/chartutil/coalesce.go +++ b/pkg/chartutil/coalesce.go @@ -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 +} diff --git a/pkg/chartutil/coalesce_test.go b/pkg/chartutil/coalesce_test.go index 13dbb4f6c..112d07376 100644 --- a/pkg/chartutil/coalesce_test.go +++ b/pkg/chartutil/coalesce_test.go @@ -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) +} diff --git a/pkg/chartutil/dependencies.go b/pkg/chartutil/dependencies.go index 6fae607f5..c97657f87 100644 --- a/pkg/chartutil/dependencies.go +++ b/pkg/chartutil/dependencies.go @@ -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) } diff --git a/pkg/chartutil/dependencies_test.go b/pkg/chartutil/dependencies_test.go index 342d7fe87..9707c2818 100644 --- a/pkg/chartutil/dependencies_test.go +++ b/pkg/chartutil/dependencies_test.go @@ -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) + } + }) + } +} diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go index 4e618e4ca..893648f80 100644 --- a/pkg/chartutil/values.go +++ b/pkg/chartutil/values.go @@ -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 } diff --git a/pkg/chartutil/values_test.go b/pkg/chartutil/values_test.go index 5338f42be..237847503 100644 --- a/pkg/chartutil/values_test.go +++ b/pkg/chartutil/values_test.go @@ -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 { diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index a78e7eafb..cacd4b136 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -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 diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index 1bb9aa4b2..745cf256f 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -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 +} diff --git a/pkg/engine/testdata/dependencies/Chart.yaml b/pkg/engine/testdata/dependencies/Chart.yaml new file mode 100644 index 000000000..135783344 --- /dev/null +++ b/pkg/engine/testdata/dependencies/Chart.yaml @@ -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 diff --git a/pkg/engine/testdata/dependencies/charts/condition_false/Chart.yaml b/pkg/engine/testdata/dependencies/charts/condition_false/Chart.yaml new file mode 100644 index 000000000..97d99b10f --- /dev/null +++ b/pkg/engine/testdata/dependencies/charts/condition_false/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +description: A Helm chart for Kubernetes +name: condition_false +version: 0.1.0 diff --git a/pkg/engine/testdata/dependencies/charts/condition_false/values.yaml b/pkg/engine/testdata/dependencies/charts/condition_false/values.yaml new file mode 100644 index 000000000..b1b9f96a5 --- /dev/null +++ b/pkg/engine/testdata/dependencies/charts/condition_false/values.yaml @@ -0,0 +1 @@ +evaluated: true diff --git a/pkg/engine/testdata/dependencies/charts/condition_null/Chart.yaml b/pkg/engine/testdata/dependencies/charts/condition_null/Chart.yaml new file mode 100644 index 000000000..1ee319d98 --- /dev/null +++ b/pkg/engine/testdata/dependencies/charts/condition_null/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +description: A Helm chart for Kubernetes +name: condition_null +version: 0.1.0 diff --git a/pkg/engine/testdata/dependencies/charts/condition_null/values.yaml b/pkg/engine/testdata/dependencies/charts/condition_null/values.yaml new file mode 100644 index 000000000..b1b9f96a5 --- /dev/null +++ b/pkg/engine/testdata/dependencies/charts/condition_null/values.yaml @@ -0,0 +1 @@ +evaluated: true diff --git a/pkg/engine/testdata/dependencies/charts/condition_true/Chart.yaml b/pkg/engine/testdata/dependencies/charts/condition_true/Chart.yaml new file mode 100644 index 000000000..056d53ceb --- /dev/null +++ b/pkg/engine/testdata/dependencies/charts/condition_true/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +description: A Helm chart for Kubernetes +name: condition_true +version: 0.1.0 diff --git a/pkg/engine/testdata/dependencies/charts/condition_true/values.yaml b/pkg/engine/testdata/dependencies/charts/condition_true/values.yaml new file mode 100644 index 000000000..b1b9f96a5 --- /dev/null +++ b/pkg/engine/testdata/dependencies/charts/condition_true/values.yaml @@ -0,0 +1 @@ +evaluated: true diff --git a/pkg/engine/testdata/dependencies/charts/import_values/Chart.yaml b/pkg/engine/testdata/dependencies/charts/import_values/Chart.yaml new file mode 100644 index 000000000..0dd82bdea --- /dev/null +++ b/pkg/engine/testdata/dependencies/charts/import_values/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +description: A Helm chart for Kubernetes +name: import_values +version: 0.1.0 diff --git a/pkg/engine/testdata/dependencies/charts/import_values/values.yaml b/pkg/engine/testdata/dependencies/charts/import_values/values.yaml new file mode 100644 index 000000000..043397fc1 --- /dev/null +++ b/pkg/engine/testdata/dependencies/charts/import_values/values.yaml @@ -0,0 +1,6 @@ +evaluated: true +import: + old: "values.yaml" + common: "values.yaml" +importValues: + imported: true diff --git a/pkg/engine/testdata/dependencies/charts/tags_false/Chart.yaml b/pkg/engine/testdata/dependencies/charts/tags_false/Chart.yaml new file mode 100644 index 000000000..c2a160441 --- /dev/null +++ b/pkg/engine/testdata/dependencies/charts/tags_false/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +description: A Helm chart for Kubernetes +name: tags_false +version: 0.1.0 diff --git a/pkg/engine/testdata/dependencies/charts/tags_false/values.yaml b/pkg/engine/testdata/dependencies/charts/tags_false/values.yaml new file mode 100644 index 000000000..b1b9f96a5 --- /dev/null +++ b/pkg/engine/testdata/dependencies/charts/tags_false/values.yaml @@ -0,0 +1 @@ +evaluated: true diff --git a/pkg/engine/testdata/dependencies/charts/tags_sub/Chart.yaml b/pkg/engine/testdata/dependencies/charts/tags_sub/Chart.yaml new file mode 100644 index 000000000..bda28393d --- /dev/null +++ b/pkg/engine/testdata/dependencies/charts/tags_sub/Chart.yaml @@ -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 diff --git a/pkg/engine/testdata/dependencies/charts/tags_sub/charts/tags_sub_false/Chart.yaml b/pkg/engine/testdata/dependencies/charts/tags_sub/charts/tags_sub_false/Chart.yaml new file mode 100644 index 000000000..cb6ff80d4 --- /dev/null +++ b/pkg/engine/testdata/dependencies/charts/tags_sub/charts/tags_sub_false/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +description: A Helm chart for Kubernetes +name: tags_sub_false +version: 0.1.0 diff --git a/pkg/engine/testdata/dependencies/charts/tags_sub/charts/tags_sub_false/values.yaml b/pkg/engine/testdata/dependencies/charts/tags_sub/charts/tags_sub_false/values.yaml new file mode 100644 index 000000000..b1b9f96a5 --- /dev/null +++ b/pkg/engine/testdata/dependencies/charts/tags_sub/charts/tags_sub_false/values.yaml @@ -0,0 +1 @@ +evaluated: true diff --git a/pkg/engine/testdata/dependencies/charts/tags_sub/charts/tags_sub_true/Chart.yaml b/pkg/engine/testdata/dependencies/charts/tags_sub/charts/tags_sub_true/Chart.yaml new file mode 100644 index 000000000..02dc90e37 --- /dev/null +++ b/pkg/engine/testdata/dependencies/charts/tags_sub/charts/tags_sub_true/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +description: A Helm chart for Kubernetes +name: tags_sub_true +version: 0.1.0 diff --git a/pkg/engine/testdata/dependencies/charts/tags_sub/charts/tags_sub_true/values.yaml b/pkg/engine/testdata/dependencies/charts/tags_sub/charts/tags_sub_true/values.yaml new file mode 100644 index 000000000..b1b9f96a5 --- /dev/null +++ b/pkg/engine/testdata/dependencies/charts/tags_sub/charts/tags_sub_true/values.yaml @@ -0,0 +1 @@ +evaluated: true diff --git a/pkg/engine/testdata/dependencies/charts/tags_sub/values.yaml b/pkg/engine/testdata/dependencies/charts/tags_sub/values.yaml new file mode 100644 index 000000000..b1b9f96a5 --- /dev/null +++ b/pkg/engine/testdata/dependencies/charts/tags_sub/values.yaml @@ -0,0 +1 @@ +evaluated: true diff --git a/pkg/engine/testdata/dependencies/charts/tags_true/Chart.yaml b/pkg/engine/testdata/dependencies/charts/tags_true/Chart.yaml new file mode 100644 index 000000000..fe64c0b2a --- /dev/null +++ b/pkg/engine/testdata/dependencies/charts/tags_true/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +description: A Helm chart for Kubernetes +name: tags_true +version: 0.1.0 diff --git a/pkg/engine/testdata/dependencies/charts/tags_true/values.yaml b/pkg/engine/testdata/dependencies/charts/tags_true/values.yaml new file mode 100644 index 000000000..b1b9f96a5 --- /dev/null +++ b/pkg/engine/testdata/dependencies/charts/tags_true/values.yaml @@ -0,0 +1 @@ +evaluated: true diff --git a/pkg/engine/testdata/dependencies/values.yaml b/pkg/engine/testdata/dependencies/values.yaml new file mode 100644 index 000000000..9148fc233 --- /dev/null +++ b/pkg/engine/testdata/dependencies/values.yaml @@ -0,0 +1,7 @@ +condition: + "true": true + "false": false + "null": null +tags: + true_tag: true + false_tag: false