Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(file) implement env var substitution for state files #286

Merged
merged 2 commits into from
Apr 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 42 additions & 6 deletions file/readfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
25 changes: 25 additions & 0 deletions file/readfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ func Test_getContent(t *testing.T) {
tests := []struct {
name string
args args
envVars map[string]string
want *Content
wantErr bool
}{
Expand Down Expand Up @@ -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{
{
Expand All @@ -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{
{
Expand Down Expand Up @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions file/testdata/bad-env-var/file.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
services:
- name: svc2
host: ${{ env "SVC2_HOST" }}
routes:
- name: r2
paths:
- /r2
plugins:
- name: prometheus
4 changes: 3 additions & 1 deletion file/testdata/file.yaml
Original file line number Diff line number Diff line change
@@ -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