diff --git a/README.md b/README.md index 01071945..2624e400 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/main.go b/main.go index 4ac73e80..6e4597d1 100644 --- a/main.go +++ b/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 } diff --git a/state/state.go b/state/state.go index aee8578e..81c2ab06 100644 --- a/state/state.go +++ b/state/state.go @@ -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) } diff --git a/state/state_test.go b/state/state_test.go index 4193da80..afe42d1e 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -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 diff --git a/tmpl/context.go b/tmpl/context.go index 77900a1c..0c7a5cb5 100644 --- a/tmpl/context.go +++ b/tmpl/context.go @@ -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) } diff --git a/tmpl/file.go b/tmpl/file.go new file mode 100644 index 00000000..a0109cab --- /dev/null +++ b/tmpl/file.go @@ -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) +} diff --git a/tmpl/tmpl_test.go b/tmpl/tmpl_test.go index 75687bfc..40ca6714 100644 --- a/tmpl/tmpl_test.go +++ b/tmpl/tmpl_test.go @@ -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) + } + }) + } +} diff --git a/valuesfile/valuesfile.go b/valuesfile/valuesfile.go new file mode 100644 index 00000000..fef72191 --- /dev/null +++ b/valuesfile/valuesfile.go @@ -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 +} diff --git a/valuesfile/valuesfile_test.go b/valuesfile/valuesfile_test.go new file mode 100644 index 00000000..f812d62e --- /dev/null +++ b/valuesfile/valuesfile_test.go @@ -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) + } +}