Commit 60f5341b authored by Matt Butcher's avatar Matt Butcher

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.
parent 54f0ffe0
......@@ -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
......
......@@ -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.
......
......@@ -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}}", "<no value>"},
{"{{.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}}", "<no value>"},
}
for _, tt := range tests {
......
......@@ -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{}{})
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment