From 0e81899f5f4f95fcef385bf84fee1edb288abaec Mon Sep 17 00:00:00 2001 From: Justin Scott Date: Tue, 14 Mar 2017 11:04:33 -0700 Subject: [PATCH] WIP feat(helm): import child values to parent Implements a mechanism in requirements.yaml to allow the import and re-parenting of value table from child chart. Closes #1995 --- pkg/chartutil/requirements.go | 126 ++++++++++++++++++ pkg/chartutil/requirements_test.go | 57 ++++++++ .../subchart1/charts/subchartA/values.yaml | 12 +- .../subpop/charts/subchart1/requirements.yaml | 6 + .../subpop/charts/subchart1/values.yaml | 11 +- .../testdata/subpop/requirements.yaml | 5 + pkg/chartutil/testdata/subpop/values.yaml | 14 ++ pkg/helm/client.go | 8 ++ pkg/resolver/resolver_test.go | 2 +- 9 files changed, 232 insertions(+), 9 deletions(-) diff --git a/pkg/chartutil/requirements.go b/pkg/chartutil/requirements.go index 3a31042d6..326afb36c 100644 --- a/pkg/chartutil/requirements.go +++ b/pkg/chartutil/requirements.go @@ -62,6 +62,8 @@ type Dependency struct { Tags []string `json:"tags"` // Enabled bool determines if chart should be loaded Enabled bool `json:"enabled"` + // ImportValues holds the mapping of source values to parent key to be imported + ImportValues []interface{} `json:"import-values"` } // ErrNoRequirementsFile to detect error condition @@ -266,3 +268,127 @@ func ProcessRequirementsEnabled(c *chart.Chart, v *chart.Config) error { return nil } + +// pathToMap creates a nested map given a YAML path in dot notation +func pathToMap(path string, data map[string]interface{}) map[string]interface{} { + ap := strings.Split(path, ".") + n := []map[string]interface{}{} + for _, v := range ap { + nm := make(map[string]interface{}) + nm[v] = make(map[string]interface{}) + n = append(n, nm) + } + for i, d := range n { + for k := range d { + z := i + 1 + if z == len(n) { + n[i][k] = data + break + } + n[i][k] = n[z] + } + } + + return n[0] +} + +// getParents returns a slice of parent charts in reverse order +func getParents(c *chart.Chart, out []*chart.Chart) []*chart.Chart { + if len(out) == 0 { + out = []*chart.Chart{} + out = append(out, c) + } + for _, ch := range c.Dependencies { + if len(ch.Dependencies) > 0 { + out = append(out, ch) + out = getParents(ch, out) + } + } + + return out +} + +// processImportValues merges values from child to parent based on ImportValues field +func processImportValues(c *chart.Chart, v *chart.Config) error { + reqs, err := LoadRequirements(c) + if err != nil { + log.Printf("Warning: ImportValues cannot load requirements for %s", c.Metadata.Name) + return nil + } + cvals, err := CoalesceValues(c, v) + nv := v.GetValues() + b := make(map[string]interface{}) + for kk, v3 := range nv { + b[kk] = v3 + } + for _, r := range reqs.Dependencies { + if len(r.ImportValues) > 0 { + var outiv []interface{} + for _, riv := range r.ImportValues { + switch tr := riv.(type) { + case map[string]interface{}: + if m, ok := riv.(map[string]interface{}); ok { + nm := make(map[string]string) + nm["child"] = m["child"].(string) + nm["parent"] = m["parent"].(string) + outiv = append(outiv, nm) + s := r.Name + "." + nm["child"] + vv, err := cvals.Table(s) + if err != nil { + log.Printf("Warning: ImportValues missing table %v", err) + continue + } + if nm["parent"] == "." { + coalesceTables(b, vv.AsMap()) + } else { + + vm := pathToMap(nm["parent"], vv.AsMap()) + coalesceTables(b, vm) + } + } + case string: + log.Printf("its a string %v", tr) + // todo validation + nm := make(map[string]string) + nm["child"] = riv.(string) + nm["parent"] = "." + outiv = append(outiv, nm) + s := r.Name + "." + nm["child"] + vv, err := cvals.Table(s) + if err != nil { + log.Printf("Warning: ImportValues missing table %v", err) + continue + } + coalesceTables(b, vv.AsMap()) + } + } + // set our formatted import values + r.ImportValues = outiv + } + } + + cv, err := coalesceValues(c, b) + if err != nil { + log.Fatalf("Error coalescing values for ImportValues %s", err) + } + y, err := yaml.Marshal(cv) + if err != nil { + log.Printf("Warning: ImportValues could not marshall %v", err) + } + bb := &chart.Config{Raw: string(y)} + v = bb + c.Values = bb + + return nil +} + +// ProcessRequirementsImportValues imports specified chart values from child to parent +func ProcessRequirementsImportValues(c *chart.Chart, v *chart.Config) error { + pc := getParents(c, nil) + for i := len(pc) - 1; i >= 0; i-- { + processImportValues(pc[i], v) + + } + + return nil +} diff --git a/pkg/chartutil/requirements_test.go b/pkg/chartutil/requirements_test.go index b9a5ae12a..21268df9d 100644 --- a/pkg/chartutil/requirements_test.go +++ b/pkg/chartutil/requirements_test.go @@ -18,6 +18,8 @@ import ( "sort" "testing" + "strconv" + "k8s.io/helm/pkg/proto/hapi/chart" ) @@ -206,3 +208,58 @@ func extractCharts(c *chart.Chart, out []*chart.Chart) []*chart.Chart { } return out } +func TestProcessRequirementsImportValues(t *testing.T) { + c, err := Load("testdata/subpop") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + + v := &chart.Config{Raw: ""} + + e := make(map[string]string) + e["imported-from-chart1.type"] = "ClusterIP" + e["imported-from-chart1.name"] = "nginx" + e["imported-from-chart1.externalPort"] = "80" + // this doesn't exist in imported table. it should merge and remain unchanged + e["imported-from-chart1.notimported1"] = "1" + e["imported-from-chartA-via-chart1.limits.cpu"] = "300m" + e["imported-from-chartA-via-chart1.limits.memory"] = "300Mi" + e["imported-from-chartA-via-chart1.limits.volume"] = "11" + e["imported-from-chartA-via-chart1.requests.truthiness"] = "0.01" + + verifyRequirementsImportValues(t, c, v, e) +} +func verifyRequirementsImportValues(t *testing.T, c *chart.Chart, v *chart.Config, e map[string]string) { + + err := ProcessRequirementsImportValues(c, v) + if err != nil { + t.Errorf("Error processing import values requirements %v", err) + } + cv := c.GetValues() + cc, err := ReadValues([]byte(cv.Raw)) + if err != nil { + t.Errorf("Error reading import values %v", err) + } + for kk, vv := range e { + pv, err := cc.PathValue(kk) + if err != nil { + t.Fatalf("Error retrieving import values table %v %v", kk, err) + return + } + + switch pv.(type) { + case float64: + s := strconv.FormatFloat(pv.(float64), 'f', -1, 64) + if s != vv { + t.Errorf("Failed to match imported float value %v with expected %v", s, vv) + return + } + default: + if pv.(string) != vv { + t.Errorf("Failed to match imported string value %v with expected %v", pv, vv) + return + } + } + + } +} 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 5e5b21065..8c348e86c 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml @@ -1,6 +1,7 @@ # Default values for subchart. # This is a YAML-formatted file. # Declare variables to be passed into your templates. +# subchartA replicaCount: 1 image: repository: nginx @@ -13,9 +14,12 @@ service: internalPort: 80 resources: limits: - cpu: 100m - memory: 128Mi + cpu: 300m + memory: 300Mi + plasticity: 1.7331 + volume: 11 requests: - cpu: 100m - memory: 128Mi + cpu: 350m + memory: 350Mi + truthiness: 0.01 diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/requirements.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/requirements.yaml index 94d278234..5adf2f10a 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart1/requirements.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/requirements.yaml @@ -6,6 +6,12 @@ dependencies: tags: - front-end - subcharta + import-values: + - child: resources.limits + parent: imported-from-chartA.limits + - child: resources.requests + parent: imported-from-chartA.requests + - name: subchartb repository: http://localhost:10191 version: 0.1.0 diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/values.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/values.yaml index 5e5b21065..4c5085d82 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart1/values.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/values.yaml @@ -1,6 +1,7 @@ # Default values for subchart. # This is a YAML-formatted file. # Declare variables to be passed into your templates. +# subchart1 replicaCount: 1 image: repository: nginx @@ -13,9 +14,11 @@ service: internalPort: 80 resources: limits: - cpu: 100m - memory: 128Mi + cpu: 200m + memory: 200Mi + plasticity: 0 requests: - cpu: 100m - memory: 128Mi + cpu: 250m + memory: 250Mi + truthiness: 200 diff --git a/pkg/chartutil/testdata/subpop/requirements.yaml b/pkg/chartutil/testdata/subpop/requirements.yaml index 9840e047d..19d320889 100644 --- a/pkg/chartutil/testdata/subpop/requirements.yaml +++ b/pkg/chartutil/testdata/subpop/requirements.yaml @@ -6,6 +6,11 @@ dependencies: tags: - front-end - subchart1 + import-values: + - child: service + parent: imported-from-chart1 + - child: imported-from-chartA + parent: imported-from-chartA-via-chart1 - name: subchart2 repository: http://localhost:10191 version: 0.1.0 diff --git a/pkg/chartutil/testdata/subpop/values.yaml b/pkg/chartutil/testdata/subpop/values.yaml index 85fc7b49d..f03c449cf 100644 --- a/pkg/chartutil/testdata/subpop/values.yaml +++ b/pkg/chartutil/testdata/subpop/values.yaml @@ -1,6 +1,20 @@ # parent/values.yaml # switch-like +imported-from-chart1: + name: bathtubginx + type: None + externalPort: 25 + notimported1: 1 + +imported-from-chartA-via-chart1: + limits: + cpu: 100m + memory: 100Mi + notimported2: 100 + requests: + truthiness: 33.3 + tags: front-end: true back-end: false diff --git a/pkg/helm/client.go b/pkg/helm/client.go index 2e773cca7..82c87e0da 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -97,6 +97,10 @@ func (h *Client) InstallReleaseFromChart(chart *chart.Chart, ns string, opts ... if err != nil { return nil, err } + err = chartutil.ProcessRequirementsImportValues(req.Chart, req.Values) + if err != nil { + return nil, err + } return h.install(ctx, req) } @@ -166,6 +170,10 @@ func (h *Client) UpdateReleaseFromChart(rlsName string, chart *chart.Chart, opts if err != nil { return nil, err } + err = chartutil.ProcessRequirementsImportValues(req.Chart, req.Values) + if err != nil { + return nil, err + } return h.update(ctx, req) } diff --git a/pkg/resolver/resolver_test.go b/pkg/resolver/resolver_test.go index 8d4b86019..5f0811f20 100644 --- a/pkg/resolver/resolver_test.go +++ b/pkg/resolver/resolver_test.go @@ -141,7 +141,7 @@ func TestResolve(t *testing.T) { } func TestHashReq(t *testing.T) { - expect := "sha256:c8250374210bd909cef274be64f871bd4e376d4ecd34a1589b5abf90b68866ba" + expect := "sha256:1feffe2016ca113f64159d91c1f77d6a83bcd23510b171d9264741bf9d63f741" req := &chartutil.Requirements{ Dependencies: []*chartutil.Dependency{ {Name: "alpine", Version: "0.1.0", Repository: "http://localhost:8879/charts"},