Skip to content
This repository has been archived by the owner on Apr 25, 2023. It is now read-only.

Merge template functions collected in Dev branch #21

Merged
merged 9 commits into from
Sep 21, 2015
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
6 changes: 5 additions & 1 deletion src/cmd/rocker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,11 @@ func buildCommand(c *cli.Context) {
}
}

cliVars := template.VarsFromStrings(c.StringSlice("var"))
cliVars, err := template.VarsFromStrings(c.StringSlice("var"))
if err != nil {
log.Fatal(err)
}

vars := template.Vars{}.Merge(cliVars)

// obtain git info about current directory
Expand Down
2 changes: 1 addition & 1 deletion src/rocker/build/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func (builder *Builder) Build() (imageID string, err error) {
}
defer fd.Close()

data, err := template.ProcessConfigTemplate(builder.Rockerfile, fd, builder.Vars.ToMapOfInterface(), map[string]interface{}{})
data, err := template.Process(builder.Rockerfile, fd, builder.Vars.ToMapOfInterface(), map[string]interface{}{})
if err != nil {
return err
}
Expand Down
61 changes: 61 additions & 0 deletions src/rocker/template/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,47 @@ This template will yield:
www.grammarly.com
```

### {{ json *anything* }} or {{ *anything* | json }}
Marshals given input to JSON.

Example:
```
ENV={{ .Env | json }}
```

This template will yield:
```
ENV={"USER":"johnsnow","DOCKER_MACHINE_NAME":"dev","PATH":"/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin",...}
```

### {{ yaml *anything* }} or {{ *anything* | yaml }}
Marshals given input to YAML.

Example:
```
{{ .Env | yaml }}
```

This template will yield:
```
USER: johnsnow
DOCKER_MACHINE_NAME: dev
PATH: /usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin
```

### {{ shell *string* }} or {{ *string* | shell }}
Escapes given string so it can be substituted to a shell command.

Example:
```Dockerfile
RUN echo {{ "hello\nworld" | shell }}
```

This template will yield:
```Dockerfile
RUN echo $'hello\nworld'
```

### {{ dump *anything* }}
Pretty-prints any variable. Useful for debugging.

Expand Down Expand Up @@ -73,6 +114,26 @@ Example:
HOME={{ .Env.HOME }}
```

# Load file content to a variable
This template engine also supports loading files content to a variables. `rocker` and `rocker-compose` support this through a command line parameters:

```bash
rocker build -var key=@key.pem
rocker-compose run -var key=@key.pem
```

If the file path is relative, it will be resolved according to the current working directory.

**Usage options:**

```
key=@relative/file/path.txt
key=@../another/relative/file/path.txt
key=@/absolute/file/path.txt
key=@~/.bash_history
key=\@keep_value_as_is
```

# Development

Please install pre-push git hook that will run tests before every push:
Expand Down
52 changes: 52 additions & 0 deletions src/rocker/template/shellarg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*-
* Copyright 2015 Grammarly, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package template

import (
"regexp"
"strings"
)

var (
complexShellArgRegex = regexp.MustCompile("(?i:[^a-z\\d_\\/:=-])")
leadingSingleQuotesRegex = regexp.MustCompile("^(?:'')+")
)

// EscapeShellarg escapes any string so it can be safely passed to a shell
func EscapeShellarg(value string) string {
// Nothing to escape, return as is
if !complexShellArgRegex.MatchString(value) {
return value
}

// escape all single quotes
value = "'" + strings.Replace(value, "'", "'\\''", -1) + "'"

// remove duplicated single quotes at the beginning
value = leadingSingleQuotesRegex.ReplaceAllString(value, "")

// remove non-escaped single-quote if there are enclosed between 2 escaped
value = strings.Replace(value, "\\'''", "\\'", -1)

// if the string contains new lines, then use bash $'string' representation
// to have the newline escape character
if strings.Contains(value, "\n") {
value = "$" + strings.Replace(value, "\n", "\\n", -1)
}

return value
}
51 changes: 51 additions & 0 deletions src/rocker/template/shellarg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*-
* Copyright 2015 Grammarly, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package template

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestEscapeShellarg_Basic(t *testing.T) {
t.Parallel()
assert.Equal(t, "Testing", EscapeShellarg("Testing"))
assert.Equal(t, "'Testing;'", EscapeShellarg("Testing;"))
}

func TestEscapeShellarg_Advanced(t *testing.T) {
t.Parallel()

assertions := map[string]string{
"hello\\nworld": "'hello\\nworld'",
"hello:world": "hello:world",
"--hello=world": "--hello=world",
"hello\\tworld": "'hello\\tworld'",
"hello\nworld": "$'hello\\nworld'",
"\thello\nworld'": "$'\thello\\nworld'\\'",
"hello world": "'hello world'",
"hello\\\\'": "'hello\\\\'\\'",
"'\\\\'world": "\\''\\\\'\\''world'",
"world\\": "'world\\'",
"'single'": "\\''single'\\'",
}

for k, v := range assertions {
assert.Equal(t, v, EscapeShellarg(k))
}
}
27 changes: 24 additions & 3 deletions src/rocker/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package template

import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
Expand All @@ -27,12 +28,13 @@ import (
"strings"
"text/template"

"github.com/go-yaml/yaml"
"github.com/kr/pretty"
)

// ProcessConfigTemplate renders config through the template processor.
// Process renders config through the template processor.
// vars and additional functions are acceptable.
func ProcessConfigTemplate(name string, reader io.Reader, vars Vars, funcs map[string]interface{}) (*bytes.Buffer, error) {
func Process(name string, reader io.Reader, vars Vars, funcs map[string]interface{}) (*bytes.Buffer, error) {

var buf bytes.Buffer
// read template
Expand All @@ -43,14 +45,17 @@ func ProcessConfigTemplate(name string, reader io.Reader, vars Vars, funcs map[s

// merge OS environment variables with the given Vars map
// todo: maybe, we need to make it configurable
vars["Env"] = VarsFromStrings(os.Environ())
vars["Env"] = ParseKvPairs(os.Environ())

// Populate functions
funcMap := map[string]interface{}{
"seq": seq,
"replace": replace,
"dump": dump,
"assert": assertFn,
"json": jsonFn,
"shell": EscapeShellarg,
"yaml": yamlFn,
}
for k, f := range funcs {
funcMap[k] = f
Expand Down Expand Up @@ -155,6 +160,22 @@ func assertFn(v interface{}) (string, error) {
return "", fmt.Errorf("Assertion failed")
}

func jsonFn(v interface{}) (string, error) {
data, err := json.Marshal(v)
if err != nil {
return "", err
}
return string(data), nil
}

func yamlFn(v interface{}) (string, error) {
data, err := yaml.Marshal(v)
if err != nil {
return "", err
}
return string(data), nil
}

func interfaceToInt(v interface{}) (int, error) {
switch v.(type) {
case int:
Expand Down
34 changes: 24 additions & 10 deletions src/rocker/template/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@ var (
}
)

func TestProcessConfigTemplate_Basic(t *testing.T) {
result, err := ProcessConfigTemplate("test", strings.NewReader("this is a test {{.mykey}}"), configTemplateVars, map[string]interface{}{})
func TestProcess_Basic(t *testing.T) {
result, err := Process("test", strings.NewReader("this is a test {{.mykey}}"), configTemplateVars, map[string]interface{}{})
if err != nil {
t.Fatal(err)
}
// fmt.Printf("Template result: %s\n", result)
assert.Equal(t, "this is a test myval", result.String(), "template should be rendered")
}

func TestProcessConfigTemplate_Seq(t *testing.T) {
func TestProcess_Seq(t *testing.T) {
assert.Equal(t, "[1 2 3 4 5]", processTemplate(t, "{{ seq 1 5 1 }}"))
assert.Equal(t, "[0 1 2 3 4]", processTemplate(t, "{{ seq 0 4 1 }}"))
assert.Equal(t, "[1 3 5]", processTemplate(t, "{{ seq 1 5 2 }}"))
Expand Down Expand Up @@ -75,35 +75,49 @@ func TestProcessConfigTemplate_Seq(t *testing.T) {
assert.Equal(t, "[1 2 3 4 5]", processTemplate(t, "{{ seq .n }}"))
}

func TestProcessConfigTemplate_Replace(t *testing.T) {
func TestProcess_Replace(t *testing.T) {
assert.Equal(t, "url-com-", processTemplate(t, `{{ replace "url.com." "." "-" }}`))
assert.Equal(t, "url", processTemplate(t, `{{ replace "url" "*" "l" }}`))
assert.Equal(t, "krl", processTemplate(t, `{{ replace "url" "u" "k" }}`))
}

func TestProcessConfigTemplate_Env(t *testing.T) {
func TestProcess_Env(t *testing.T) {
env := os.Environ()
kv := strings.SplitN(env[0], "=", 2)
assert.Equal(t, kv[1], processTemplate(t, fmt.Sprintf("{{ .Env.%s }}", kv[0])))
}

func TestProcessConfigTemplate_Dump(t *testing.T) {
func TestProcess_Dump(t *testing.T) {
assert.Equal(t, `map[string]string{"foo":"bar"}`, processTemplate(t, "{{ dump .data }}"))
}

func TestProcessConfigTemplate_AssertSuccess(t *testing.T) {
func TestProcess_AssertSuccess(t *testing.T) {
assert.Equal(t, "output", processTemplate(t, "{{ assert true }}output"))
}

func TestProcessConfigTemplate_AssertFail(t *testing.T) {
func TestProcess_AssertFail(t *testing.T) {
tpl := "{{ assert .Version }}lololo"
_, err := ProcessConfigTemplate("test", strings.NewReader(tpl), configTemplateVars, map[string]interface{}{})
_, err := Process("test", strings.NewReader(tpl), configTemplateVars, map[string]interface{}{})
errStr := "Error executing template test, error: template: test:1:3: executing \"test\" at <assert .Version>: error calling assert: Assertion failed"
assert.Equal(t, errStr, err.Error())
}

func TestProcess_Json(t *testing.T) {
assert.Equal(t, "key: {\"foo\":\"bar\"}", processTemplate(t, "key: {{ .data | json }}"))
}

func TestProcess_Shellarg(t *testing.T) {
assert.Equal(t, "echo 'hello world'", processTemplate(t, "echo {{ \"hello world\" | shell }}"))
}

func TestProcess_Yaml(t *testing.T) {
assert.Equal(t, "key: foo: bar\n", processTemplate(t, "key: {{ .data | yaml }}"))
assert.Equal(t, "key: myval\n", processTemplate(t, "key: {{ .mykey | yaml }}"))
assert.Equal(t, "key: |-\n hello\n world\n", processTemplate(t, "key: {{ \"hello\\nworld\" | yaml }}"))
}

func processTemplate(t *testing.T, tpl string) string {
result, err := ProcessConfigTemplate("test", strings.NewReader(tpl), configTemplateVars, map[string]interface{}{})
result, err := Process("test", strings.NewReader(tpl), configTemplateVars, map[string]interface{}{})
if err != nil {
t.Fatal(err)
}
Expand Down
1 change: 1 addition & 0 deletions src/rocker/template/testdata/content.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello
Loading