fix(chartutil): move values coalescing into chartutil

pull/808/head
Matt Butcher 8 years ago
parent 06d0e52cb1
commit e8109048a9

@ -0,0 +1,3 @@
description: A Helm chart for Kubernetes
name: moby
version: 0.1.0

@ -0,0 +1,3 @@
description: A Helm chart for Kubernetes
name: pequod
version: 0.1.0

@ -0,0 +1,3 @@
description: A Helm chart for Kubernetes
name: ahab
version: 0.1.0

@ -0,0 +1,2 @@
scope: pequod
name: pequod

@ -0,0 +1,3 @@
description: A Helm chart for Kubernetes
name: spouter
version: 0.1.0

@ -0,0 +1,4 @@
scope: moby
name: moby
override: bad
top: nope

@ -4,9 +4,11 @@ import (
"errors"
"io"
"io/ioutil"
"log"
"strings"
"github.com/ghodss/yaml"
"k8s.io/helm/pkg/proto/hapi/chart"
)
// ErrNoTable indicates that a chart does not have a matching table.
@ -73,7 +75,7 @@ func ReadValues(data []byte) (vals Values, err error) {
return
}
// ReadValuesFile will parse a YAML file into a Values.
// ReadValuesFile will parse a YAML file into a map of values.
func ReadValuesFile(filename string) (Values, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
@ -81,3 +83,118 @@ func ReadValuesFile(filename string) (Values, error) {
}
return ReadValues(data)
}
// CoalesceValues coalesces all of the values in a chart (and its subcharts).
//
// The overrides map may be used to specifically override configuration values.
func CoalesceValues(chrt *chart.Chart, vals *chart.Config, overrides map[string]interface{}) (Values, error) {
var cvals Values
// Parse values if not nil. We merge these at the top level because
// the passed-in values are in the same namespace as the parent chart.
if vals != nil {
evals, err := ReadValues([]byte(vals.Raw))
if err != nil {
return cvals, err
}
// Override the top-level values. Overrides are NEVER merged deeply.
// The assumption is that an override is intended to set an explicit
// and exact value.
for k, v := range overrides {
evals[k] = v
}
cvals = coalesceValues(chrt, evals)
} else if len(overrides) > 0 {
cvals = coalesceValues(chrt, overrides)
}
cvals = coalesceDeps(chrt, cvals)
return cvals, nil
}
// coalesce coalesces the dest values and the chart values, giving priority to the dest values.
//
// This is a helper function for CoalesceValues.
func coalesce(ch *chart.Chart, dest map[string]interface{}) map[string]interface{} {
dest = coalesceValues(ch, dest)
coalesceDeps(ch, dest)
return dest
}
// coalesceDeps coalesces the dependencies of the given chart.
func coalesceDeps(chrt *chart.Chart, dest map[string]interface{}) map[string]interface{} {
for _, subchart := range chrt.Dependencies {
if c, ok := dest[subchart.Metadata.Name]; !ok {
// If dest doesn't already have the key, create it.
dest[subchart.Metadata.Name] = map[string]interface{}{}
} else if !istable(c) {
log.Printf("error: type mismatch on %s: %t", subchart.Metadata.Name, c)
return dest
}
if dv, ok := dest[subchart.Metadata.Name]; ok {
dest[subchart.Metadata.Name] = coalesce(subchart, dv.(map[string]interface{}))
}
}
return dest
}
// coalesceValues builds up a values map for a particular chart.
//
// Values in v will override the values in the chart.
func coalesceValues(c *chart.Chart, v map[string]interface{}) map[string]interface{} {
// If there are no values in the chart, we just return the given values
if c.Values == nil || c.Values.Raw == "" {
return v
}
nv, err := ReadValues([]byte(c.Values.Raw))
if err != nil {
// On error, we return just the overridden values.
// FIXME: We should log this error. It indicates that the YAML data
// did not parse.
log.Printf("error reading default values: %s", err)
return v
}
for key, val := range nv {
if _, ok := v[key]; !ok {
v[key] = val
} else if dest, ok := v[key].(Values); ok {
src, ok := val.(Values)
if !ok {
log.Printf("warning: skipped value for %s: Not a table.", key)
continue
}
// coalesce tables
coalesceTables(dest, src)
}
}
return v
}
// coalesceTables merges a source map into a destination map.
func coalesceTables(dst, src map[string]interface{}) map[string]interface{} {
for key, val := range src {
if istable(val) {
if innerdst, ok := dst[key]; !ok {
dst[key] = val
} else if istable(innerdst) {
coalesceTables(innerdst.(map[string]interface{}), val.(map[string]interface{}))
} else {
log.Printf("warning: cannot overwrite table with non table for %s (%v)", key, val)
}
continue
} else if dv, ok := dst[key]; ok && istable(dv) {
log.Printf("warning: destination for %s is a table. Ignoring non-table value %v", key, val)
continue
}
dst[key] = val
}
return dst
}
// istable is a special-purpose function to see if the present thing matches the definition of a YAML table.
func istable(v interface{}) bool {
_, ok := v.(map[string]interface{})
return ok
}

@ -2,9 +2,12 @@ package chartutil
import (
"bytes"
"encoding/json"
"fmt"
"testing"
"text/template"
"k8s.io/helm/pkg/proto/hapi/chart"
)
func TestReadValues(t *testing.T) {
@ -139,3 +142,106 @@ func ttpl(tpl string, v map[string]interface{}) (string, error) {
}
return b.String(), nil
}
var testCoalesceValuesYaml = `
top: yup
pequod:
ahab:
scope: whale
`
func TestCoalesceValues(t *testing.T) {
tchart := "testdata/moby"
overrides := map[string]interface{}{
"override": "good",
}
c, err := LoadDir(tchart)
if err != nil {
t.Fatal(err)
}
tvals := &chart.Config{Raw: testCoalesceValuesYaml}
v, err := CoalesceValues(c, tvals, overrides)
j, _ := json.MarshalIndent(v, "", " ")
t.Logf("Coalesced Values: %s", string(j))
tests := []struct {
tpl string
expect string
}{
{"{{.top}}", "yup"},
{"{{.override}}", "good"},
{"{{.name}}", "moby"},
{"{{.pequod.name}}", "pequod"},
{"{{.pequod.ahab.name}}", "ahab"},
{"{{.pequod.ahab.scope}}", "whale"},
}
for _, tt := range tests {
if o, err := ttpl(tt.tpl, v); err != nil || o != tt.expect {
t.Errorf("Expected %q to expand to %q, got %q", tt.tpl, tt.expect, o)
}
}
}
func TestCoalesceTables(t *testing.T) {
dst := map[string]interface{}{
"name": "Ishmael",
"address": map[string]interface{}{
"street": "123 Spouter Inn Ct.",
"city": "Nantucket",
},
"details": map[string]interface{}{
"friends": []string{"Tashtego"},
},
"boat": "pequod",
}
src := map[string]interface{}{
"occupation": "whaler",
"address": map[string]interface{}{
"state": "MA",
"street": "234 Spouter Inn Ct.",
},
"details": "empty",
"boat": map[string]interface{}{
"mast": true,
},
}
coalesceTables(dst, src)
if dst["name"] != "Ishmael" {
t.Errorf("Unexpected name: %s", dst["name"])
}
if dst["occupation"] != "whaler" {
t.Errorf("Unexpected occupation: %s", dst["occupation"])
}
addr, ok := dst["address"].(map[string]interface{})
if !ok {
t.Fatal("Address went away.")
}
if addr["street"].(string) != "234 Spouter Inn Ct." {
t.Errorf("Unexpected address: %v", addr["street"])
}
if addr["city"].(string) != "Nantucket" {
t.Errorf("Unexpected city: %v", addr["city"])
}
if addr["state"].(string) != "MA" {
t.Errorf("Unexpected state: %v", addr["state"])
}
if det, ok := dst["details"].(map[string]interface{}); !ok {
t.Fatalf("Details is the wrong type: %v", dst["details"])
} else if _, ok := det["friends"]; !ok {
t.Error("Could not find your friends. Maybe you don't have any. :-(")
}
if dst["boat"].(string) != "pequod" {
t.Errorf("Expected boat string, got %v", dst["boat"])
}
}

@ -34,7 +34,7 @@ func New() *Engine {
}
}
// Render takes a chart, optional values, and attempts to render the Go templates.
// Render takes a chart, optional values, and value overrids, and attempts to render the Go templates.
//
// Render can be called repeatedly on the same engine.
//

Loading…
Cancel
Save