diff --git a/docs/charts.md b/docs/charts.md index b7d97cc0b..414f80bf7 100644 --- a/docs/charts.md +++ b/docs/charts.md @@ -302,6 +302,107 @@ helm install --set tags.front-end=true --set subchart2.enabled=false * The `tags:` key in values must be a top level key. Globals and nested `tags:` tables are not currently supported. +#### Importing Child Values via requirements.yaml + +In some cases it is desirable to allow a child chart's values to propagate to the parent chart and be +shared as common defaults. An additional benefit of using the `exports` format is that it will enable future +tooling to introspect user-settable values. + +The keys containing the values to be imported can be specified in the parent chart's `requirements.yaml` file +using a YAML list. Each item in the list is a key which is imported from the child chart's `exports` field. + +To import values not contained in the `exports` key, use the [child/parent](#using-the-child/parent-format) format. +Examples of both formats are described below. + +##### Using the exports format + +If a child chart's `values.yaml` file contains an `exports` field at the root, its contents may be imported +directly into the parent's values by specifying the keys to import as in the example below: + +```yaml +# parent's requirements.yaml file + ... + import-values: + - data +``` +```yaml +# child's values.yaml file +... +exports: + data: + myint: 99 +``` + +Since we are specifying the key `data` in our import list, Helm looks in the the `exports` field of the child +chart for `data` key and imports its contents. + +The final parent values would contain our exported field: + +```yaml +# parent's values file +... +myint: 99 + +``` + +Please note the parent key `data` is not contained in the parent's final values. If you need to specify the +parent key, use the 'child/parent' format. + +##### Using the child/parent format + +To access values that are not contained in the `exports` key of the child chart's values, you will need to +specify the source key of the values to be imported (`child`) and the destination path in the parent chart's +values (`parent`). + +The `import-values` in the example below instructs Helm to take any values found at `child:` path and copy them +to the parent's values at the path specified in `parent:` + +```yaml +# parent's requirements.yaml file +dependencies: + - name: subchart1 + repository: http://localhost:10191 + version: 0.1.0 + ... + import-values: + - child: default.data + parent: myimports +``` +In the above example, values found at `default.data` in the subchart1's values will be imported +to the `myimports` key in the parent chart's values as detailed below: + +```yaml +# parent's values.yaml file + +myimports: + myint: 0 + mybool: false + mystring: "helm rocks!" + +``` +```yaml +# subchart1's values.yaml file + +default: + data: + myint: 999 + mybool: true + +``` +The parent chart's resulting values would be: + +```yaml +# parent's final values + +myimports: + myint: 999 + mybool: true + mystring: "helm rocks!" + +``` + +The parent's final values now contains the `myint` and `mybool` fields imported from subchart1. + ## Templates and Values Helm Chart templates are written in the diff --git a/pkg/chartutil/requirements.go b/pkg/chartutil/requirements.go index 3a31042d6..53e28e788 100644 --- a/pkg/chartutil/requirements.go +++ b/pkg/chartutil/requirements.go @@ -62,6 +62,9 @@ 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. Each item can be a + // string or pair of child/parent sublist items. + ImportValues []interface{} `json:"import-values"` } // ErrNoRequirementsFile to detect error condition @@ -266,3 +269,128 @@ 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{} { + if path == "." { + return data + } + ap := strings.Split(path, ".") + if len(ap) == 0 { + return nil + } + n := []map[string]interface{}{} + // created nested map for each key, adding to slice + for _, v := range ap { + nm := make(map[string]interface{}) + nm[v] = make(map[string]interface{}) + n = append(n, nm) + } + // find the last key (map) and set our data + 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{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 the chart's dependencies' ImportValues field. +func processImportValues(c *chart.Chart, v *chart.Config) error { + reqs, err := LoadRequirements(c) + if err != nil { + return err + } + // combine chart values and its dependencies' values + cvals, err := CoalesceValues(c, v) + if err != nil { + return err + } + nv := v.GetValues() + b := make(map[string]interface{}, len(nv)) + // convert values to map + for kk, vvv := range nv { + b[kk] = vvv + } + // import values from each dependency if specified in import-values + for _, r := range reqs.Dependencies { + if len(r.ImportValues) > 0 { + var outiv []interface{} + for _, riv := range r.ImportValues { + switch iv := riv.(type) { + case map[string]interface{}: + nm := map[string]string{ + "child": iv["child"].(string), + "parent": iv["parent"].(string), + } + outiv = append(outiv, nm) + s := r.Name + "." + nm["child"] + // get child table + vv, err := cvals.Table(s) + if err != nil { + log.Printf("Warning: ImportValues missing table: %v", err) + continue + } + // create value map from child to be merged into parent + vm := pathToMap(nm["parent"], vv.AsMap()) + b = coalesceTables(cvals, vm) + case string: + nm := map[string]string{ + "child": "exports." + iv, + "parent": ".", + } + outiv = append(outiv, nm) + s := r.Name + "." + nm["child"] + vm, err := cvals.Table(s) + if err != nil { + log.Printf("Warning: ImportValues missing table: %v", err) + continue + } + b = coalesceTables(b, vm.AsMap()) + } + } + // set our formatted import values + r.ImportValues = outiv + } + } + b = coalesceTables(b, cvals) + y, err := yaml.Marshal(b) + if err != nil { + return err + } + // set the new values + c.Values.Raw = string(y) + + 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..c92c9f052 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,114 @@ 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-chart1.SC1bool"] = "true" + e["imported-chart1.SC1float"] = "3.14" + e["imported-chart1.SC1int"] = "100" + e["imported-chart1.SC1string"] = "dollywood" + e["imported-chart1.SC1extra1"] = "11" + e["imported-chart1.SPextra1"] = "helm rocks" + e["imported-chart1.SC1extra1"] = "11" + + e["imported-chartA.SCAbool"] = "false" + e["imported-chartA.SCAfloat"] = "3.1" + e["imported-chartA.SCAint"] = "55" + e["imported-chartA.SCAstring"] = "jabba" + e["imported-chartA.SPextra3"] = "1.337" + e["imported-chartA.SC1extra2"] = "1.337" + e["imported-chartA.SCAnested1.SCAnested2"] = "true" + + e["imported-chartA-B.SCAbool"] = "false" + e["imported-chartA-B.SCAfloat"] = "3.1" + e["imported-chartA-B.SCAint"] = "55" + e["imported-chartA-B.SCAstring"] = "jabba" + + e["imported-chartA-B.SCBbool"] = "true" + e["imported-chartA-B.SCBfloat"] = "7.77" + e["imported-chartA-B.SCBint"] = "33" + e["imported-chartA-B.SCBstring"] = "boba" + e["imported-chartA-B.SPextra5"] = "k8s" + e["imported-chartA-B.SC1extra5"] = "tiller" + + e["overridden-chart1.SC1bool"] = "false" + e["overridden-chart1.SC1float"] = "3.141592" + e["overridden-chart1.SC1int"] = "99" + e["overridden-chart1.SC1string"] = "pollywog" + e["overridden-chart1.SPextra2"] = "42" + + e["overridden-chartA.SCAbool"] = "true" + e["overridden-chartA.SCAfloat"] = "41.3" + e["overridden-chartA.SCAint"] = "808" + e["overridden-chartA.SCAstring"] = "jaberwocky" + e["overridden-chartA.SPextra4"] = "true" + + e["overridden-chartA-B.SCAbool"] = "true" + e["overridden-chartA-B.SCAfloat"] = "41.3" + e["overridden-chartA-B.SCAint"] = "808" + e["overridden-chartA-B.SCAstring"] = "jaberwocky" + e["overridden-chartA-B.SCBbool"] = "false" + e["overridden-chartA-B.SCBfloat"] = "1.99" + e["overridden-chartA-B.SCBint"] = "77" + e["overridden-chartA-B.SCBstring"] = "jango" + e["overridden-chartA-B.SPextra6"] = "111" + e["overridden-chartA-B.SCAextra1"] = "23" + e["overridden-chartA-B.SCBextra1"] = "13" + e["overridden-chartA-B.SC1extra6"] = "77" + + // `exports` style + e["SCBexported1B"] = "1965" + e["SC1extra7"] = "true" + e["SCBexported2A"] = "blaster" + e["global.SC1exported2.all.SC1exported3"] = "SC1expstr" + + 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 + } + case bool: + b := strconv.FormatBool(pv.(bool)) + if b != vv { + t.Errorf("Failed to match imported bool value %v with expected %v", b, 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..712b3a2fa 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml @@ -1,21 +1,17 @@ # Default values for subchart. # This is a YAML-formatted file. # Declare variables to be passed into your templates. -replicaCount: 1 -image: - repository: nginx - tag: stable - pullPolicy: IfNotPresent +# subchartA service: name: nginx type: ClusterIP externalPort: 80 internalPort: 80 -resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi +SCAdata: + SCAbool: false + SCAfloat: 3.1 + SCAint: 55 + SCAstring: "jabba" + SCAnested1: + SCAnested2: true diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartB/values.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartB/values.yaml index 5e5b21065..774fdd75c 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartB/values.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartB/values.yaml @@ -1,21 +1,35 @@ # Default values for subchart. # This is a YAML-formatted file. # Declare variables to be passed into your templates. -replicaCount: 1 -image: - repository: nginx - tag: stable - pullPolicy: IfNotPresent service: name: nginx type: ClusterIP externalPort: 80 internalPort: 80 -resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi + +SCBdata: + SCBbool: true + SCBfloat: 7.77 + SCBint: 33 + SCBstring: "boba" + +exports: + SCBexported1: + SCBexported1A: + SCBexported1B: 1965 + + SCBexported2: + SCBexported2A: "blaster" + +global: + kolla: + nova: + api: + all: + port: 8774 + metadata: + all: + port: 8775 + + diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/requirements.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/requirements.yaml index 94d278234..abfe85e76 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart1/requirements.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/requirements.yaml @@ -6,10 +6,27 @@ dependencies: tags: - front-end - subcharta + import-values: + - child: SCAdata + parent: imported-chartA + - child: SCAdata + parent: overridden-chartA + - child: SCAdata + parent: imported-chartA-B + - name: subchartb repository: http://localhost:10191 version: 0.1.0 condition: subchartb.enabled + import-values: + - child: SCBdata + parent: imported-chartB + - child: SCBdata + parent: imported-chartA-B + - child: exports.SCBexported2 + parent: exports.SCBexported2 + - SCBexported1 + tags: - front-end - subchartb diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/values.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/values.yaml index 5e5b21065..72d3fa5c8 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart1/values.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/values.yaml @@ -1,21 +1,55 @@ # Default values for subchart. # This is a YAML-formatted file. # Declare variables to be passed into your templates. -replicaCount: 1 -image: - repository: nginx - tag: stable - pullPolicy: IfNotPresent +# subchart1 service: name: nginx type: ClusterIP externalPort: 80 internalPort: 80 -resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi + +SC1data: + SC1bool: true + SC1float: 3.14 + SC1int: 100 + SC1string: "dollywood" + SC1extra1: 11 + +imported-chartA: + SC1extra2: 1.337 + +overridden-chartA: + SCAbool: true + SCAfloat: 3.14 + SCAint: 100 + SCAstring: "jabathehut" + SC1extra3: true + +imported-chartA-B: + SC1extra5: "tiller" + +overridden-chartA-B: + SCAbool: true + SCAfloat: 3.33 + SCAint: 555 + SCAstring: "wormwood" + SCAextra1: 23 + + SCBbool: true + SCBfloat: 0.25 + SCBint: 98 + SCBstring: "murkwood" + SCBextra1: 13 + + SC1extra6: 77 + +SCBexported1A: + SC1extra7: true + +exports: + SC1exported1: + global: + SC1exported2: + all: + SC1exported3: "SC1expstr" \ No newline at end of file diff --git a/pkg/chartutil/testdata/subpop/requirements.yaml b/pkg/chartutil/testdata/subpop/requirements.yaml index 9840e047d..a8eb0aace 100644 --- a/pkg/chartutil/testdata/subpop/requirements.yaml +++ b/pkg/chartutil/testdata/subpop/requirements.yaml @@ -6,6 +6,22 @@ dependencies: tags: - front-end - subchart1 + import-values: + - child: SC1data + parent: imported-chart1 + - child: SC1data + parent: overridden-chart1 + - child: imported-chartA + parent: imported-chartA + - child: imported-chartA-B + parent: imported-chartA-B + - child: overridden-chartA-B + parent: overridden-chartA-B + - child: SCBexported1A + parent: . + - SCBexported2 + - SC1exported1 + - 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..55e872d41 100644 --- a/pkg/chartutil/testdata/subpop/values.yaml +++ b/pkg/chartutil/testdata/subpop/values.yaml @@ -1,6 +1,40 @@ # parent/values.yaml -# switch-like +imported-chart1: + SPextra1: "helm rocks" + +overridden-chart1: + SC1bool: false + SC1float: 3.141592 + SC1int: 99 + SC1string: "pollywog" + SPextra2: 42 + + +imported-chartA: + SPextra3: 1.337 + +overridden-chartA: + SCAbool: true + SCAfloat: 41.3 + SCAint: 808 + SCAstring: "jaberwocky" + SPextra4: true + +imported-chartA-B: + SPextra5: "k8s" + +overridden-chartA-B: + SCAbool: true + SCAfloat: 41.3 + SCAint: 808 + SCAstring: "jaberwocky" + SCBbool: false + SCBfloat: 1.99 + SCBint: 77 + SCBstring: "jango" + SPextra6: 111 + tags: front-end: true back-end: false diff --git a/pkg/helm/client.go b/pkg/helm/client.go index d66895596..4fb28d101 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) } @@ -167,6 +171,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 394956f70..ef5a3bee0 100644 --- a/pkg/resolver/resolver_test.go +++ b/pkg/resolver/resolver_test.go @@ -146,7 +146,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"},