From 60f5341b91ce78b097382ea45a0678c7fac02a81 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 14 Jun 2016 11:10:24 -0600 Subject: [PATCH] feat(chartutil): support global variables This provides support for "global" variables. It does this by declaring "global" to be a special namespace. It then copies this namespace into every subchart, coalescing it into any "global" namespace found there. The net result is that if "global.foo" is set in the YAML file, it will be available to every chart/subchart as ".global.foo" regardless of where that chart is in the subchart tree. --- docs/charts.md | 102 +++++++++++++++---- pkg/chartutil/testdata/albatross/values.yaml | 3 + pkg/chartutil/values.go | 53 +++++++++- pkg/chartutil/values_test.go | 17 ++++ pkg/engine/engine_test.go | 15 ++- 5 files changed, 167 insertions(+), 23 deletions(-) diff --git a/docs/charts.md b/docs/charts.md index 63fbb0239..b1ed4da1c 100644 --- a/docs/charts.md +++ b/docs/charts.md @@ -211,10 +211,10 @@ Considering the template in the previous section, a `values.yaml` file that supplies the necessary values would look like this: ```yaml -imageRegistry = "quay.io/deis" -dockerTag = "latest" -pullPolicy = "alwaysPull" -storage = "s3" +imageRegistry: "quay.io/deis" +dockerTag: "latest" +pullPolicy: "alwaysPull" +storage: "s3" ``` A values file is formatted in YAML. A chart may include a default @@ -230,17 +230,17 @@ values file. For example, consider a `myvals.yaml` file that looks like this: ```yaml -storage = "gcs" +storage: "gcs" ``` When this is merged with the `values.yaml` in the chart, the resulting generated content will be: ```yaml -imageRegistry = "quay.io/deis" -dockerTag = "latest" -pullPolicy = "alwaysPull" -storage = "gcs" +imageRegistry: "quay.io/deis" +dockerTag: "latest" +pullPolicy: "alwaysPull" +storage: "gcs" ``` Note that only the last field was overridden. @@ -260,27 +260,91 @@ dependencies. The values file could supply values to all of these components: ```yaml -title = "My Wordpress Site" # Sent to the Wordpress template +title: "My Wordpress Site" # Sent to the Wordpress template -[mysql] -max_connections = 100 # Sent to MySQL -password = "secret" +mysql: + max_connections: 100 # Sent to MySQL + password: "secret" -[apache] -port = 8080 # Passed to Apache +apache: + port: 8080 # Passed to Apache ``` Charts at a higher level have access to all of the variables defined -beneath. So the wordpress chart can access `mysql.password`. But lower +beneath. So the wordpress chart can access `.mysql.password`. But lower level charts cannot access things in parent charts, so MySQL will not be able to access the `title` property. Nor, for that matter, can it access -`apache.port`. +`.apache.port`. Values are namespaced, but namespaces are pruned. So for the Wordpress -chart, it can access the MySQL password field as `mysql.password`. But +chart, it can access the MySQL password field as `.mysql.password`. But for the MySQL chart, the scope of the values has been reduced and the namespace prefix removed, so it will see the password field simply as -`password`. +`.password`. + +#### Global Values + +As of 2.0.0-Alpha.2, Helm supports sspecial "global" value. Consider +this modified version of the previous exampl: + +```yaml +title: "My Wordpress Site" # Sent to the Wordpress template + +global: + app: MyWordpress + +mysql: + max_connections: 100 # Sent to MySQL + password: "secret" + +apache: + port: 8080 # Passed to Apache +``` + +The above adds a `global` section with the value `app: MyWordpress`. +This value is available to _all_ charts as `.global.app`. + +For example, the `mysql` templates may access `app` as `{{.global.app}}`, and +so can the `apache` chart. Effectively, the values file above is +regenerated like this: + +```yaml +title: "My Wordpress Site" # Sent to the Wordpress template + +global: + app: MyWordpress + +mysql: + global: + app: MyWordpress + max_connections: 100 # Sent to MySQL + password: "secret" + +apache: + global: + app: MyWordpress + port: 8080 # Passed to Apache +``` + +This provides a way of sharing one top-level variable with all +subcharts, which is useful for things like setting `metadata` properties +like labels. + +If a subchart declares a global variable, that global will be passed +_downward_ (to the subchart's subcharts), but not _upward_ to the parent +chart. There is no way for a subchart to influence the values of the +parent chart. + +_Global sections are restricted to only simple key/value pairs. They do +not support nesting._ + +For example, the following is **illegal** and will not work: + +```yaml +global: + foo: # It is illegal to nest an object inside of global. + bar: baz +``` ### References diff --git a/pkg/chartutil/testdata/albatross/values.yaml b/pkg/chartutil/testdata/albatross/values.yaml index 0acfa292f..3121cd7ce 100644 --- a/pkg/chartutil/testdata/albatross/values.yaml +++ b/pkg/chartutil/testdata/albatross/values.yaml @@ -1 +1,4 @@ albatross: "true" + +global: + author: Coleridge diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go index 2e6a90043..9346f53fd 100644 --- a/pkg/chartutil/values.go +++ b/pkg/chartutil/values.go @@ -14,6 +14,9 @@ import ( // ErrNoTable indicates that a chart does not have a matching table. var ErrNoTable = errors.New("no table") +// GlobalKey is the name of the Values key that is used for storing global vars. +const GlobalKey = "global" + // Values represents a collection of chart values. type Values map[string]interface{} @@ -88,7 +91,7 @@ func ReadValuesFile(filename string) (Values, error) { // // The overrides map may be used to specifically override configuration values. // -// Values are coalesced together using the fillowing rules: +// Values are coalesced together using the following rules: // // - Values in a higher level chart always override values in a lower-level // dependency chart @@ -100,6 +103,7 @@ func CoalesceValues(chrt *chart.Chart, vals *chart.Config, overrides map[string] // 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 { + log.Printf("Merging overrides into config.") evals, err := ReadValues([]byte(vals.Raw)) if err != nil { return cvals, err @@ -140,10 +144,55 @@ func coalesceDeps(chrt *chart.Chart, dest map[string]interface{}) map[string]int return dest } if dv, ok := dest[subchart.Metadata.Name]; ok { - dest[subchart.Metadata.Name] = coalesce(subchart, dv.(map[string]interface{})) + dvmap := dv.(map[string]interface{}) + + // Get globals out of dest and merge them into dvmap. + coalesceGlobals(dvmap, dest) + + // Now coalesce the rest of the values. + dest[subchart.Metadata.Name] = coalesce(subchart, dvmap) + } + } + return dest +} + +// coalesceGlobals copies the globals out of src and merges them into dest. +// +// For convenience, returns dest. +func coalesceGlobals(dest, src map[string]interface{}) map[string]interface{} { + var dg, sg map[string]interface{} + + if destglob, ok := dest[GlobalKey]; !ok { + dg = map[string]interface{}{} + } else if dg, ok = destglob.(map[string]interface{}); !ok { + log.Printf("warning: skipping globals because destination %s is not a table.", GlobalKey) + return dg + } + + if srcglob, ok := src[GlobalKey]; !ok { + sg = map[string]interface{}{} + } else if sg, ok = srcglob.(map[string]interface{}); !ok { + log.Printf("warning: skipping globals because source %s is not a table.", GlobalKey) + return dg + } + + // We manually copy (instead of using coalesceTables) because (a) we need + // to prevent loops, and (b) we disallow nesting tables under globals. + // Globals should _just_ be k/v pairs. + for key, val := range sg { + if istable(val) { + log.Printf("warning: nested values are illegal in globals (%s)", key) + continue + } else if dv, ok := dg[key]; ok && istable(dv) { + log.Printf("warning: nested values are illegal in globals (%s)", key) + continue } + // TODO: Do we need to do any additional checking on the value? + dg[key] = val } + dest[GlobalKey] = dg return dest + } // coalesceValues builds up a values map for a particular chart. diff --git a/pkg/chartutil/values_test.go b/pkg/chartutil/values_test.go index f15c4af47..edcb1ea12 100644 --- a/pkg/chartutil/values_test.go +++ b/pkg/chartutil/values_test.go @@ -146,7 +146,14 @@ func ttpl(tpl string, v map[string]interface{}) (string, error) { var testCoalesceValuesYaml = ` top: yup +global: + name: Ishmael + subject: Queequeg + pequod: + global: + name: Stinky + harpooner: Tashtego ahab: scope: whale ` @@ -174,9 +181,19 @@ func TestCoalesceValues(t *testing.T) { {"{{.top}}", "yup"}, {"{{.override}}", "good"}, {"{{.name}}", "moby"}, + {"{{.global.name}}", "Ishmael"}, + {"{{.global.subject}}", "Queequeg"}, + {"{{.global.harpooner}}", ""}, {"{{.pequod.name}}", "pequod"}, {"{{.pequod.ahab.name}}", "ahab"}, {"{{.pequod.ahab.scope}}", "whale"}, + {"{{.pequod.ahab.global.name}}", "Ishmael"}, + {"{{.pequod.ahab.global.subject}}", "Queequeg"}, + {"{{.pequod.ahab.global.harpooner}}", "Tashtego"}, + {"{{.pequod.global.name}}", "Ishmael"}, + {"{{.pequod.global.subject}}", "Queequeg"}, + {"{{.spouter.global.name}}", "Ishmael"}, + {"{{.spouter.global.harpooner}}", ""}, } for _, tt := range tests { diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index 0753ed09c..85cfdf5ab 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -29,6 +29,7 @@ func TestRender(t *testing.T) { }, Templates: []*chart.Template{ {Name: "test1", Data: []byte("{{.outer | title }} {{.inner | title}}")}, + {Name: "test2", Data: []byte("{{.global.callme | lower }}")}, }, Values: &chart.Config{ Raw: "outer: DEFAULT\ninner: DEFAULT", @@ -41,6 +42,9 @@ func TestRender(t *testing.T) { overrides := map[string]interface{}{ "outer": "spouter", + "global": map[string]interface{}{ + "callme": "Ishmael", + }, } e := New() @@ -58,6 +62,11 @@ func TestRender(t *testing.T) { t.Errorf("Expected %q, got %q", expect, out["test1"]) } + expect = "ishmael" + if out["test2"] != expect { + t.Errorf("Expected %q, got %q", expect, out["test2"]) + } + if _, err := e.Render(c, v); err != nil { t.Errorf("Unexpected error: %s", err) } @@ -194,7 +203,7 @@ func TestRenderNestedValues(t *testing.T) { deepest := &chart.Chart{ Metadata: &chart.Metadata{Name: "deepest"}, Templates: []*chart.Template{ - {Name: deepestpath, Data: []byte(`And this same {{.what}} that smiles to-day`)}, + {Name: deepestpath, Data: []byte(`And this same {{.what}} that smiles {{.global.when}}`)}, }, Values: &chart.Config{Raw: `what: "milkshake"`}, } @@ -228,7 +237,9 @@ herrick: what: rosebuds herrick: deepest: - what: flower`, + what: flower +global: + when: to-day`, } inject, err := chartutil.CoalesceValues(outer, &injValues, map[string]interface{}{})