diff --git a/file/readfile.go b/file/readfile.go index 0ddb42656..dc4981e05 100644 --- a/file/readfile.go +++ b/file/readfile.go @@ -2,11 +2,14 @@ package file import ( "bufio" + "bytes" + "fmt" "io" "io/ioutil" "os" "path/filepath" "strings" + "text/template" ghodss "github.com/ghodss/yaml" "github.com/imdario/mergo" @@ -80,22 +83,26 @@ func getReaders(fileOrDir string) ([]io.Reader, error) { // readContent reads all the byes until io.EOF and unmarshals the read // bytes into Content. func readContent(reader io.Reader) (*Content, error) { - var content Content - var bytes []byte var err error - bytes, err = ioutil.ReadAll(reader) + contentBytes, err := ioutil.ReadAll(reader) if err != nil { return nil, err } - err = validate(bytes) + renderedContent, err := renderTemplate(string(contentBytes)) + if err != nil { + return nil, fmt.Errorf("parsing file: %w", err) + } + renderedContentBytes := []byte(renderedContent) + err = validate(renderedContentBytes) if err != nil { return nil, errors.Wrap(err, "validating file content") } - err = yamlUnmarshal(bytes, &content) + var result Content + err = yamlUnmarshal(renderedContentBytes, &result) if err != nil { return nil, err } - return &content, nil + return &result, nil } // yamlUnmarshal is a wrapper around yaml.Unmarshal to ensure that the right @@ -132,3 +139,32 @@ func configFilesInDir(dir string) ([]string, error) { } return res, nil } + +func getPrefixedEnvVar(key string) (string, error) { + const envVarPrefix = "DECK_" + if !strings.HasPrefix(key, envVarPrefix) { + return "", fmt.Errorf("environment variables in the state file must "+ + "be prefixed with 'DECK_', found: '%s'", key) + } + value, exists := os.LookupEnv(key) + if !exists { + return "", fmt.Errorf("environment variable '%s' present in state file but not set", key) + } + return value, nil +} + +func renderTemplate(content string) (string, error) { + t := template.New("state").Funcs(template.FuncMap{ + "env": getPrefixedEnvVar, + }).Delims("${{", "}}") + t, err := t.Parse(content) + if err != nil { + return "", err + } + var buffer bytes.Buffer + err = t.Execute(&buffer, nil) + if err != nil { + return "", err + } + return buffer.String(), nil +} diff --git a/file/readfile_test.go b/file/readfile_test.go index 0ec7b15d5..0ddbd1594 100644 --- a/file/readfile_test.go +++ b/file/readfile_test.go @@ -131,6 +131,7 @@ func Test_getContent(t *testing.T) { tests := []struct { name string args args + envVars map[string]string want *Content wantErr bool }{ @@ -167,12 +168,16 @@ func Test_getContent(t *testing.T) { { name: "single file", args: args{[]string{"testdata/file.yaml"}}, + envVars: map[string]string{ + "DECK_SVC2_HOST": "2.example.com", + }, want: &Content{ Services: []FService{ { Service: kong.Service{ Name: kong.String("svc2"), Host: kong.String("2.example.com"), + Tags: kong.StringSlice("<"), }, Routes: []*FRoute{ { @@ -194,15 +199,29 @@ func Test_getContent(t *testing.T) { }, wantErr: false, }, + { + name: "environment variable present in file but not set", + args: args{[]string{"testdata/file.yaml"}}, + wantErr: true, + }, + { + name: "file with bad environment variable", + args: args{[]string{"testdata/bad-env-var/file.yaml"}}, + wantErr: true, + }, { name: "multiple files", args: args{[]string{"testdata/file.yaml", "testdata/file.json"}}, + envVars: map[string]string{ + "DECK_SVC2_HOST": "2.example.com", + }, want: &Content{ Services: []FService{ { Service: kong.Service{ Name: kong.String("svc2"), Host: kong.String("2.example.com"), + Tags: kong.StringSlice("<"), }, Routes: []*FRoute{ { @@ -304,6 +323,12 @@ func Test_getContent(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + for k, v := range tt.envVars { + os.Setenv(k, v) + defer func(k string) { + os.Unsetenv(k) + }(k) + } got, err := getContent(tt.args.filenames) if (err != nil) != tt.wantErr { t.Errorf("getContent() error = %v, wantErr %v", err, tt.wantErr) diff --git a/file/testdata/bad-env-var/file.yaml b/file/testdata/bad-env-var/file.yaml new file mode 100644 index 000000000..9795333df --- /dev/null +++ b/file/testdata/bad-env-var/file.yaml @@ -0,0 +1,9 @@ +services: +- name: svc2 + host: ${{ env "SVC2_HOST" }} + routes: + - name: r2 + paths: + - /r2 +plugins: +- name: prometheus diff --git a/file/testdata/file.yaml b/file/testdata/file.yaml index e58ba59ba..4d9d0ff62 100644 --- a/file/testdata/file.yaml +++ b/file/testdata/file.yaml @@ -1,9 +1,11 @@ services: - name: svc2 - host: 2.example.com + host: ${{ env "DECK_SVC2_HOST" }} routes: - name: r2 paths: - /r2 + tags: + - '<' # verifies that the templating engine does not perform character escaping plugins: - name: prometheus