From 79a3db0a63aad7d717454a4ddfd989096bc1c39e Mon Sep 17 00:00:00 2001 From: Steve Wilkerson Date: Thu, 2 Mar 2017 09:37:14 -0600 Subject: [PATCH] feat(helm): add support for required properties Adds the `required` function in enginge.go to support required properties in values.yml. When a chart developer wishes to specify intent in requiring a value, they can use this function to declare an error message that gets returned when chart rendering fails when a required value is not present in values.yml. Closes #1580 --- docs/charts.md | 3 +++ docs/charts_tips_and_tricks.md | 39 ++++++++++++++++++++++++++++++++-- pkg/engine/engine.go | 17 ++++++++++++++- pkg/engine/engine_test.go | 38 ++++++++++++++++++++++++++++++++- 4 files changed, 93 insertions(+), 4 deletions(-) diff --git a/docs/charts.md b/docs/charts.md index be69597fb..9e347a9e3 100644 --- a/docs/charts.md +++ b/docs/charts.md @@ -436,6 +436,9 @@ anything. **NOTE:** If the `--set` flag is used on `helm install` or `helm upgrade`, those values are simply converted to YAML on the client side. +**NOTE:** If any required entries in the values file exist, they can be declared +as required in the chart template by using the ['required' function](charts_tips_and_tricks.md) + Any of these values are then accessible inside of templates using the `.Values` object: diff --git a/docs/charts_tips_and_tricks.md b/docs/charts_tips_and_tricks.md index c59d6bde9..01d9ea83c 100644 --- a/docs/charts_tips_and_tricks.md +++ b/docs/charts_tips_and_tricks.md @@ -14,8 +14,8 @@ First, we added almost all of the functions in the for security reasons: `env` and `expandenv` (which would have given chart authors access to Tiller's environment). -We also added one special template function: `include`. The `include` function -allows you to bring in another template, and then pass the results to other +We also added two special template functions: `include` and `required`. The `include` +function allows you to bring in another template, and then pass the results to other template functions. For example, this template snippet includes a template called `mytpl.tpl`, then @@ -25,6 +25,17 @@ lowercases the result, then wraps that in double quotes. value: {{include "mytpl.tpl" . | lower | quote}} ``` +The `required` function allows you to declare a particular +values entry as required for template rendering. If the value is empty, the template +rendering will fail with a user submitted error message. + +The following example of the `required` function declares an entry for .Values.who +is required, and will print an error message when that entry is missing: + +```yaml +value: {{required "A valid .Values.who entry required!" .Values.who }} +``` + ## Quote Strings, Don't Quote Integers When you are working with string data, you are always safer quoting the @@ -61,6 +72,30 @@ Because YAML ascribes significance to indentation levels and whitespace, this is one great way to include snippets of code, but handle indentation in a relevant context. +## Using the 'required' function + +Go provides a way for setting template options to control behavior +when a map is indexed with a key that's not present in the map. This +is typically set with template.Options("missingkey=option"), where option +can be default, zero, or error. While setting this option to error will +stop execution with an arror, this would apply to every missing key in the +map. There may be situations where a chart developer wants to enforce this +behavior for select values in the values.yml file. + +The `required` function gives developers the ability to declare a value entry +as required for template rendering. If the entry is empty in values.yml, the +template will not render and will return an error message supplied by the +developer. + +For example: + +``` +{{ required "A valid foo is required!" .Values.foo }} +``` + +The above will render the template when .Values.foo is defined, but will fail +to render and exit when .Values.foo is undefined. + ## Automatically Roll Deployments When ConfigMaps or Secrets change Often times configmaps or secrets are injected as configuration diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 8f4eff127..ab813968f 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -63,6 +63,8 @@ func New() *Engine { // // - "include": This is late-bound in Engine.Render(). The version // included in the FuncMap is a placeholder. +// - "required": This is late-bound in Engine.Render(). The version +// included in thhe FuncMap is a placeholder. func FuncMap() template.FuncMap { f := sprig.TxtFuncMap() delete(f, "env") @@ -79,7 +81,8 @@ func FuncMap() template.FuncMap { // This is a placeholder for the "include" function, which is // late-bound to a template. By declaring it here, we preserve the // integrity of the linter. - "include": func(string, interface{}) string { return "not implemented" }, + "include": func(string, interface{}) string { return "not implemented" }, + "required": func(string, interface{}) interface{} { return "not implemented" }, } for k, v := range extra { @@ -143,6 +146,18 @@ func (e *Engine) alterFuncMap(t *template.Template) template.FuncMap { return buf.String() } + // Add the 'required' function here + funcMap["required"] = func(warn string, val interface{}) (interface{}, error) { + if val == nil { + return val, fmt.Errorf(warn) + } else if _, ok := val.(string); ok { + if val == "" { + return val, fmt.Errorf(warn) + } + } + return val, nil + } + return funcMap } diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index f0dcba354..f821b2de5 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -49,7 +49,7 @@ func TestFuncMap(t *testing.T) { } // Test for Engine-specific template functions. - expect := []string{"include", "toYaml", "fromYaml", "toToml", "toJson", "fromJson"} + expect := []string{"include", "required", "toYaml", "fromYaml", "toToml", "toJson", "fromJson"} for _, f := range expect { if _, ok := fns[f]; !ok { t.Errorf("Expected add-on function %q", f) @@ -409,4 +409,40 @@ func TestAlterFuncMap(t *testing.T) { if got := out["conrad/templates/quote"]; got != expect { t.Errorf("Expected %q, got %q (%v)", expect, got, out) } + + reqChart := &chart.Chart{ + Metadata: &chart.Metadata{Name: "conan"}, + Templates: []*chart.Template{ + {Name: "templates/quote", Data: []byte(`All your base are belong to {{ required "A valid 'who' is required" .Values.who }}`)}, + {Name: "templates/bases", Data: []byte(`All {{ required "A valid 'bases' is required" .Values.bases }} of them!`)}, + }, + Values: &chart.Config{Raw: ``}, + Dependencies: []*chart.Chart{}, + } + + reqValues := chartutil.Values{ + "Values": chartutil.Values{ + "who": "us", + "bases": 2, + }, + "Chart": reqChart.Metadata, + "Release": chartutil.Values{ + "Name": "That 90s meme", + }, + } + + outReq, err := New().Render(reqChart, reqValues) + if err != nil { + t.Fatal(err) + } + + expectStr := "All your base are belong to us" + if gotStr := outReq["conan/templates/quote"]; gotStr != expectStr { + t.Errorf("Expected %q, got %q (%v)", expectStr, gotStr, outReq) + } + expectNum := "All 2 of them!" + if gotNum := outReq["conan/templates/bases"]; gotNum != expectNum { + t.Errorf("Expected %q, got %q (%v)", expectNum, gotNum, outReq) + } + }