From e8109048a9e59b7b3617a58c42d55f8df8d2fb76 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Thu, 9 Jun 2016 13:27:42 -0600 Subject: [PATCH] fix(chartutil): move values coalescing into chartutil --- pkg/chartutil/testdata/moby/Chart.yaml | 3 + .../testdata/moby/charts/pequod/Chart.yaml | 3 + .../moby/charts/pequod/charts/ahab/Chart.yaml | 3 + .../charts/pequod/charts/ahab/values.yaml | 2 + .../testdata/moby/charts/pequod/values.yaml | 2 + .../testdata/moby/charts/spouter/Chart.yaml | 3 + .../testdata/moby/charts/spouter/values.yaml | 1 + pkg/chartutil/testdata/moby/values.yaml | 4 + pkg/chartutil/values.go | 119 +++++++++++++++++- pkg/chartutil/values_test.go | 106 ++++++++++++++++ pkg/engine/engine.go | 2 +- 11 files changed, 246 insertions(+), 2 deletions(-) create mode 100755 pkg/chartutil/testdata/moby/Chart.yaml create mode 100755 pkg/chartutil/testdata/moby/charts/pequod/Chart.yaml create mode 100755 pkg/chartutil/testdata/moby/charts/pequod/charts/ahab/Chart.yaml create mode 100644 pkg/chartutil/testdata/moby/charts/pequod/charts/ahab/values.yaml create mode 100644 pkg/chartutil/testdata/moby/charts/pequod/values.yaml create mode 100755 pkg/chartutil/testdata/moby/charts/spouter/Chart.yaml create mode 100644 pkg/chartutil/testdata/moby/charts/spouter/values.yaml create mode 100644 pkg/chartutil/testdata/moby/values.yaml diff --git a/pkg/chartutil/testdata/moby/Chart.yaml b/pkg/chartutil/testdata/moby/Chart.yaml new file mode 100755 index 000000000..b725af916 --- /dev/null +++ b/pkg/chartutil/testdata/moby/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +name: moby +version: 0.1.0 diff --git a/pkg/chartutil/testdata/moby/charts/pequod/Chart.yaml b/pkg/chartutil/testdata/moby/charts/pequod/Chart.yaml new file mode 100755 index 000000000..d9a3bfd5f --- /dev/null +++ b/pkg/chartutil/testdata/moby/charts/pequod/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +name: pequod +version: 0.1.0 diff --git a/pkg/chartutil/testdata/moby/charts/pequod/charts/ahab/Chart.yaml b/pkg/chartutil/testdata/moby/charts/pequod/charts/ahab/Chart.yaml new file mode 100755 index 000000000..c3cdf397d --- /dev/null +++ b/pkg/chartutil/testdata/moby/charts/pequod/charts/ahab/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +name: ahab +version: 0.1.0 diff --git a/pkg/chartutil/testdata/moby/charts/pequod/charts/ahab/values.yaml b/pkg/chartutil/testdata/moby/charts/pequod/charts/ahab/values.yaml new file mode 100644 index 000000000..86c3f63aa --- /dev/null +++ b/pkg/chartutil/testdata/moby/charts/pequod/charts/ahab/values.yaml @@ -0,0 +1,2 @@ +scope: ahab +name: ahab diff --git a/pkg/chartutil/testdata/moby/charts/pequod/values.yaml b/pkg/chartutil/testdata/moby/charts/pequod/values.yaml new file mode 100644 index 000000000..d6e34b274 --- /dev/null +++ b/pkg/chartutil/testdata/moby/charts/pequod/values.yaml @@ -0,0 +1,2 @@ +scope: pequod +name: pequod diff --git a/pkg/chartutil/testdata/moby/charts/spouter/Chart.yaml b/pkg/chartutil/testdata/moby/charts/spouter/Chart.yaml new file mode 100755 index 000000000..f6819c06d --- /dev/null +++ b/pkg/chartutil/testdata/moby/charts/spouter/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +name: spouter +version: 0.1.0 diff --git a/pkg/chartutil/testdata/moby/charts/spouter/values.yaml b/pkg/chartutil/testdata/moby/charts/spouter/values.yaml new file mode 100644 index 000000000..f71d92a9f --- /dev/null +++ b/pkg/chartutil/testdata/moby/charts/spouter/values.yaml @@ -0,0 +1 @@ +scope: spouter diff --git a/pkg/chartutil/testdata/moby/values.yaml b/pkg/chartutil/testdata/moby/values.yaml new file mode 100644 index 000000000..1972c0844 --- /dev/null +++ b/pkg/chartutil/testdata/moby/values.yaml @@ -0,0 +1,4 @@ +scope: moby +name: moby +override: bad +top: nope diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go index d1c9aceec..09fd82a92 100644 --- a/pkg/chartutil/values.go +++ b/pkg/chartutil/values.go @@ -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 +} diff --git a/pkg/chartutil/values_test.go b/pkg/chartutil/values_test.go index 6cf69e3f6..f15c4af47 100644 --- a/pkg/chartutil/values_test.go +++ b/pkg/chartutil/values_test.go @@ -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"]) + } +} diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 17254f17e..435d726ca 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -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. //