From c1f5ddd63828e8f7310c82199a3e9154a13e4c4c Mon Sep 17 00:00:00 2001 From: Ilya Lesikov Date: Tue, 24 Aug 2021 20:00:02 +0300 Subject: [PATCH] feat: implement export-values Signed-off-by: Ilya Lesikov Signed-off-by: Sumit Kulhadia Co-authored-by: Ralf Pannemans Co-authored-by: Sumit Kulhadia Co-authored-by: Philipp Stehle Co-authored-by: Pavel Busko --- pkg/chart/dependency.go | 3 + pkg/chartutil/dependencies.go | 220 ++++++++++++++---- pkg/chartutil/dependencies_test.go | 66 +++++- pkg/chartutil/testdata/subpop/Chart.yaml | 13 ++ .../subpop/charts/subchart1/Chart.yaml | 4 + .../subchart1/charts/subchartA/values.yaml | 11 + .../subpop/charts/subchart1/values.yaml | 21 +- .../subpop/charts/subchart2/values.yaml | 1 - pkg/chartutil/testdata/subpop/values.yaml | 22 ++ 9 files changed, 304 insertions(+), 57 deletions(-) diff --git a/pkg/chart/dependency.go b/pkg/chart/dependency.go index 4ef5eeb32..56be0c026 100644 --- a/pkg/chart/dependency.go +++ b/pkg/chart/dependency.go @@ -45,6 +45,9 @@ type Dependency struct { // ImportValues holds the mapping of source values to parent key to be imported. Each item can be a // string or pair of child/parent sublist items. ImportValues []interface{} `json:"import-values,omitempty"` + // ExportValues holds the mapping of parent values to child key to be exported. Each item can be a + // string or pair of parent/child sublist items. + ExportValues []interface{} `json:"export-values,omitempty"` // Alias usable alias to be used for the chart Alias string `json:"alias,omitempty"` } diff --git a/pkg/chartutil/dependencies.go b/pkg/chartutil/dependencies.go index c38a8b6c4..49c102fa0 100644 --- a/pkg/chartutil/dependencies.go +++ b/pkg/chartutil/dependencies.go @@ -16,6 +16,7 @@ limitations under the License. package chartutil import ( + "fmt" "log" "strings" @@ -31,7 +32,7 @@ func ProcessDependencies(c *chart.Chart, v Values) error { if err := processDependencyEnabled(c, v, ""); err != nil { return err } - return processDependencyImportValues(c, false) + return processDependencyImportExportValues(c, false) } // ProcessDependenciesWithMerge checks through this chart's dependencies, processing accordingly. @@ -41,7 +42,8 @@ func ProcessDependenciesWithMerge(c *chart.Chart, v Values) error { if err := processDependencyEnabled(c, v, ""); err != nil { return err } - return processDependencyImportValues(c, true) + + return processDependencyImportExportValues(c, true) } // processDependencyConditions disables charts based on condition path value in values @@ -233,8 +235,9 @@ func set(path []string, data map[string]interface{}) map[string]interface{} { return cur } -// processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field. -func processImportValues(c *chart.Chart, merge bool) error { +// processImportExportValues merges values between child and parent based on the chart's dependencies' ImportValues +// and ExportValues fields. +func processImportExportValues(c *chart.Chart, merge bool) error { if c.Metadata.Dependencies == nil { return nil } @@ -250,51 +253,16 @@ func processImportValues(c *chart.Chart, merge bool) error { return err } b := make(map[string]interface{}) - // import values from each dependency if specified in import-values + // For each dependency export values from parent or import values from child if export-values or import-values + // specified. for _, r := range c.Metadata.Dependencies { - var outiv []interface{} - for _, riv := range r.ImportValues { - 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 - if merge { - b = MergeTables(b, pathToMap(parent, vv.AsMap())) - } else { - b = CoalesceTables(b, pathToMap(parent, vv.AsMap())) - } - 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 - } - if merge { - b = MergeTables(b, vm.AsMap()) - } else { - b = CoalesceTables(b, vm.AsMap()) - } - } + if merge { + b = MergeTables(b, getExportedValues(c, r, cvals, merge)) + b = MergeTables(b, getImportedValues(r, cvals, merge)) + } else { + b = CoalesceTables(b, getExportedValues(c, r, cvals, merge)) + b = CoalesceTables(b, getImportedValues(r, cvals, merge)) } - r.ImportValues = outiv } // Imported values from a child to a parent chart have a higher priority than @@ -345,13 +313,163 @@ func trimNilValues(vals map[string]interface{}) map[string]interface{} { return valsCopyMap } -// processDependencyImportValues imports specified chart values from child to parent. -func processDependencyImportValues(c *chart.Chart, merge bool) error { +// Generates values map which is imported from specified child to parent via import-values. +func getImportedValues(r *chart.Dependency, cvals Values, merge bool) map[string]interface{} { + b := make(map[string]interface{}) + var outiv []interface{} + for _, riv := range r.ImportValues { + 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 + if merge { + b = MergeTables(b, pathToMap(parent, vv.AsMap())) + } else { + b = CoalesceTables(b, pathToMap(parent, vv.AsMap())) + } + 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 + } + if merge { + b = MergeTables(b, vm.AsMap()) + } else { + b = CoalesceTables(b, vm.AsMap()) + } + } + } + // set our formatted import values + r.ImportValues = outiv + return b +} + +// Generates values map which is exported from parent to specified child via export-values. +func getExportedValues(c *chart.Chart, r *chart.Dependency, cvals Values, merge bool) map[string]interface{} { + b := make(map[string]interface{}) + var exportValues []interface{} + for _, rev := range r.ExportValues { + parent, child, err := parseExportValues(rev, r) + if err != nil { + log.Printf("Warning: invalid ExportValues defined in chart %q for its dependency %q: %s", c.Name(), r.Name, err) + continue + } + + exportValues = append(exportValues, map[string]string{ + "parent": parent, + "child": child, + }) + + var childValMap map[string]interface{} + // try to get parent table + vm, err := cvals.Table(parent) + if err == nil { + childValMap = pathToMap(child, vm.AsMap()) + } else { + // still it might be not a table but a simple value + value, e := cvals.PathValue(parent) + if e != nil { + log.Printf("Warning: ExportValues defined in chart %q for its dependency %q can't get the parent path: %v", c.Name(), r.Name, err) + continue + } + + childSlice := parsePath(child) + childLen := len(childSlice) + if childLen == 1 { + log.Printf("Warning: in ExportValues defined in chart %q for its dependency %q you are trying to assign a primitive data type (string, int, etc) to the root of your dependent chart values. We will ignore this ExportValues, because this is most likely not what you want. Fix the ExportValues to hide this warning.", c.Name(), r.Name) + continue + } + + childValMap = pathToMap( + joinPath(childSlice[:childLen-1]...), + map[string]interface{}{ + childSlice[childLen-1]: value, + }, + ) + } + + // merge new map with values exported to the child into the resulting values map + if merge { + b = MergeTables(childValMap, b) + } else { + b = CoalesceTables(childValMap, b) + } + } + // set formatted export values + r.ExportValues = exportValues + + return b +} + +// Parse and validate export-values. +func parseExportValues(rev interface{}, r *chart.Dependency) (string, string, error) { + var parent, child string + + switch ev := rev.(type) { + case map[string]interface{}: + var ok bool + parent, ok = ev["parent"].(string) + if !ok { + return "", "", fmt.Errorf("parent must be a string") + } + child, ok = ev["child"].(string) + if !ok { + return "", "", fmt.Errorf("child must be a string") + } + + switch parent { + case "", ".": + return "", "", fmt.Errorf("parent %q is not allowed", parent) + } + + switch child { + case "", ".": + child = r.Name + default: + child = r.Name + "." + child + } + case string: + switch parent = ev; parent { + case "", ".": + parent = "exports" + default: + parent = "exports." + parent + } + child = r.Name + default: + return "", "", fmt.Errorf("invalid type of ExportValues") + } + + return parent, child, nil +} + +// processDependencyImportExportValues imports (child to parent) and/or exports (parent to child) values if +// import-values or export-values specified for the dependency. +func processDependencyImportExportValues(c *chart.Chart, merge bool) error { for _, d := range c.Dependencies() { // recurse - if err := processDependencyImportValues(d, merge); err != nil { + if err := processDependencyImportExportValues(d, merge); err != nil { return err } } - return processImportValues(c, merge) + return processImportExportValues(c, merge) } diff --git a/pkg/chartutil/dependencies_test.go b/pkg/chartutil/dependencies_test.go index 34ae12f95..d1ab75361 100644 --- a/pkg/chartutil/dependencies_test.go +++ b/pkg/chartutil/dependencies_test.go @@ -218,7 +218,7 @@ func TestProcessDependencyImportValues(t *testing.T) { e["SCBexported2A"] = "blaster" e["global.SC1exported2.all.SC1exported3"] = "SC1expstr" - if err := processDependencyImportValues(c, false); err != nil { + if err := processDependencyImportExportValues(c, false); err != nil { t.Fatalf("processing import values dependencies %v", err) } cc := Values(c.Values) @@ -258,7 +258,7 @@ func TestProcessDependencyImportValues(t *testing.T) { } c = loadChart(t, "testdata/subpop") - if err := processDependencyImportValues(c, true); err != nil { + if err := processDependencyImportExportValues(c, true); err != nil { t.Fatalf("processing import values dependencies %v", err) } cc = Values(c.Values) @@ -294,7 +294,7 @@ func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) { e["app2.service.port"] = "9090" e["app3.service.port"] = "9090" e["app4.service.port"] = "1234" - if err := processDependencyImportValues(c, true); err != nil { + if err := processDependencyImportExportValues(c, true); err != nil { t.Fatalf("processing import values dependencies %v", err) } cc := Values(c.Values) @@ -317,11 +317,69 @@ func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) { } } +func TestProcessDependencyExportValues(t *testing.T) { + chart := loadChart(t, "testdata/subpop") + + expectation := make(map[string]interface{}) + + // single value export + expectation["subchart1.single-value-parent"] = "exported-from-parent-7" + + // merge with overrides + expectation["subchart1.merged-values.SC1bool"] = true + expectation["subchart1.merged-values.SPExtra11"] = "should-be-unchanged" + expectation["subchart1.merged-values.SC1float"] = 22.2 + expectation["subchart1.merged-values.SC1int"] = float64(222) + expectation["subchart1.merged-values.SC1string"] = "exported-from-parent-2" + + // merge exports + expectation["subchart1.merged-values.SPNested1.SPExtra19"] = "exported-from-parent-6" + + // `exports` style + expectation["subchart1.merged-values.SPExtra9"] = "exported-from-parent-4" + expectation["subchart1.merged-values.SPExtra12"] = "should-be-unchanged" + + // passed from child to grandchild with overrides + expectation["subchart1.subcharta.exported-overridden-chart1.SCAbool"] = true + expectation["subchart1.subcharta.exported-overridden-chart1.SCAfloat"] = 33.3 + expectation["subchart1.subcharta.exported-overridden-chart1.SCAint"] = float64(333) + expectation["subchart1.subcharta.exported-overridden-chart1.SCAstring"] = "exported-from-chart1" + expectation["subchart1.subcharta.exported-overridden-chart1.SPExtra13"] = "exported-from-chart1-2" + expectation["subchart1.subcharta.exported-overridden-chart1.SPExtra15"] = "should-be-unchanged" + + // passed through from parent to grandchild + expectation["subchart1.subcharta.merged-values.SPExtra17"] = "exported-from-parent-5" + expectation["subchart1.subcharta.merged-values.SPExtra18"] = "should-be-unchanged" + expectation["subchart1.subcharta.merged-values.SPExtra19"] = "exported-from-parent-9" //overwritten + + // passed from child to grandchild, `exports` style + expectation["subchart1.subcharta.merged-values.SPExtra14"] = "exported-from-chart1-3" + expectation["subchart1.subcharta.merged-values.SPExtra16"] = "should-be-unchanged" + + // exporting everything + expectation["subchart1.explicit-export1.merged-values.SPExtra9"] = "exported-from-parent-4" + + if err := ProcessDependencies(chart, Values{}); err != nil { + t.Fatalf("processing dependencies failed: %v", err) + } + chartValues := Values(chart.Values) + for path, expectedValue := range expectation { + actualValue, err := chartValues.PathValue(path) + if err != nil { + t.Fatalf("retrieving export values table %v %v", path, err) + } + + if actualValue != expectedValue { + t.Errorf("failed to match exported string value %q with expected %v", actualValue, expectedValue) + } + } +} + func TestProcessDependencyImportValuesForEnabledCharts(t *testing.T) { c := loadChart(t, "testdata/import-values-from-enabled-subchart/parent-chart") nameOverride := "parent-chart-prod" - if err := processDependencyImportValues(c, true); err != nil { + if err := processDependencyImportExportValues(c, true); err != nil { t.Fatalf("processing import values dependencies %v", err) } diff --git a/pkg/chartutil/testdata/subpop/Chart.yaml b/pkg/chartutil/testdata/subpop/Chart.yaml index 27118672a..d1157fb38 100644 --- a/pkg/chartutil/testdata/subpop/Chart.yaml +++ b/pkg/chartutil/testdata/subpop/Chart.yaml @@ -2,7 +2,9 @@ apiVersion: v1 description: A Helm chart for Kubernetes name: parentchart version: 0.1.0 + dependencies: + - name: subchart1 repository: http://localhost:10191 version: 0.1.0 @@ -25,6 +27,17 @@ dependencies: parent: . - SCBexported2 - SC1exported1 + export-values: + - parent: values-for-the-child + child: merged-values + - parent: values-for-the-child-2 + child: merged-values + - parent: exported-single-value + child: single-value-parent + - parent: values-for-the-grandchild + child: subcharta.merged-values + - explicit-export1 + - "" - name: subchart2 repository: http://localhost:10191 diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/Chart.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/Chart.yaml index 9d8c03ee1..bda5a355a 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart1/Chart.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/Chart.yaml @@ -17,6 +17,10 @@ dependencies: parent: overridden-chartA - child: SCAdata parent: imported-chartA-B + export-values: + - parent: exported-overridden-chart1 + child: exported-overridden-chart1 + - explicit-child-export - name: subchartb repository: http://localhost:10191 diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml index f0381ae6a..329c5dfdb 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml @@ -15,3 +15,14 @@ SCAdata: SCAnested1: SCAnested2: true +exported-overridden-chart1: + SCAbool: true + SCAfloat: 33.3 + SCAint: 333 + SCAstring: "exported-from-chart1" + SPExtra15: "should-be-unchanged" + +merged-values: + SPExtra16: "should-be-unchanged" + SPExtra18: "should-be-unchanged" + SPExtra19: "should-be-overwritten" diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/values.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/values.yaml index a974e316a..65dc14ed4 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart1/values.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/values.yaml @@ -29,6 +29,22 @@ overridden-chartA: imported-chartA-B: SC1extra5: "tiller" + +merged-values: + SPExtra11: "should-be-unchanged" + SPExtra12: "should-be-unchanged" + SC1bool: false + SC1float: 11.1 + SC1int: 111 + SC1string: "should-be-overridden" + +exported-overridden-chart1: + SCAbool: true + SCAfloat: 33.3 + SCAint: 333 + SCAstring: "exported-from-chart1" + SPExtra13: "exported-from-chart1-2" + overridden-chartA-B: SCAbool: true SCAfloat: 3.33 @@ -52,4 +68,7 @@ exports: global: SC1exported2: all: - SC1exported3: "SC1expstr" \ No newline at end of file + SC1exported3: "SC1expstr" + explicit-child-export: + merged-values: + SPExtra14: "exported-from-chart1-3" diff --git a/pkg/chartutil/testdata/subpop/charts/subchart2/values.yaml b/pkg/chartutil/testdata/subpop/charts/subchart2/values.yaml index 5e5b21065..fad554f74 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart2/values.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart2/values.yaml @@ -18,4 +18,3 @@ resources: requests: cpu: 100m memory: 128Mi - diff --git a/pkg/chartutil/testdata/subpop/values.yaml b/pkg/chartutil/testdata/subpop/values.yaml index ba70ed406..0bc3242c3 100644 --- a/pkg/chartutil/testdata/subpop/values.yaml +++ b/pkg/chartutil/testdata/subpop/values.yaml @@ -35,6 +35,28 @@ overridden-chartA-B: SCBstring: "jango" SPextra6: 111 +values-for-the-child: + SC1bool: true + SC1float: 22.2 + SC1int: 222 + SC1string: "exported-from-parent-2" + +values-for-the-child-2: + SPNested1: + SPExtra19: "exported-from-parent-6" + + +exported-single-value: "exported-from-parent-7" + +values-for-the-grandchild: + SPExtra17: "exported-from-parent-5" + SPExtra19: "exported-from-parent-9" + +exports: + explicit-export1: + merged-values: + SPExtra9: "exported-from-parent-4" + tags: front-end: true back-end: false