-
Notifications
You must be signed in to change notification settings - Fork 191
Support interpolating environment variables #47
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
base: | ||
image: $IMAGE |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
package project | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/Sirupsen/logrus" | ||
) | ||
|
||
func parseVariable(line string, pos int, mapping func(string) string) (string, int, bool) { | ||
var buffer bytes.Buffer | ||
|
||
for ; pos < len(line); pos++ { | ||
c := line[pos] | ||
|
||
switch { | ||
case c == '_' || (c >= 'A' && c <= 'Z'): | ||
buffer.WriteByte(c) | ||
default: | ||
return mapping(buffer.String()), pos - 1, true | ||
} | ||
} | ||
|
||
return mapping(buffer.String()), pos, true | ||
} | ||
|
||
func parseVariableWithBraces(line string, pos int, mapping func(string) string) (string, int, bool) { | ||
var buffer bytes.Buffer | ||
|
||
for ; pos < len(line); pos++ { | ||
c := line[pos] | ||
|
||
switch { | ||
case c == '}': | ||
bufferString := buffer.String() | ||
|
||
if bufferString == "" { | ||
return "", 0, false | ||
} | ||
|
||
return mapping(buffer.String()), pos, true | ||
case c == '_' || (c >= 'A' && c <= 'Z'): | ||
buffer.WriteByte(c) | ||
default: | ||
return "", 0, false | ||
} | ||
} | ||
|
||
return "", 0, false | ||
} | ||
|
||
func parseInterpolationExpression(line string, pos int, mapping func(string) string) (string, int, bool) { | ||
c := line[pos] | ||
|
||
switch { | ||
case c == '$': | ||
return "$", pos, true | ||
case c == '{': | ||
return parseVariableWithBraces(line, pos+1, mapping) | ||
case c >= 'A' && c <= 'Z': | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is missing lowercase letters, which are valid in variable names. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I created #70 to address this. |
||
return parseVariable(line, pos, mapping) | ||
default: | ||
return "", 0, false | ||
} | ||
} | ||
|
||
func parseLine(line string, mapping func(string) string) (string, bool) { | ||
var buffer bytes.Buffer | ||
|
||
for pos := 0; pos < len(line); pos++ { | ||
c := line[pos] | ||
switch { | ||
case c == '$': | ||
var replaced string | ||
var success bool | ||
|
||
replaced, pos, success = parseInterpolationExpression(line, pos+1, mapping) | ||
|
||
if !success { | ||
return "", false | ||
} | ||
|
||
buffer.WriteString(replaced) | ||
default: | ||
buffer.WriteByte(c) | ||
} | ||
} | ||
|
||
return buffer.String(), true | ||
} | ||
|
||
func parseConfig(option, service string, data *interface{}, mapping func(string) string) error { | ||
switch typedData := (*data).(type) { | ||
case string: | ||
var success bool | ||
*data, success = parseLine(typedData, mapping) | ||
|
||
if !success { | ||
return fmt.Errorf("Invalid interpolation format for \"%s\" option in service \"%s\": \"%s\"", option, service, typedData) | ||
} | ||
case []interface{}: | ||
for k, v := range typedData { | ||
err := parseConfig(option, service, &v, mapping) | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
typedData[k] = v | ||
} | ||
case map[interface{}]interface{}: | ||
for k, v := range typedData { | ||
err := parseConfig(option, service, &v, mapping) | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
typedData[k] = v | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func interpolate(environmentLookup EnvironmentLookup, config *rawServiceMap) error { | ||
for k, v := range *config { | ||
for k2, v2 := range v { | ||
err := parseConfig(k2, k, &v2, func(s string) string { | ||
values := environmentLookup.Lookup(s, k, nil) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wasn't sure what to do with the third argument here since a ServiceConfig isn't created until after interpolation is performed. This works fine using the OsEnvLookup implementation, which is currently the only one, but it might not be a good idea in general. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that makes sense. I think it's fine to say that the serviceConfig could be nil and the impl of EnvironmentLookup so be programmed so. |
||
|
||
if len(values) == 0 { | ||
logrus.Warnf("The %s variable is not set. Substituting a blank string.", s) | ||
return "" | ||
} | ||
|
||
return strings.Split(values[0], "=")[1] | ||
}) | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
(*config)[k][k2] = v2 | ||
} | ||
} | ||
|
||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
package project | ||
|
||
import ( | ||
"fmt" | ||
"github.com/stretchr/testify/assert" | ||
"gopkg.in/yaml.v2" | ||
"os" | ||
"testing" | ||
) | ||
|
||
func testInterpolatedLine(t *testing.T, expectedLine, interpolatedLine string, envVariables map[string]string) { | ||
interpolatedLine, _ = parseLine(interpolatedLine, func(s string) string { | ||
return envVariables[s] | ||
}) | ||
|
||
assert.Equal(t, expectedLine, interpolatedLine) | ||
} | ||
|
||
func testInvalidInterpolatedLine(t *testing.T, line string) { | ||
_, success := parseLine(line, func(string) string { | ||
return "" | ||
}) | ||
|
||
assert.Equal(t, false, success) | ||
} | ||
|
||
func TestParseLine(t *testing.T) { | ||
variables := map[string]string{ | ||
"A": "ABC", | ||
"X": "XYZ", | ||
"E": "", | ||
} | ||
|
||
testInterpolatedLine(t, "ABC", "$A", variables) | ||
testInterpolatedLine(t, "ABC", "${A}", variables) | ||
|
||
testInterpolatedLine(t, "ABC DE", "$A DE", variables) | ||
testInterpolatedLine(t, "ABCDE", "${A}DE", variables) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would add a |
||
|
||
testInterpolatedLine(t, "$A", "$$A", variables) | ||
testInterpolatedLine(t, "${A}", "$${A}", variables) | ||
|
||
testInterpolatedLine(t, "$ABC", "$$${A}", variables) | ||
testInterpolatedLine(t, "$ABC", "$$$A", variables) | ||
|
||
testInterpolatedLine(t, "ABC XYZ", "$A $X", variables) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here, a |
||
testInterpolatedLine(t, "ABCXYZ", "$A$X", variables) | ||
testInterpolatedLine(t, "ABCXYZ", "${A}${X}", variables) | ||
|
||
testInterpolatedLine(t, "", "$B", variables) | ||
testInterpolatedLine(t, "", "${B}", variables) | ||
testInterpolatedLine(t, "", "$ADE", variables) | ||
|
||
testInterpolatedLine(t, "", "$E", variables) | ||
testInterpolatedLine(t, "", "${E}", variables) | ||
|
||
testInvalidInterpolatedLine(t, "${") | ||
testInvalidInterpolatedLine(t, "$}") | ||
testInvalidInterpolatedLine(t, "${}") | ||
testInvalidInterpolatedLine(t, "${ }") | ||
testInvalidInterpolatedLine(t, "${A }") | ||
testInvalidInterpolatedLine(t, "${ A}") | ||
testInvalidInterpolatedLine(t, "${A!}") | ||
testInvalidInterpolatedLine(t, "$!") | ||
} | ||
|
||
type MockEnvironmentLookup struct { | ||
Variables map[string]string | ||
} | ||
|
||
func (m MockEnvironmentLookup) Lookup(key, serviceName string, config *ServiceConfig) []string { | ||
return []string{fmt.Sprintf("%s=%s", key, m.Variables[key])} | ||
} | ||
|
||
func testInterpolatedConfig(t *testing.T, expectedConfig, interpolatedConfig string, envVariables map[string]string) { | ||
for k, v := range envVariables { | ||
os.Setenv(k, v) | ||
} | ||
|
||
expectedConfigBytes := []byte(expectedConfig) | ||
interpolatedConfigBytes := []byte(interpolatedConfig) | ||
|
||
expectedData := make(rawServiceMap) | ||
interpolatedData := make(rawServiceMap) | ||
|
||
yaml.Unmarshal(expectedConfigBytes, &expectedData) | ||
yaml.Unmarshal(interpolatedConfigBytes, &interpolatedData) | ||
|
||
_ = interpolate(MockEnvironmentLookup{envVariables}, &interpolatedData) | ||
|
||
for k := range envVariables { | ||
os.Unsetenv(k) | ||
} | ||
|
||
assert.Equal(t, expectedData, interpolatedData) | ||
} | ||
|
||
func testInvalidInterpolatedConfig(t *testing.T, interpolatedConfig string) { | ||
interpolatedConfigBytes := []byte(interpolatedConfig) | ||
interpolatedData := make(rawServiceMap) | ||
yaml.Unmarshal(interpolatedConfigBytes, &interpolatedData) | ||
|
||
err := interpolate(new(MockEnvironmentLookup), &interpolatedData) | ||
|
||
assert.NotNil(t, err) | ||
} | ||
|
||
func TestInterpolate(t *testing.T) { | ||
testInterpolatedConfig(t, | ||
`web: | ||
# unbracketed name | ||
image: busybox | ||
|
||
# array element | ||
ports: | ||
- "80:8000" | ||
|
||
# dictionary item value | ||
labels: | ||
mylabel: "myvalue" | ||
|
||
# unset value | ||
hostname: "host-" | ||
|
||
# escaped interpolation | ||
command: "${ESCAPED}"`, | ||
`web: | ||
# unbracketed name | ||
image: $IMAGE | ||
|
||
# array element | ||
ports: | ||
- "${HOST_PORT}:8000" | ||
|
||
# dictionary item value | ||
labels: | ||
mylabel: "${LABEL_VALUE}" | ||
|
||
# unset value | ||
hostname: "host-${UNSET_VALUE}" | ||
|
||
# escaped interpolation | ||
command: "$${ESCAPED}"`, map[string]string{ | ||
"IMAGE": "busybox", | ||
"HOST_PORT": "80", | ||
"LABEL_VALUE": "myvalue", | ||
}) | ||
|
||
testInvalidInterpolatedConfig(t, | ||
`web: | ||
image: "${"`) | ||
|
||
testInvalidInterpolatedConfig(t, | ||
`web: | ||
image: busybox | ||
|
||
# array element | ||
ports: | ||
- "${}:8000"`) | ||
|
||
testInvalidInterpolatedConfig(t, | ||
`web: | ||
image: busybox | ||
|
||
# array element | ||
ports: | ||
- "80:8000" | ||
|
||
# dictionary item value | ||
labels: | ||
mylabel: "${ LABEL_VALUE}"`) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add another test or include it in this one that validates that interpolation happens for extended templates meaning that the parent and child templates both have variables in them.