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
pull/2112/head
Justin Scott 8 years ago
parent 890b6f5627
commit 0e81899f5f

@ -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
}

@ -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
}
}
}
}

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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)
}

@ -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"},

Loading…
Cancel
Save