feat: `get` and `getOrNil` template funcs to allow defaulting in templates (#370)
* feat: `get` and `getOrNil` template funcs to allow defaulting in templates Ref #357 * Add docs about missing keys and default values in templates
This commit is contained in:
parent
6f0dc6e069
commit
770c3daa5f
|
|
@ -443,7 +443,7 @@ releaseName: prod
|
|||
`values.yaml.gotmpl`
|
||||
|
||||
```yaml
|
||||
domain: {{ .Environment.Values.domain | default "dev.example.com" }}
|
||||
domain: {{ .Environment.Values | getOrNil "my.domain" | default "dev.example.com" }}
|
||||
```
|
||||
|
||||
`helmfile sync` installs `myapp` with the value `domain=dev.example.com`,
|
||||
|
|
@ -704,6 +704,13 @@ Run `helmfile --environment staging sync` and see it results in helmfile running
|
|||
|
||||
Voilà! You can mix helm releases that are backed by remote charts, local charts, and even kustomize overlays.
|
||||
|
||||
## Guides
|
||||
|
||||
Use the [Helmfile Best Practices Guide](/docs/writing-helmfile.md) to write advanced helmfiles that features:
|
||||
|
||||
- Default values
|
||||
- Layering
|
||||
|
||||
## Using env files
|
||||
|
||||
helmfile itself doesn't have an ability to load env files. But you can write some bash script to achieve the goal:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,34 @@
|
|||
|
||||
This guide covers the Helmfile’s considered patterns for writing advanced helmfiles. It focuses on how helmfile should be structured and executed.
|
||||
|
||||
## Missing keys and Default values
|
||||
|
||||
helmfile tries its best to inform users for noticing potential mistakes.
|
||||
|
||||
One example of how helmfile achieves it is that, `helmfile` fails when you tried to access missing keys in environment values.
|
||||
|
||||
That is, the following example let `helmfile` fail when you have no `eventApi.replicas` defined in environment values.
|
||||
|
||||
```
|
||||
{{ .Environment.Values.eventApi.replicas | default 1 }}
|
||||
```
|
||||
|
||||
In case it isn't a mistake and you do want to allow missing keys, use the `getOrNil` template function:
|
||||
|
||||
```
|
||||
{{ .Environment.Values | getOrNil "eventApi.replicas" }}
|
||||
```
|
||||
|
||||
This result in printing `<no value` in your template, that may or may not result in a failure.
|
||||
|
||||
If you want a kind of default values that is used when a missing key was referenced, use `default` like:
|
||||
|
||||
```
|
||||
{{ .Environment.Values | getOrNil "eventApi.replicas" | default 1 }}
|
||||
```
|
||||
|
||||
Now, you get `1` when there is no `eventApi.replicas` defined in environment values.
|
||||
|
||||
## Layering
|
||||
|
||||
You may occasionally end up with many helmfiles that shares common parts like which repositories to use, and whichi release to be bundled by default.
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ func (c *Context) createFuncMap() template.FuncMap {
|
|||
"fromYaml": FromYaml,
|
||||
"setValueAtPath": SetValueAtPath,
|
||||
"requiredEnv": RequiredEnv,
|
||||
"get": get,
|
||||
"getOrNil": getOrNil,
|
||||
}
|
||||
if c.preRender {
|
||||
// disable potential side-effect template calls
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
package tmpl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type noValueError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e *noValueError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
func get(path string, obj interface{}) (interface{}, error) {
|
||||
if path == "" {
|
||||
return obj, nil
|
||||
}
|
||||
keys := strings.Split(path, ".")
|
||||
switch typedObj := obj.(type) {
|
||||
case map[string]interface{}:
|
||||
v, ok := typedObj[keys[0]]
|
||||
if !ok {
|
||||
return nil, &noValueError{fmt.Sprintf("no value exist for key \"%s\" in %v", keys[0], typedObj)}
|
||||
}
|
||||
return get(strings.Join(keys[1:], "."), v)
|
||||
case map[interface{}]interface{}:
|
||||
v, ok := typedObj[keys[0]]
|
||||
if !ok {
|
||||
return nil, &noValueError{fmt.Sprintf("no value exist for key \"%s\" in %v", keys[0], typedObj)}
|
||||
}
|
||||
return get(strings.Join(keys[1:], "."), v)
|
||||
default:
|
||||
maybeStruct := reflect.ValueOf(typedObj)
|
||||
if maybeStruct.NumField() < 1 {
|
||||
return nil, &noValueError{fmt.Sprintf("unexpected type(%v) of value for key \"%s\": it must be either map[string]interface{} or any struct", reflect.TypeOf(obj), keys[0])}
|
||||
}
|
||||
f := maybeStruct.FieldByName(keys[0])
|
||||
if !f.IsValid() {
|
||||
return nil, &noValueError{fmt.Sprintf("no field named \"%s\" exist in %v", keys[0], typedObj)}
|
||||
}
|
||||
v := f.Interface()
|
||||
return get(strings.Join(keys[1:], "."), v)
|
||||
}
|
||||
}
|
||||
|
||||
func getOrNil(path string, o interface{}) (interface{}, error) {
|
||||
v, err := get(path, o)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *noValueError:
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package tmpl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetStruct(t *testing.T) {
|
||||
type Foo struct{ Bar string }
|
||||
|
||||
obj := struct{ Foo }{Foo{Bar: "Bar"}}
|
||||
|
||||
v1, err := get("Foo.Bar", obj)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if v1 != "Bar" {
|
||||
t.Errorf("unexpected value for path Foo.Bar in %v: expected=Bar, actual=%v", obj, v1)
|
||||
}
|
||||
|
||||
_, err = get("Foo.baz", obj)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("expected error but was not occurred")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMap(t *testing.T) {
|
||||
obj := map[string]interface{}{"Foo": map[string]interface{}{"Bar": "Bar"}}
|
||||
|
||||
v1, err := get("Foo.Bar", obj)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if v1 != "Bar" {
|
||||
t.Errorf("unexpected value for path Foo.Bar in %v: expected=Bar, actual=%v", obj, v1)
|
||||
}
|
||||
|
||||
_, err = get("Foo.baz", obj)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("expected error but was not occurred")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrNilStruct(t *testing.T) {
|
||||
type Foo struct{ Bar string }
|
||||
|
||||
obj := struct{ Foo }{Foo{Bar: "Bar"}}
|
||||
|
||||
v1, err := getOrNil("Foo.Bar", obj)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if v1 != "Bar" {
|
||||
t.Errorf("unexpected value for path Foo.Bar in %v: expected=Bar, actual=%v", obj, v1)
|
||||
}
|
||||
|
||||
v2, err := getOrNil("Foo.baz", obj)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if v2 != nil {
|
||||
t.Errorf("unexpected value for path Foo.baz in %v: expected=nil, actual=%v", obj, v2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrNilMap(t *testing.T) {
|
||||
obj := map[string]interface{}{"Foo": map[string]interface{}{"Bar": "Bar"}}
|
||||
|
||||
v1, err := getOrNil("Foo.Bar", obj)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if v1 != "Bar" {
|
||||
t.Errorf("unexpected value for path Foo.Bar in %v: expected=Bar, actual=%v", obj, v1)
|
||||
}
|
||||
|
||||
v2, err := getOrNil("Foo.baz", obj)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if v2 != nil {
|
||||
t.Errorf("unexpected value for path Foo.baz in %v: expected=nil, actual=%v", obj, v2)
|
||||
}
|
||||
}
|
||||
|
|
@ -60,6 +60,56 @@ func TestRenderTemplate_WithData(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRenderTemplate_AccessingMissingKeyWithGetOrNil(t *testing.T) {
|
||||
valuesYamlContent := `foo:
|
||||
bar: {{ . | getOrNil "foo.bar" }}
|
||||
`
|
||||
expected := `foo:
|
||||
bar: <no value>
|
||||
`
|
||||
expectedFilename := "values.yaml"
|
||||
data := map[string]interface{}{}
|
||||
ctx := &Context{readFile: func(filename string) ([]byte, error) {
|
||||
if filename != expectedFilename {
|
||||
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
|
||||
}
|
||||
return []byte(valuesYamlContent), nil
|
||||
}}
|
||||
buf, err := ctx.RenderTemplateToBuffer(valuesYamlContent, data)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
actual := buf.String()
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderTemplate_Defaulting(t *testing.T) {
|
||||
valuesYamlContent := `foo:
|
||||
bar: {{ . | getOrNil "foo.bar" | default "DEFAULT" }}
|
||||
`
|
||||
expected := `foo:
|
||||
bar: DEFAULT
|
||||
`
|
||||
expectedFilename := "values.yaml"
|
||||
data := map[string]interface{}{}
|
||||
ctx := &Context{readFile: func(filename string) ([]byte, error) {
|
||||
if filename != expectedFilename {
|
||||
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
|
||||
}
|
||||
return []byte(valuesYamlContent), nil
|
||||
}}
|
||||
buf, err := ctx.RenderTemplateToBuffer(valuesYamlContent, data)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
actual := buf.String()
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func renderTemplateToString(s string) (string, error) {
|
||||
ctx := &Context{readFile: func(filename string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("unexpected call to readFile: filename=%s", filename)
|
||||
|
|
|
|||
Loading…
Reference in New Issue