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