fix: Keep backward-compatibility broken after introduction of values file template (#257)
Fixes #249
This commit is contained in:
		
							parent
							
								
									9b71c64ef2
								
							
						
					
					
						commit
						b3ebd4cdd0
					
				|  | @ -290,16 +290,18 @@ releases | |||
| - name: myapp | ||||
|   chart: mychart | ||||
|   values: | ||||
|   - values.yaml.tpl | ||||
|   - values.yaml.gotmpl | ||||
| ``` | ||||
| 
 | ||||
| whereas `values.yaml.tpl` would be something like: | ||||
| Every values file whose file extension is `.gotmpl` is considered as a tempalte file. | ||||
| 
 | ||||
| Suppose `values.yaml.gotmpl` was something like: | ||||
| 
 | ||||
| ```yaml | ||||
| {{ readFile "values.yaml" | fromYaml | setValueAtPath "foo.bar" "FOO_BAR" | toYaml }} | ||||
| ``` | ||||
| 
 | ||||
| Suppose `values.yaml` was: | ||||
| And `values.yaml` was: | ||||
| 
 | ||||
| ```yaml | ||||
| foo: | ||||
|  |  | |||
							
								
								
									
										3
									
								
								main.go
								
								
								
								
							
							
						
						
									
										3
									
								
								main.go
								
								
								
								
							|  | @ -14,6 +14,7 @@ import ( | |||
| 	"github.com/roboll/helmfile/args" | ||||
| 	"github.com/roboll/helmfile/helmexec" | ||||
| 	"github.com/roboll/helmfile/state" | ||||
| 	"github.com/roboll/helmfile/tmpl" | ||||
| 	"github.com/urfave/cli" | ||||
| 	"go.uber.org/zap" | ||||
| 	"go.uber.org/zap/zapcore" | ||||
|  | @ -399,7 +400,7 @@ func eachDesiredStateDo(c *cli.Context, converge func(*state.HelmState, helmexec | |||
| 	} | ||||
| 	allSelectorNotMatched := true | ||||
| 	for _, f := range desiredStateFiles { | ||||
| 		yamlBuf, err := state.RenderTemplateFileToBuffer(f) | ||||
| 		yamlBuf, err := tmpl.RenderTemplateFileToBuffer(f) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  |  | |||
|  | @ -12,10 +12,9 @@ import ( | |||
| 	"strings" | ||||
| 	"sync" | ||||
| 
 | ||||
| 	"bytes" | ||||
| 	"regexp" | ||||
| 
 | ||||
| 	"github.com/roboll/helmfile/tmpl" | ||||
| 	"github.com/roboll/helmfile/valuesfile" | ||||
| 	"go.uber.org/zap" | ||||
| 	"gopkg.in/yaml.v2" | ||||
| ) | ||||
|  | @ -98,16 +97,6 @@ type SetValue struct { | |||
| 	Values []string `yaml:"values"` | ||||
| } | ||||
| 
 | ||||
| // CreateFromTemplateFile loads the helmfile from disk and processes the template
 | ||||
| func CreateFromTemplateFile(file string, logger *zap.SugaredLogger) (*HelmState, error) { | ||||
| 	yamlBuf, err := RenderTemplateFileToBuffer(file) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return CreateFromYaml(yamlBuf.Bytes(), file, logger) | ||||
| } | ||||
| 
 | ||||
| func CreateFromYaml(content []byte, file string, logger *zap.SugaredLogger) (*HelmState, error) { | ||||
| 	var state HelmState | ||||
| 
 | ||||
|  | @ -130,19 +119,6 @@ func CreateFromYaml(content []byte, file string, logger *zap.SugaredLogger) (*He | |||
| 	return &state, nil | ||||
| } | ||||
| 
 | ||||
| func RenderTemplateFileToBuffer(file string) (*bytes.Buffer, error) { | ||||
| 	content, err := ioutil.ReadFile(file) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return renderTemplateToBuffer(string(content)) | ||||
| } | ||||
| 
 | ||||
| func renderTemplateToBuffer(s string) (*bytes.Buffer, error) { | ||||
| 	return tmpl.DefaultContext.RenderTemplateToBuffer(s) | ||||
| } | ||||
| 
 | ||||
| func (state *HelmState) applyDefaultsTo(spec *ReleaseSpec) { | ||||
| 	if state.Namespace != "" { | ||||
| 		spec.Namespace = state.Namespace | ||||
|  | @ -698,17 +674,18 @@ func (state *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, basePat | |||
| 				return nil, err | ||||
| 			} | ||||
| 
 | ||||
| 			yamlBuf, err := RenderTemplateFileToBuffer(path) | ||||
| 			if err != nil { | ||||
| 
 | ||||
| 				return nil, fmt.Errorf("failed to render [%s], because of %v", path, err) | ||||
| 			} | ||||
| 			valfile, err := ioutil.TempFile("", "values") | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			defer valfile.Close() | ||||
| 			yamlBytes := yamlBuf.Bytes() | ||||
| 
 | ||||
| 			r := valuesfile.NewRenderer(ioutil.ReadFile) | ||||
| 			yamlBytes, err := r.RenderToBytes(path) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 
 | ||||
| 			if _, err := valfile.Write(yamlBytes); err != nil { | ||||
| 				return nil, fmt.Errorf("failed to write %s: %v", valfile.Name(), err) | ||||
| 			} | ||||
|  |  | |||
|  | @ -12,14 +12,6 @@ import ( | |||
| 
 | ||||
| var logger = helmexec.NewLogger(os.Stdout, "warn") | ||||
| 
 | ||||
| func renderTemplateToString(s string) (string, error) { | ||||
| 	tplString, err := renderTemplateToBuffer(s) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return tplString.String(), nil | ||||
| } | ||||
| 
 | ||||
| func TestReadFromYaml(t *testing.T) { | ||||
| 	yamlFile := "example/path/to/yaml/file" | ||||
| 	yamlContent := []byte(`releases: | ||||
|  | @ -520,122 +512,6 @@ func TestHelmState_flagsForUpgrade(t *testing.T) { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| func Test_renderTemplateToString(t *testing.T) { | ||||
| 	type args struct { | ||||
| 		s    string | ||||
| 		envs map[string]string | ||||
| 	} | ||||
| 	tests := []struct { | ||||
| 		name    string | ||||
| 		args    args | ||||
| 		want    string | ||||
| 		wantErr bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "simple replacement", | ||||
| 			args: args{ | ||||
| 				s: "{{ env \"HF_TEST_VAR\" }}", | ||||
| 				envs: map[string]string{ | ||||
| 					"HF_TEST_VAR": "content", | ||||
| 				}, | ||||
| 			}, | ||||
| 			want:    "content", | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "two replacements", | ||||
| 			args: args{ | ||||
| 				s: "{{ env \"HF_TEST_ALPHA\" }}{{ env \"HF_TEST_BETA\" }}", | ||||
| 				envs: map[string]string{ | ||||
| 					"HF_TEST_ALPHA": "first", | ||||
| 					"HF_TEST_BETA":  "second", | ||||
| 				}, | ||||
| 			}, | ||||
| 			want:    "firstsecond", | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "replacement and comment", | ||||
| 			args: args{ | ||||
| 				s: "{{ env \"HF_TEST_ALPHA\" }}{{/* comment */}}", | ||||
| 				envs: map[string]string{ | ||||
| 					"HF_TEST_ALPHA": "first", | ||||
| 				}, | ||||
| 			}, | ||||
| 			want:    "first", | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "global template function", | ||||
| 			args: args{ | ||||
| 				s: "{{ env \"HF_TEST_ALPHA\" | len }}", | ||||
| 				envs: map[string]string{ | ||||
| 					"HF_TEST_ALPHA": "abcdefg", | ||||
| 				}, | ||||
| 			}, | ||||
| 			want:    "7", | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "env var not set", | ||||
| 			args: args{ | ||||
| 				s: "{{ env \"HF_TEST_NONE\" }}", | ||||
| 				envs: map[string]string{ | ||||
| 					"HF_TEST_THIS": "first", | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: "", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "undefined function", | ||||
| 			args: args{ | ||||
| 				s: "{{ env foo }}", | ||||
| 				envs: map[string]string{ | ||||
| 					"foo": "bar", | ||||
| 				}, | ||||
| 			}, | ||||
| 			wantErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "required env var", | ||||
| 			args: args{ | ||||
| 				s: "{{ requiredEnv \"HF_TEST\" }}", | ||||
| 				envs: map[string]string{ | ||||
| 					"HF_TEST": "value", | ||||
| 				}, | ||||
| 			}, | ||||
| 			want:    "value", | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "required env var not set", | ||||
| 			args: args{ | ||||
| 				s:    "{{ requiredEnv \"HF_TEST_NONE\" }}", | ||||
| 				envs: map[string]string{}, | ||||
| 			}, | ||||
| 			wantErr: true, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			for k, v := range tt.args.envs { | ||||
| 				err := os.Setenv(k, v) | ||||
| 				if err != nil { | ||||
| 					t.Error("renderTemplateToString() could not set env var for testing") | ||||
| 				} | ||||
| 			} | ||||
| 			got, err := renderTemplateToString(tt.args.s) | ||||
| 			if (err != nil) != tt.wantErr { | ||||
| 				t.Errorf("renderTemplateToString() for %s error = %v, wantErr %v", tt.name, err, tt.wantErr) | ||||
| 				return | ||||
| 			} | ||||
| 			if got != tt.want { | ||||
| 				t.Errorf("renderTemplateToString() for %s = %v, want %v", tt.name, got, tt.want) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func Test_isLocalChart(t *testing.T) { | ||||
| 	type args struct { | ||||
| 		chart string | ||||
|  |  | |||
|  | @ -1,15 +1,5 @@ | |||
| package tmpl | ||||
| 
 | ||||
| import "io/ioutil" | ||||
| 
 | ||||
| var DefaultContext *Context | ||||
| 
 | ||||
| func init() { | ||||
| 	DefaultContext = &Context{ | ||||
| 		readFile: ioutil.ReadFile, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type Context struct { | ||||
| 	readFile func(string) ([]byte, error) | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,43 @@ | |||
| package tmpl | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"io/ioutil" | ||||
| ) | ||||
| 
 | ||||
| var DefaultFileRenderer *templateFileRenderer | ||||
| 
 | ||||
| func init() { | ||||
| 	DefaultFileRenderer = NewFileRenderer(ioutil.ReadFile) | ||||
| } | ||||
| 
 | ||||
| type templateFileRenderer struct { | ||||
| 	ReadFile func(string) ([]byte, error) | ||||
| 	Context  *Context | ||||
| } | ||||
| 
 | ||||
| type FileRenderer interface { | ||||
| 	RenderTemplateFileToBuffer(file string) (*bytes.Buffer, error) | ||||
| } | ||||
| 
 | ||||
| func NewFileRenderer(readFile func(filename string) ([]byte, error)) *templateFileRenderer { | ||||
| 	return &templateFileRenderer{ | ||||
| 		ReadFile: readFile, | ||||
| 		Context: &Context{ | ||||
| 			readFile: readFile, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (r *templateFileRenderer) RenderTemplateFileToBuffer(file string) (*bytes.Buffer, error) { | ||||
| 	content, err := r.ReadFile(file) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return r.Context.RenderTemplateToBuffer(string(content)) | ||||
| } | ||||
| 
 | ||||
| func RenderTemplateFileToBuffer(file string) (*bytes.Buffer, error) { | ||||
| 	return DefaultFileRenderer.RenderTemplateFileToBuffer(file) | ||||
| } | ||||
|  | @ -2,11 +2,12 @@ package tmpl | |||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestRenderTemplate(t *testing.T) { | ||||
| func TestRenderTemplate_Values(t *testing.T) { | ||||
| 	valuesYamlContent := `foo: | ||||
|   bar: BAR | ||||
| ` | ||||
|  | @ -29,3 +30,130 @@ func TestRenderTemplate(t *testing.T) { | |||
| 		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) | ||||
| 	}} | ||||
| 	tplString, err := ctx.RenderTemplateToBuffer(s) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return tplString.String(), nil | ||||
| } | ||||
| 
 | ||||
| func Test_renderTemplateToString(t *testing.T) { | ||||
| 	type args struct { | ||||
| 		s    string | ||||
| 		envs map[string]string | ||||
| 	} | ||||
| 	tests := []struct { | ||||
| 		name    string | ||||
| 		args    args | ||||
| 		want    string | ||||
| 		wantErr bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "simple replacement", | ||||
| 			args: args{ | ||||
| 				s: "{{ env \"HF_TEST_VAR\" }}", | ||||
| 				envs: map[string]string{ | ||||
| 					"HF_TEST_VAR": "content", | ||||
| 				}, | ||||
| 			}, | ||||
| 			want:    "content", | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "two replacements", | ||||
| 			args: args{ | ||||
| 				s: "{{ env \"HF_TEST_ALPHA\" }}{{ env \"HF_TEST_BETA\" }}", | ||||
| 				envs: map[string]string{ | ||||
| 					"HF_TEST_ALPHA": "first", | ||||
| 					"HF_TEST_BETA":  "second", | ||||
| 				}, | ||||
| 			}, | ||||
| 			want:    "firstsecond", | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "replacement and comment", | ||||
| 			args: args{ | ||||
| 				s: "{{ env \"HF_TEST_ALPHA\" }}{{/* comment */}}", | ||||
| 				envs: map[string]string{ | ||||
| 					"HF_TEST_ALPHA": "first", | ||||
| 				}, | ||||
| 			}, | ||||
| 			want:    "first", | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "global template function", | ||||
| 			args: args{ | ||||
| 				s: "{{ env \"HF_TEST_ALPHA\" | len }}", | ||||
| 				envs: map[string]string{ | ||||
| 					"HF_TEST_ALPHA": "abcdefg", | ||||
| 				}, | ||||
| 			}, | ||||
| 			want:    "7", | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "env var not set", | ||||
| 			args: args{ | ||||
| 				s: "{{ env \"HF_TEST_NONE\" }}", | ||||
| 				envs: map[string]string{ | ||||
| 					"HF_TEST_THIS": "first", | ||||
| 				}, | ||||
| 			}, | ||||
| 			want: "", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "undefined function", | ||||
| 			args: args{ | ||||
| 				s: "{{ env foo }}", | ||||
| 				envs: map[string]string{ | ||||
| 					"foo": "bar", | ||||
| 				}, | ||||
| 			}, | ||||
| 			wantErr: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "required env var", | ||||
| 			args: args{ | ||||
| 				s: "{{ requiredEnv \"HF_TEST\" }}", | ||||
| 				envs: map[string]string{ | ||||
| 					"HF_TEST": "value", | ||||
| 				}, | ||||
| 			}, | ||||
| 			want:    "value", | ||||
| 			wantErr: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "required env var not set", | ||||
| 			args: args{ | ||||
| 				s:    "{{ requiredEnv \"HF_TEST_NONE\" }}", | ||||
| 				envs: map[string]string{}, | ||||
| 			}, | ||||
| 			wantErr: true, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			for k, v := range tt.args.envs { | ||||
| 				err := os.Setenv(k, v) | ||||
| 				if err != nil { | ||||
| 					t.Error("renderTemplateToString() could not set env var for testing") | ||||
| 				} | ||||
| 			} | ||||
| 			got, err := renderTemplateToString(tt.args.s) | ||||
| 			if (err != nil) != tt.wantErr { | ||||
| 				t.Errorf("renderTemplateToString() for %s error = %v, wantErr %v", tt.name, err, tt.wantErr) | ||||
| 				return | ||||
| 			} | ||||
| 			if got != tt.want { | ||||
| 				t.Errorf("renderTemplateToString() for %s = %v, want %v", tt.name, got, tt.want) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,39 @@ | |||
| package valuesfile | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/roboll/helmfile/tmpl" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| type renderer struct { | ||||
| 	readFile         func(string) ([]byte, error) | ||||
| 	tmplFileRenderer tmpl.FileRenderer | ||||
| } | ||||
| 
 | ||||
| func NewRenderer(readFile func(filename string) ([]byte, error)) *renderer { | ||||
| 	return &renderer{ | ||||
| 		readFile:         readFile, | ||||
| 		tmplFileRenderer: tmpl.NewFileRenderer(readFile), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (r *renderer) RenderToBytes(path string) ([]byte, error) { | ||||
| 	var yamlBytes []byte | ||||
| 	splits := strings.Split(path, ".") | ||||
| 	if len(splits) > 0 && splits[len(splits)-1] == "gotmpl" { | ||||
| 		yamlBuf, err := r.tmplFileRenderer.RenderTemplateFileToBuffer(path) | ||||
| 		if err != nil { | ||||
| 
 | ||||
| 			return nil, fmt.Errorf("failed to render [%s], because of %v", path, err) | ||||
| 		} | ||||
| 		yamlBytes = yamlBuf.Bytes() | ||||
| 	} else { | ||||
| 		var err error | ||||
| 		yamlBytes, err = r.readFile(path) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("failed to load [%s]: %v", path, err) | ||||
| 		} | ||||
| 	} | ||||
| 	return yamlBytes, nil | ||||
| } | ||||
|  | @ -0,0 +1,61 @@ | |||
| package valuesfile | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestRenderToBytes_Gotmpl(t *testing.T) { | ||||
| 	valuesYamlTmplContent := `foo: | ||||
|   bar: '{{ readFile "data.txt" }}' | ||||
| ` | ||||
| 	dataFileContent := "FOO_BAR" | ||||
| 	expected := `foo: | ||||
|   bar: 'FOO_BAR' | ||||
| ` | ||||
| 	dataFile := "data.txt" | ||||
| 	valuesTmplFile := "values.yaml.gotmpl" | ||||
| 	r := NewRenderer(func(filename string) ([]byte, error) { | ||||
| 		switch filename { | ||||
| 		case valuesTmplFile: | ||||
| 			return []byte(valuesYamlTmplContent), nil | ||||
| 		case dataFile: | ||||
| 			return []byte(dataFileContent), nil | ||||
| 		} | ||||
| 		return nil, fmt.Errorf("unexpected filename: expected=%v or %v, actual=%s", dataFile, valuesTmplFile, filename) | ||||
| 	}) | ||||
| 	buf, err := r.RenderToBytes(valuesTmplFile) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("unexpected error: %v", err) | ||||
| 	} | ||||
| 	actual := string(buf) | ||||
| 	if !reflect.DeepEqual(actual, expected) { | ||||
| 		t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestRenderToBytes_Yaml(t *testing.T) { | ||||
| 	valuesYamlContent := `foo: | ||||
|   bar: '{{ readFile "data.txt" }}' | ||||
| ` | ||||
| 	expected := `foo: | ||||
|   bar: '{{ readFile "data.txt" }}' | ||||
| ` | ||||
| 	valuesFile := "values.yaml" | ||||
| 	r := NewRenderer(func(filename string) ([]byte, error) { | ||||
| 		switch filename { | ||||
| 		case valuesFile: | ||||
| 			return []byte(valuesYamlContent), nil | ||||
| 		} | ||||
| 		return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", valuesFile, filename) | ||||
| 	}) | ||||
| 	buf, err := r.RenderToBytes(valuesFile) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("unexpected error: %v", err) | ||||
| 	} | ||||
| 	actual := string(buf) | ||||
| 	if !reflect.DeepEqual(actual, expected) { | ||||
| 		t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual) | ||||
| 	} | ||||
| } | ||||
		Loading…
	
		Reference in New Issue