Skip to content

Commit

Permalink
bosh create-env allows variable interpolation in middle of value
Browse files Browse the repository at this point in the history
- the `agent.mbus` property is a good example of the usefulness of this
feature

```yaml
properties:
  agent:
    # final value = nats://nats:[email protected]:4222
    mbus: "nats://nats:((nats_password))@((private_ip)):4222"
```

[#134030589](https://www.pivotaltracker.com/story/show/134030589)

Signed-off-by: Brian Cunnie <[email protected]>
  • Loading branch information
ljfranklin authored and dpb587-pivotal committed Nov 11, 2016
1 parent 210bc19 commit 0a80a1d
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 39 deletions.
80 changes: 54 additions & 26 deletions director/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"gopkg.in/yaml.v2"
)

var templateFormatRegex = regexp.MustCompile(`^\(\((!?[-\w\p{L}]+)\)\)$`)
var templateFormatRegex = regexp.MustCompile(`\(\((!?[-\w\p{L}]+)\)\)`)

type Template struct {
bytes []byte
Expand Down Expand Up @@ -40,7 +40,10 @@ func (t Template) Evaluate(vars Variables, ops patch.Ops, opts EvaluateOpts) ([]

missingVars := map[string]struct{}{}

obj = t.interpolate(obj, vars, opts, missingVars)
obj, err = t.interpolate(obj, vars, opts, missingVars)
if err != nil {
return []byte{}, err
}

if len(missingVars) > 0 {
var missingVarKeys []string
Expand Down Expand Up @@ -68,54 +71,79 @@ func (t Template) Evaluate(vars Variables, ops patch.Ops, opts EvaluateOpts) ([]
return bytes, nil
}

func (t Template) interpolate(node interface{}, vars Variables, opts EvaluateOpts, missingVars map[string]struct{}) interface{} {
func (t Template) interpolate(node interface{}, vars Variables, opts EvaluateOpts, missingVars map[string]struct{}) (interface{}, error) {
switch node.(type) {
case map[interface{}]interface{}:
nodeMap := node.(map[interface{}]interface{})

for k, v := range nodeMap {
evaluatedValue := t.interpolate(v, vars, opts, missingVars)

if keyAsString, ok := k.(string); ok {
if key, eval := t.needsEvaluation(keyAsString); eval {
if foundVarKey, exists := vars[key]; exists {
delete(nodeMap, k)
k = foundVarKey
} else if opts.ExpectAllKeys {
missingVars[key] = struct{}{}
}
}
evaluatedValue, err := t.interpolate(v, vars, opts, missingVars)
if err != nil {
return nil, err
}

nodeMap[k] = evaluatedValue
evaluatedKey, err := t.interpolate(k, vars, opts, missingVars)
if err != nil {
return nil, err
}
delete(nodeMap, k) // delete in case key has changed
nodeMap[evaluatedKey] = evaluatedValue
}

case []interface{}:
nodeArray := node.([]interface{})

for i, x := range nodeArray {
nodeArray[i] = t.interpolate(x, vars, opts, missingVars)
var err error
nodeArray[i], err = t.interpolate(x, vars, opts, missingVars)
if err != nil {
return nil, err
}
}

case string:
if key, found := t.needsEvaluation(node.(string)); found {
nodeString := node.(string)

for key, placeholders := range t.keysToPlaceholders(nodeString) {
if foundVar, exists := vars[key]; exists {
return foundVar
// ensure that value type is preserved when replacing the entire field
replaceEntireField := (nodeString == fmt.Sprintf("((%s))", key) || nodeString == fmt.Sprintf("((!%s))", key))
if replaceEntireField {
return foundVar, nil
}

for _, placeholder := range placeholders {
switch foundVar.(type) {
case string, int, int16, int32, int64, uint, uint16, uint32, uint64:
nodeString = strings.Replace(nodeString, placeholder, fmt.Sprintf("%v", foundVar), 1)
default:
return nil, fmt.Errorf("Invalid type '%T' for value '%v' and key '%s'. Supported types for interpolation within a string are integers and strings.", foundVar, foundVar, key)
}
}
} else if opts.ExpectAllKeys {
missingVars[key] = struct{}{}
}
}

return nodeString, nil
}

return node
return node, nil
}

func (t Template) needsEvaluation(value string) (string, bool) {
found := templateFormatRegex.FindAllSubmatch([]byte(value), 1)
func (t Template) keysToPlaceholders(value string) map[string][]string {
matches := templateFormatRegex.FindAllSubmatch([]byte(value), -1)

keysToPlaceholders := map[string][]string{}
if matches == nil {
return keysToPlaceholders
}

if len(found) != 0 && len(found[0]) != 0 {
return strings.TrimPrefix(string(found[0][1]), "!"), true
for _, match := range matches {
capture := match[1]
key := strings.TrimPrefix(string(capture), "!")
if len(key) > 0 {
keysToPlaceholders[key] = append(keysToPlaceholders[key], fmt.Sprintf("((%s))", capture))
}
}

return "", false
return keysToPlaceholders
}
94 changes: 81 additions & 13 deletions director/template/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,28 @@ name6:
`)))
})

It("can interpolate different data types into a byte slice with !key", func() {
template := NewTemplate([]byte("otherstuff: ((!boule))"))
vars := Variables{"boule": true}

result, err := template.Evaluate(vars, patch.Ops{}, EvaluateOpts{})
Expect(err).NotTo(HaveOccurred())
Expect(result).To(Equal([]byte("otherstuff: true\n")))
})

It("return errors if there are missing variable keys and ExpectAllKeys is true", func() {
template := NewTemplate([]byte(`
((key)): ((key2))
((key3)): 2
dup-key: ((key3))
array:
((key4))_array:
- ((key_in_array))
`))
vars := Variables{"key3": "foo"}

_, err := template.Evaluate(vars, patch.Ops{}, EvaluateOpts{ExpectAllKeys: true})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal("Expected to find variables: key, key2, key_in_array"))
Expect(err.Error()).To(Equal("Expected to find variables: key, key2, key4, key_in_array"))
})

It("does not return error if there are missing variable keys and ExpectAllKeys is false", func() {
Expand All @@ -91,17 +100,6 @@ array:
Expect(result).To(Equal([]byte("((key)): ((key2))\nfoo: 2\n")))
})

Context("When template is a string", func() {
It("returns it", func() {
template := NewTemplate([]byte(`"string with a ((key))"`))
vars := Variables{"key": "not key"}

result, err := template.Evaluate(vars, patch.Ops{}, EvaluateOpts{})
Expect(err).NotTo(HaveOccurred())
Expect(result).To(Equal([]byte("string with a ((key))\n")))
})
})

Context("When template is a number", func() {
It("returns it", func() {
template := NewTemplate([]byte(`1234`))
Expand Down Expand Up @@ -145,6 +143,76 @@ array:
Expect(result).To(Equal([]byte("dash: underscore\n")))
})

It("can interpolate a secret key in the middle of a string", func() {
template := NewTemplate([]byte("url: https://((ip))"))
vars := Variables{
"ip": "10.0.0.0",
}

result, err := template.Evaluate(vars, patch.Ops{}, EvaluateOpts{})
Expect(err).NotTo(HaveOccurred())
Expect(result).To(Equal([]byte("url: https://10.0.0.0\n")))
})

It("can interpolate multiple secret keys in the middle of a string", func() {
template := NewTemplate([]byte("uri: nats://nats:((password))@((ip)):4222"))
vars := Variables{
"password": "secret",
"ip": "10.0.0.0",
}

result, err := template.Evaluate(vars, patch.Ops{}, EvaluateOpts{})
Expect(err).NotTo(HaveOccurred())
Expect(result).To(Equal([]byte("uri: nats://nats:[email protected]:4222\n")))
})

It("can interpolate multiple keys of type string and int in the middle of a string", func() {
template := NewTemplate([]byte("address: ((ip)):((port))"))
vars := Variables{
"port": 4222,
"ip": "10.0.0.0",
}

result, err := template.Evaluate(vars, patch.Ops{}, EvaluateOpts{})
Expect(err).NotTo(HaveOccurred())
Expect(result).To(Equal([]byte("address: 10.0.0.0:4222\n")))
})

It("raises error when interpolating an unsupported type in the middle of a string", func() {
template := NewTemplate([]byte("address: ((definition)):((eulers_number))"))
vars := Variables{
"eulers_number": 2.717,
"definition": "natural_log",
}

_, err := template.Evaluate(vars, patch.Ops{}, EvaluateOpts{})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("2.717"))
Expect(err.Error()).To(ContainSubstring("eulers_number"))
})

It("can interpolate a single key multiple times in the middle of a string", func() {
template := NewTemplate([]byte("acct_and_password: ((user)):((user))"))
vars := Variables{
"user": "nats",
}

result, err := template.Evaluate(vars, patch.Ops{}, EvaluateOpts{})
Expect(err).NotTo(HaveOccurred())
Expect(result).To(Equal([]byte("acct_and_password: nats:nats\n")))
})

It("can interpolate values into the middle of a key", func() {
template := NewTemplate([]byte("((iaas))_cpi: props"))
vars := Variables{
"iaas": "aws",
}

result, err := template.Evaluate(vars, patch.Ops{}, EvaluateOpts{})
Expect(err).NotTo(HaveOccurred())
Expect(result).To(Equal([]byte("aws_cpi: props\n")))
})

It("can interpolate the same value multiple times into a byte slice", func() {
template := NewTemplate([]byte("((key)): ((key))"))
vars := Variables{"key": "foo"}
Expand Down

0 comments on commit 0a80a1d

Please sign in to comment.