Skip to content

Commit 1b47dd8

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

File tree

5 files changed

+398
-4
lines changed

5 files changed

+398
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
base:
2+
image: $IMAGE

integration/basic_test.go

+60
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,66 @@ func (s *RunSuite) TestHelloWorld(c *C) {
4949
c.Assert(cn.Name, Equals, "/"+name)
5050
}
5151

52+
func (s *RunSuite) TestInterpolation(c *C) {
53+
os.Setenv("IMAGE", "tianon/true")
54+
55+
p := s.CreateProjectFromText(c, `
56+
test:
57+
image: $IMAGE
58+
`)
59+
60+
name := fmt.Sprintf("%s_%s_1", p, "test")
61+
testContainer := s.GetContainerByName(c, name)
62+
63+
p = s.CreateProjectFromText(c, `
64+
reference:
65+
image: tianon/true
66+
`)
67+
68+
name = fmt.Sprintf("%s_%s_1", p, "reference")
69+
referenceContainer := s.GetContainerByName(c, name)
70+
71+
c.Assert(testContainer, NotNil)
72+
73+
c.Assert(referenceContainer.Image, Equals, testContainer.Image)
74+
75+
os.Unsetenv("IMAGE")
76+
}
77+
78+
func (s *RunSuite) TestInterpolationWithExtends(c *C) {
79+
os.Setenv("IMAGE", "tianon/true")
80+
os.Setenv("TEST_PORT", "8000")
81+
82+
p := s.CreateProjectFromText(c, `
83+
test:
84+
extends:
85+
file: ./assets/interpolation/docker-compose.yml
86+
service: base
87+
ports:
88+
- ${TEST_PORT}
89+
`)
90+
91+
name := fmt.Sprintf("%s_%s_1", p, "test")
92+
testContainer := s.GetContainerByName(c, name)
93+
94+
p = s.CreateProjectFromText(c, `
95+
reference:
96+
image: tianon/true
97+
ports:
98+
- 8000
99+
`)
100+
101+
name = fmt.Sprintf("%s_%s_1", p, "reference")
102+
referenceContainer := s.GetContainerByName(c, name)
103+
104+
c.Assert(testContainer, NotNil)
105+
106+
c.Assert(referenceContainer.Image, Equals, testContainer.Image)
107+
108+
os.Unsetenv("TEST_PORT")
109+
os.Unsetenv("IMAGE")
110+
}
111+
52112
func (s *RunSuite) TestUp(c *C) {
53113
p := s.ProjectFromText(c, "up", SimpleTemplate)
54114

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+
"strings"
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+
38+
if bufferString == "" {
39+
return "", 0, false
40+
}
41+
42+
return mapping(buffer.String()), pos, true
43+
case c == '_' || (c >= 'A' && c <= 'Z'):
44+
buffer.WriteByte(c)
45+
default:
46+
return "", 0, false
47+
}
48+
}
49+
50+
return "", 0, false
51+
}
52+
53+
func parseInterpolationExpression(line string, pos int, mapping func(string) string) (string, int, bool) {
54+
c := line[pos]
55+
56+
switch {
57+
case c == '$':
58+
return "$", pos, true
59+
case c == '{':
60+
return parseVariableWithBraces(line, pos+1, mapping)
61+
case c >= 'A' && c <= 'Z':
62+
return parseVariable(line, pos, mapping)
63+
default:
64+
return "", 0, false
65+
}
66+
}
67+
68+
func parseLine(line string, mapping func(string) string) (string, bool) {
69+
var buffer bytes.Buffer
70+
71+
for pos := 0; pos < len(line); pos++ {
72+
c := line[pos]
73+
switch {
74+
case c == '$':
75+
var replaced string
76+
var success bool
77+
78+
replaced, pos, success = parseInterpolationExpression(line, pos+1, mapping)
79+
80+
if !success {
81+
return "", false
82+
}
83+
84+
buffer.WriteString(replaced)
85+
default:
86+
buffer.WriteByte(c)
87+
}
88+
}
89+
90+
return buffer.String(), true
91+
}
92+
93+
func parseConfig(option, service string, data *interface{}, mapping func(string) string) error {
94+
switch typedData := (*data).(type) {
95+
case string:
96+
var success bool
97+
*data, success = parseLine(typedData, mapping)
98+
99+
if !success {
100+
return fmt.Errorf("Invalid interpolation format for \"%s\" option in service \"%s\": \"%s\"", option, service, typedData)
101+
}
102+
case []interface{}:
103+
for k, v := range typedData {
104+
err := parseConfig(option, service, &v, mapping)
105+
106+
if err != nil {
107+
return err
108+
}
109+
110+
typedData[k] = v
111+
}
112+
case map[interface{}]interface{}:
113+
for k, v := range typedData {
114+
err := parseConfig(option, service, &v, mapping)
115+
116+
if err != nil {
117+
return err
118+
}
119+
120+
typedData[k] = v
121+
}
122+
}
123+
124+
return nil
125+
}
126+
127+
func interpolate(environmentLookup EnvironmentLookup, config *rawServiceMap) error {
128+
for k, v := range *config {
129+
for k2, v2 := range v {
130+
err := parseConfig(k2, k, &v2, func(s string) string {
131+
values := environmentLookup.Lookup(s, k, nil)
132+
133+
if len(values) == 0 {
134+
logrus.Warnf("The %s variable is not set. Substituting a blank string.", s)
135+
return ""
136+
}
137+
138+
return strings.Split(values[0], "=")[1]
139+
})
140+
141+
if err != nil {
142+
return err
143+
}
144+
145+
(*config)[k][k2] = v2
146+
}
147+
}
148+
149+
return nil
150+
}

project/interpolation_test.go

+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package project
2+
3+
import (
4+
"fmt"
5+
"github.com/stretchr/testify/assert"
6+
"gopkg.in/yaml.v2"
7+
"os"
8+
"testing"
9+
)
10+
11+
func testInterpolatedLine(t *testing.T, expectedLine, interpolatedLine string, envVariables map[string]string) {
12+
interpolatedLine, _ = parseLine(interpolatedLine, func(s string) string {
13+
return envVariables[s]
14+
})
15+
16+
assert.Equal(t, expectedLine, interpolatedLine)
17+
}
18+
19+
func testInvalidInterpolatedLine(t *testing.T, line string) {
20+
_, success := parseLine(line, func(string) string {
21+
return ""
22+
})
23+
24+
assert.Equal(t, false, success)
25+
}
26+
27+
func TestParseLine(t *testing.T) {
28+
variables := map[string]string{
29+
"A": "ABC",
30+
"X": "XYZ",
31+
"E": "",
32+
}
33+
34+
testInterpolatedLine(t, "ABC", "$A", variables)
35+
testInterpolatedLine(t, "ABC", "${A}", variables)
36+
37+
testInterpolatedLine(t, "ABC DE", "$A DE", variables)
38+
testInterpolatedLine(t, "ABCDE", "${A}DE", variables)
39+
40+
testInterpolatedLine(t, "$A", "$$A", variables)
41+
testInterpolatedLine(t, "${A}", "$${A}", variables)
42+
43+
testInterpolatedLine(t, "$ABC", "$$${A}", variables)
44+
testInterpolatedLine(t, "$ABC", "$$$A", variables)
45+
46+
testInterpolatedLine(t, "ABC XYZ", "$A $X", variables)
47+
testInterpolatedLine(t, "ABCXYZ", "$A$X", variables)
48+
testInterpolatedLine(t, "ABCXYZ", "${A}${X}", variables)
49+
50+
testInterpolatedLine(t, "", "$B", variables)
51+
testInterpolatedLine(t, "", "${B}", variables)
52+
testInterpolatedLine(t, "", "$ADE", variables)
53+
54+
testInterpolatedLine(t, "", "$E", variables)
55+
testInterpolatedLine(t, "", "${E}", variables)
56+
57+
testInvalidInterpolatedLine(t, "${")
58+
testInvalidInterpolatedLine(t, "$}")
59+
testInvalidInterpolatedLine(t, "${}")
60+
testInvalidInterpolatedLine(t, "${ }")
61+
testInvalidInterpolatedLine(t, "${A }")
62+
testInvalidInterpolatedLine(t, "${ A}")
63+
testInvalidInterpolatedLine(t, "${A!}")
64+
testInvalidInterpolatedLine(t, "$!")
65+
}
66+
67+
type MockEnvironmentLookup struct {
68+
Variables map[string]string
69+
}
70+
71+
func (m MockEnvironmentLookup) Lookup(key, serviceName string, config *ServiceConfig) []string {
72+
return []string{fmt.Sprintf("%s=%s", key, m.Variables[key])}
73+
}
74+
75+
func testInterpolatedConfig(t *testing.T, expectedConfig, interpolatedConfig string, envVariables map[string]string) {
76+
for k, v := range envVariables {
77+
os.Setenv(k, v)
78+
}
79+
80+
expectedConfigBytes := []byte(expectedConfig)
81+
interpolatedConfigBytes := []byte(interpolatedConfig)
82+
83+
expectedData := make(rawServiceMap)
84+
interpolatedData := make(rawServiceMap)
85+
86+
yaml.Unmarshal(expectedConfigBytes, &expectedData)
87+
yaml.Unmarshal(interpolatedConfigBytes, &interpolatedData)
88+
89+
_ = interpolate(MockEnvironmentLookup{envVariables}, &interpolatedData)
90+
91+
for k := range envVariables {
92+
os.Unsetenv(k)
93+
}
94+
95+
assert.Equal(t, expectedData, interpolatedData)
96+
}
97+
98+
func testInvalidInterpolatedConfig(t *testing.T, interpolatedConfig string) {
99+
interpolatedConfigBytes := []byte(interpolatedConfig)
100+
interpolatedData := make(rawServiceMap)
101+
yaml.Unmarshal(interpolatedConfigBytes, &interpolatedData)
102+
103+
err := interpolate(new(MockEnvironmentLookup), &interpolatedData)
104+
105+
assert.NotNil(t, err)
106+
}
107+
108+
func TestInterpolate(t *testing.T) {
109+
testInterpolatedConfig(t,
110+
`web:
111+
# unbracketed name
112+
image: busybox
113+
114+
# array element
115+
ports:
116+
- "80:8000"
117+
118+
# dictionary item value
119+
labels:
120+
mylabel: "myvalue"
121+
122+
# unset value
123+
hostname: "host-"
124+
125+
# escaped interpolation
126+
command: "${ESCAPED}"`,
127+
`web:
128+
# unbracketed name
129+
image: $IMAGE
130+
131+
# array element
132+
ports:
133+
- "${HOST_PORT}:8000"
134+
135+
# dictionary item value
136+
labels:
137+
mylabel: "${LABEL_VALUE}"
138+
139+
# unset value
140+
hostname: "host-${UNSET_VALUE}"
141+
142+
# escaped interpolation
143+
command: "$${ESCAPED}"`, map[string]string{
144+
"IMAGE": "busybox",
145+
"HOST_PORT": "80",
146+
"LABEL_VALUE": "myvalue",
147+
})
148+
149+
testInvalidInterpolatedConfig(t,
150+
`web:
151+
image: "${"`)
152+
153+
testInvalidInterpolatedConfig(t,
154+
`web:
155+
image: busybox
156+
157+
# array element
158+
ports:
159+
- "${}:8000"`)
160+
161+
testInvalidInterpolatedConfig(t,
162+
`web:
163+
image: busybox
164+
165+
# array element
166+
ports:
167+
- "80:8000"
168+
169+
# dictionary item value
170+
labels:
171+
mylabel: "${ LABEL_VALUE}"`)
172+
}

0 commit comments

Comments
 (0)