Merge pull request #2112 from jascott1/feat/1995-import_child_values

feat(helm): import child values to parent
pull/2220/head
Matt Butcher 8 years ago committed by GitHub
commit 6d5b3bbb21

@ -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 * The `tags:` key in values must be a top level key. Globals and nested `tags:` tables
are not currently supported. 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 ## Templates and Values
Helm Chart templates are written in the Helm Chart templates are written in the

@ -62,6 +62,9 @@ type Dependency struct {
Tags []string `json:"tags"` Tags []string `json:"tags"`
// Enabled bool determines if chart should be loaded // Enabled bool determines if chart should be loaded
Enabled bool `json:"enabled"` 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 // ErrNoRequirementsFile to detect error condition
@ -266,3 +269,128 @@ func ProcessRequirementsEnabled(c *chart.Chart, v *chart.Config) error {
return nil 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
}

@ -18,6 +18,8 @@ import (
"sort" "sort"
"testing" "testing"
"strconv"
"k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/chart"
) )
@ -206,3 +208,114 @@ func extractCharts(c *chart.Chart, out []*chart.Chart) []*chart.Chart {
} }
return out 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
}
}
}
}

@ -1,21 +1,17 @@
# Default values for subchart. # Default values for subchart.
# This is a YAML-formatted file. # This is a YAML-formatted file.
# Declare variables to be passed into your templates. # Declare variables to be passed into your templates.
replicaCount: 1 # subchartA
image:
repository: nginx
tag: stable
pullPolicy: IfNotPresent
service: service:
name: nginx name: nginx
type: ClusterIP type: ClusterIP
externalPort: 80 externalPort: 80
internalPort: 80 internalPort: 80
resources: SCAdata:
limits: SCAbool: false
cpu: 100m SCAfloat: 3.1
memory: 128Mi SCAint: 55
requests: SCAstring: "jabba"
cpu: 100m SCAnested1:
memory: 128Mi SCAnested2: true

@ -1,21 +1,35 @@
# Default values for subchart. # Default values for subchart.
# This is a YAML-formatted file. # This is a YAML-formatted file.
# Declare variables to be passed into your templates. # Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: nginx
tag: stable
pullPolicy: IfNotPresent
service: service:
name: nginx name: nginx
type: ClusterIP type: ClusterIP
externalPort: 80 externalPort: 80
internalPort: 80 internalPort: 80
resources:
limits: SCBdata:
cpu: 100m SCBbool: true
memory: 128Mi SCBfloat: 7.77
requests: SCBint: 33
cpu: 100m SCBstring: "boba"
memory: 128Mi
exports:
SCBexported1:
SCBexported1A:
SCBexported1B: 1965
SCBexported2:
SCBexported2A: "blaster"
global:
kolla:
nova:
api:
all:
port: 8774
metadata:
all:
port: 8775

@ -6,10 +6,27 @@ dependencies:
tags: tags:
- front-end - front-end
- subcharta - subcharta
import-values:
- child: SCAdata
parent: imported-chartA
- child: SCAdata
parent: overridden-chartA
- child: SCAdata
parent: imported-chartA-B
- name: subchartb - name: subchartb
repository: http://localhost:10191 repository: http://localhost:10191
version: 0.1.0 version: 0.1.0
condition: subchartb.enabled condition: subchartb.enabled
import-values:
- child: SCBdata
parent: imported-chartB
- child: SCBdata
parent: imported-chartA-B
- child: exports.SCBexported2
parent: exports.SCBexported2
- SCBexported1
tags: tags:
- front-end - front-end
- subchartb - subchartb

@ -1,21 +1,55 @@
# Default values for subchart. # Default values for subchart.
# This is a YAML-formatted file. # This is a YAML-formatted file.
# Declare variables to be passed into your templates. # Declare variables to be passed into your templates.
replicaCount: 1 # subchart1
image:
repository: nginx
tag: stable
pullPolicy: IfNotPresent
service: service:
name: nginx name: nginx
type: ClusterIP type: ClusterIP
externalPort: 80 externalPort: 80
internalPort: 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"

@ -6,6 +6,22 @@ dependencies:
tags: tags:
- front-end - front-end
- subchart1 - 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 - name: subchart2
repository: http://localhost:10191 repository: http://localhost:10191
version: 0.1.0 version: 0.1.0

@ -1,6 +1,40 @@
# parent/values.yaml # 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: tags:
front-end: true front-end: true
back-end: false back-end: false

@ -97,6 +97,10 @@ func (h *Client) InstallReleaseFromChart(chart *chart.Chart, ns string, opts ...
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = chartutil.ProcessRequirementsImportValues(req.Chart, req.Values)
if err != nil {
return nil, err
}
return h.install(ctx, req) return h.install(ctx, req)
} }
@ -167,6 +171,10 @@ func (h *Client) UpdateReleaseFromChart(rlsName string, chart *chart.Chart, opts
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = chartutil.ProcessRequirementsImportValues(req.Chart, req.Values)
if err != nil {
return nil, err
}
return h.update(ctx, req) return h.update(ctx, req)
} }

@ -146,7 +146,7 @@ func TestResolve(t *testing.T) {
} }
func TestHashReq(t *testing.T) { func TestHashReq(t *testing.T) {
expect := "sha256:c8250374210bd909cef274be64f871bd4e376d4ecd34a1589b5abf90b68866ba" expect := "sha256:1feffe2016ca113f64159d91c1f77d6a83bcd23510b171d9264741bf9d63f741"
req := &chartutil.Requirements{ req := &chartutil.Requirements{
Dependencies: []*chartutil.Dependency{ Dependencies: []*chartutil.Dependency{
{Name: "alpine", Version: "0.1.0", Repository: "http://localhost:8879/charts"}, {Name: "alpine", Version: "0.1.0", Repository: "http://localhost:8879/charts"},

Loading…
Cancel
Save