fix: Keep backward-compatibility broken after introduction of values file template (#257)

Fixes #249
This commit is contained in:
KUOKA Yusuke 2018-08-30 16:57:37 +09:00 committed by GitHub
parent 9b71c64ef2
commit b3ebd4cdd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 287 additions and 170 deletions

View File

@ -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:

View File

@ -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
}

View File

@ -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)
}

View File

@ -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

View File

@ -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)
}

43
tmpl/file.go Normal file
View File

@ -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)
}

View 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)
}
})
}
}

39
valuesfile/valuesfile.go Normal file
View File

@ -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
}

View File

@ -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)
}
}