Skip to content

Commit 41d6bcc

Browse files
committed
Support interpolating environment variables
Fixes docker#40 Signed-off-by: Josh Curl <hello@joshcurl.com>
1 parent 19c18ef commit 41d6bcc

File tree

4 files changed

+342
-0
lines changed

4 files changed

+342
-0
lines changed

integration/basic_test.go

+26
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,32 @@ func (s *RunSuite) TestHelloWorld(c *C) {
3131
c.Assert(cn.Name, Equals, "/"+name)
3232
}
3333

34+
func (s *RunSuite) TestInterpolation(c *C) {
35+
os.Setenv("IMAGE", "tianon/true")
36+
37+
p := s.CreateProjectFromText(c, `
38+
reference:
39+
image: $IMAGE
40+
`)
41+
42+
name := fmt.Sprintf("%s_%s_1", p, "reference")
43+
referenceContainer := s.GetContainerByName(c, name)
44+
45+
p = s.CreateProjectFromText(c, `
46+
test:
47+
image: tianon/true
48+
`)
49+
50+
name = fmt.Sprintf("%s_%s_1", p, "test")
51+
testContainer := s.GetContainerByName(c, name)
52+
53+
c.Assert(testContainer, NotNil)
54+
55+
c.Assert(referenceContainer.Image, Equals, testContainer.Image)
56+
57+
os.Unsetenv("IMAGE")
58+
}
59+
3460
func (s *RunSuite) TestUp(c *C) {
3561
p := s.ProjectFromText(c, "up", SimpleTemplate)
3662

project/interpolation.go

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package project
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os"
7+
8+
"github.com/Sirupsen/logrus"
9+
)
10+
11+
func parseVariable(line string, pos int, mapping func(string) string) (string, int, bool) {
12+
var buffer bytes.Buffer
13+
14+
for ; pos < len(line); pos++ {
15+
c := line[pos]
16+
17+
switch {
18+
case c == '_' || (c >= 'A' && c <= 'Z'):
19+
buffer.WriteByte(c)
20+
default:
21+
return mapping(buffer.String()), pos - 1, true
22+
}
23+
}
24+
25+
return mapping(buffer.String()), pos, true
26+
}
27+
28+
func parseVariableWithBraces(line string, pos int, mapping func(string) string) (string, int, bool) {
29+
var buffer bytes.Buffer
30+
31+
for ; pos < len(line); pos++ {
32+
c := line[pos]
33+
34+
switch {
35+
case c == '}':
36+
bufferString := buffer.String()
37+
if bufferString == "" {
38+
return "", 0, false
39+
} else {
40+
return mapping(buffer.String()), pos, true
41+
}
42+
case c == '_' || (c >= 'A' && c <= 'Z'):
43+
buffer.WriteByte(c)
44+
default:
45+
return "", 0, false
46+
}
47+
}
48+
49+
return "", 0, false
50+
}
51+
52+
func parseInterpolationExpression(line string, pos int, mapping func(string) string) (string, int, bool) {
53+
c := line[pos]
54+
55+
switch {
56+
case c == '$':
57+
return "$", pos, true
58+
case c == '{':
59+
return parseVariableWithBraces(line, pos+1, mapping)
60+
case c >= 'A' && c <= 'Z':
61+
return parseVariable(line, pos, mapping)
62+
default:
63+
return "", 0, false
64+
}
65+
66+
return "", pos, true
67+
}
68+
69+
func parseLine(line string, mapping func(string) string) (string, bool) {
70+
var buffer bytes.Buffer
71+
72+
for pos := 0; pos < len(line); pos++ {
73+
c := line[pos]
74+
switch {
75+
case c == '$':
76+
var replaced string
77+
var success bool
78+
79+
replaced, pos, success = parseInterpolationExpression(line, pos+1, mapping)
80+
81+
if !success {
82+
return "", false
83+
}
84+
85+
buffer.WriteString(replaced)
86+
default:
87+
buffer.WriteByte(c)
88+
}
89+
}
90+
91+
return buffer.String(), true
92+
}
93+
94+
func interpolate(option, service string, data *interface{}, mapping func(string) string) error {
95+
switch typedData := (*data).(type) {
96+
case string:
97+
var success bool
98+
*data, success = parseLine(typedData, mapping)
99+
100+
if !success {
101+
return fmt.Errorf("Invalid interpolation format for \"%s\" option in service \"%s\": \"%s\"", option, service, typedData)
102+
}
103+
case []interface{}:
104+
for k, v := range typedData {
105+
err := interpolate(option, service, &v, mapping)
106+
107+
if err != nil {
108+
return err
109+
}
110+
111+
typedData[k] = v
112+
}
113+
case map[interface{}]interface{}:
114+
for k, v := range typedData {
115+
err := interpolate(option, service, &v, mapping)
116+
117+
if err != nil {
118+
return err
119+
}
120+
121+
typedData[k] = v
122+
}
123+
}
124+
125+
return nil
126+
}
127+
128+
func Interpolate(config *rawServiceMap) error {
129+
for k, v := range *config {
130+
for k2, v2 := range v {
131+
err := interpolate(k2, k, &v2, func(s string) string {
132+
value := os.Getenv(s)
133+
134+
if value == "" {
135+
logrus.Warnf("The %s variable is not set. Substituting a blank string.", s)
136+
}
137+
138+
return value
139+
})
140+
141+
if err != nil {
142+
return err
143+
}
144+
145+
(*config)[k][k2] = v2
146+
}
147+
}
148+
149+
return nil
150+
}
+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package project
2+
3+
import (
4+
"github.com/stretchr/testify/assert"
5+
"gopkg.in/yaml.v2"
6+
"os"
7+
"testing"
8+
)
9+
10+
func testInterpolatedLine(t *testing.T, expectedLine, interpolatedLine string, envVariables map[string]string) {
11+
interpolatedLine, _ = parseLine(interpolatedLine, func(s string) string {
12+
return envVariables[s]
13+
})
14+
15+
assert.Equal(t, expectedLine, interpolatedLine)
16+
}
17+
18+
func testInvalidInterpolatedLine(t *testing.T, line string) {
19+
_, success := parseLine(line, func(string) string {
20+
return ""
21+
})
22+
23+
assert.Equal(t, false, success)
24+
}
25+
26+
func TestParseLine(t *testing.T) {
27+
variables := map[string]string{
28+
"A": "ABC",
29+
"X": "XYZ",
30+
"E": "",
31+
}
32+
33+
testInterpolatedLine(t, "ABC", "$A", variables)
34+
testInterpolatedLine(t, "ABC", "${A}", variables)
35+
36+
testInterpolatedLine(t, "ABC DE", "$A DE", variables)
37+
testInterpolatedLine(t, "ABCDE", "${A}DE", variables)
38+
39+
testInterpolatedLine(t, "$A", "$$A", variables)
40+
testInterpolatedLine(t, "${A}", "$${A}", variables)
41+
42+
testInterpolatedLine(t, "$ABC", "$$${A}", variables)
43+
testInterpolatedLine(t, "$ABC", "$$$A", variables)
44+
45+
testInterpolatedLine(t, "ABC XYZ", "$A $X", variables)
46+
testInterpolatedLine(t, "ABCXYZ", "${A}${X}", variables)
47+
48+
testInterpolatedLine(t, "", "$B", variables)
49+
testInterpolatedLine(t, "", "${B}", variables)
50+
51+
testInterpolatedLine(t, "", "$E", variables)
52+
testInterpolatedLine(t, "", "${E}", variables)
53+
54+
testInvalidInterpolatedLine(t, "${")
55+
testInvalidInterpolatedLine(t, "$}")
56+
testInvalidInterpolatedLine(t, "${}")
57+
testInvalidInterpolatedLine(t, "${ }")
58+
testInvalidInterpolatedLine(t, "${A }")
59+
testInvalidInterpolatedLine(t, "${ A}")
60+
testInvalidInterpolatedLine(t, "${A!}")
61+
testInvalidInterpolatedLine(t, "$!")
62+
}
63+
64+
func testInterpolatedConfig(t *testing.T, expectedConfig, interpolatedConfig string, envVariables map[string]string) {
65+
for k, v := range envVariables {
66+
os.Setenv(k, v)
67+
}
68+
69+
expectedConfigBytes := []byte(expectedConfig)
70+
interpolatedConfigBytes := []byte(interpolatedConfig)
71+
72+
expectedData := make(rawServiceMap)
73+
interpolatedData := make(rawServiceMap)
74+
75+
yaml.Unmarshal(expectedConfigBytes, &expectedData)
76+
yaml.Unmarshal(interpolatedConfigBytes, &interpolatedData)
77+
78+
_ = Interpolate(&interpolatedData)
79+
80+
for k := range envVariables {
81+
os.Unsetenv(k)
82+
}
83+
84+
assert.Equal(t, expectedData, interpolatedData)
85+
}
86+
87+
func testInvalidInterpolatedConfig(t *testing.T, interpolatedConfig string) {
88+
interpolatedConfigBytes := []byte(interpolatedConfig)
89+
interpolatedData := make(rawServiceMap)
90+
yaml.Unmarshal(interpolatedConfigBytes, &interpolatedData)
91+
92+
err := Interpolate(&interpolatedData)
93+
94+
assert.NotNil(t, err)
95+
}
96+
97+
func TestInterpolate(t *testing.T) {
98+
testInterpolatedConfig(t,
99+
`web:
100+
# unbracketed name
101+
image: busybox
102+
103+
# array element
104+
ports:
105+
- "80:8000"
106+
107+
# dictionary item value
108+
labels:
109+
mylabel: "myvalue"
110+
111+
# unset value
112+
hostname: "host-"
113+
114+
# escaped interpolation
115+
command: "${ESCAPED}"`,
116+
`web:
117+
# unbracketed name
118+
image: $IMAGE
119+
120+
# array element
121+
ports:
122+
- "${HOST_PORT}:8000"
123+
124+
# dictionary item value
125+
labels:
126+
mylabel: "${LABEL_VALUE}"
127+
128+
# unset value
129+
hostname: "host-${UNSET_VALUE}"
130+
131+
# escaped interpolation
132+
command: "$${ESCAPED}"`, map[string]string{
133+
"IMAGE": "busybox",
134+
"HOST_PORT": "80",
135+
"LABEL_VALUE": "myvalue",
136+
})
137+
138+
testInvalidInterpolatedConfig(t,
139+
`web:
140+
image: "${"`)
141+
142+
testInvalidInterpolatedConfig(t,
143+
`web:
144+
image: busybox
145+
146+
# array element
147+
ports:
148+
- "${}:8000"`)
149+
150+
testInvalidInterpolatedConfig(t,
151+
`web:
152+
image: busybox
153+
154+
# array element
155+
ports:
156+
- "80:8000"
157+
158+
# dictionary item value
159+
labels:
160+
mylabel: "${ LABEL_VALUE}"`)
161+
}

project/merge.go

+5
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ func Merge(p *Project, bytes []byte) (map[string]*ServiceConfig, error) {
3838
logrus.Fatalf("Could not parse config for project %s : %v", p.Name, err)
3939
}
4040

41+
err = Interpolate(&datas)
42+
if err != nil {
43+
return nil, err
44+
}
45+
4146
for name, data := range datas {
4247
data, err := parse(p.context.ConfigLookup, p.File, data, datas)
4348
if err != nil {

0 commit comments

Comments
 (0)