Add sprig for command templates (#9053)

Adds the sprig functions to the template funcmap prepended with `sprig_` to match the behavior in consul-template
This commit is contained in:
Charlie Voiselle 2023-02-07 14:07:20 -05:00 committed by GitHub
parent 8cc212167b
commit 31a289891d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 87 additions and 63 deletions

3
.changelog/9053.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
cli: Added sprig function support for `-t` templates
```

View File

@ -3,9 +3,9 @@ package command
import (
"bytes"
"fmt"
"io"
"text/template"
"github.com/Masterminds/sprig/v3"
"github.com/hashicorp/go-msgpack/codec"
)
@ -36,14 +36,12 @@ func DataFormat(format, tmpl string) (DataFormatter, error) {
return nil, fmt.Errorf("Unsupported format is specified.")
}
type JSONFormat struct {
}
type JSONFormat struct{}
// TransformData returns JSON format string data.
func (p *JSONFormat) TransformData(data interface{}) (string, error) {
var buf bytes.Buffer
enc := codec.NewEncoder(&buf, jsonHandlePretty)
err := enc.Encode(data)
err := codec.NewEncoder(&buf, jsonHandlePretty).Encode(data)
if err != nil {
return "", err
}
@ -57,21 +55,21 @@ type TemplateFormat struct {
// TransformData returns template format string data.
func (p *TemplateFormat) TransformData(data interface{}) (string, error) {
var out io.Writer = new(bytes.Buffer)
var out bytes.Buffer
if len(p.tmpl) == 0 {
return "", fmt.Errorf("template needs to be specified the golang templates.")
return "", fmt.Errorf("template needs to be specified in golang's text/template format.")
}
t, err := template.New("format").Parse(p.tmpl)
t, err := template.New("").Funcs(makeFuncMap()).Parse(p.tmpl)
if err != nil {
return "", err
}
err = t.Execute(out, data)
err = t.Execute(&out, data)
if err != nil {
return "", err
}
return fmt.Sprint(out), nil
return out.String(), nil
}
func Format(json bool, template string, data interface{}) (string, error) {
@ -93,8 +91,21 @@ func Format(json bool, template string, data interface{}) (string, error) {
out, err := f.TransformData(data)
if err != nil {
return "", fmt.Errorf("Error formatting the data: %s", err)
return "", fmt.Errorf("Error formatting the data: %w", err)
}
return out, nil
}
func makeFuncMap() template.FuncMap {
fm := template.FuncMap{}
// Add the Sprig functions to the funcmap. These functions are decorated
// with `sprig_` to match how they are treated in consul-template
for k, v := range sprig.FuncMap() {
target := "sprig_" + k
fm[target] = v
}
return fm
}

View File

@ -1,68 +1,78 @@
package command
import (
"strings"
"testing"
"github.com/hashicorp/nomad/ci"
"github.com/shoenig/test/must"
)
type testData struct {
Region string
ID string
Name string
}
func TestDataFormat(t *testing.T) {
ci.Parallel(t)
type testData struct {
Region string
ID string
Name string
}
const expectJSON = `{
var tData = testData{"global", "1", "example"}
// Note: this variable is space indented (4) and requires the final brace to
// be at char 1
const expectJSON = `{
"ID": "1",
"Name": "example",
"Region": "global"
}`
var (
tData = testData{"global", "1", "example"}
testFormat = map[string]string{"json": "", "template": "{{.Region}}"}
expectOutput = map[string]string{"json": expectJSON, "template": "global"}
)
var tcs = map[string]struct {
format string
template string
expect string
isError bool
}{
"json_good": {
format: "json",
template: "",
expect: expectJSON,
},
"template_good": {
format: "template",
template: "{{.Region}}",
expect: "global",
},
"template_bad": {
format: "template",
template: "{{.foo}}",
isError: true,
expect: "can't evaluate field foo",
},
"template_empty": {
format: "template",
template: "",
isError: true,
expect: "template needs to be specified in golang's text/template format.",
},
"template_sprig": {
format: "template",
template: `{{$a := 1}}{{ $a | sprig_add 1 }}`,
expect: "2",
},
}
func TestDataFormat(t *testing.T) {
ci.Parallel(t)
for k, v := range testFormat {
fm, err := DataFormat(k, v)
if err != nil {
t.Fatalf("err: %v", err)
}
result, err := fm.TransformData(tData)
if err != nil {
t.Fatalf("err: %v", err)
}
if result != expectOutput[k] {
t.Fatalf("expected output:\n%s\nactual:\n%s", expectOutput[k], result)
}
}
}
func TestInvalidJSONTemplate(t *testing.T) {
ci.Parallel(t)
// Invalid template {{.foo}}
fm, err := DataFormat("template", "{{.foo}}")
if err != nil {
t.Fatalf("err: %v", err)
}
_, err = fm.TransformData(tData)
if !strings.Contains(err.Error(), "can't evaluate field foo") {
t.Fatalf("expected invalid template error, got: %s", err.Error())
}
// No template is specified
fm, err = DataFormat("template", "")
if err != nil {
t.Fatalf("err: %v", err)
}
_, err = fm.TransformData(tData)
if !strings.Contains(err.Error(), "template needs to be specified the golang templates.") {
t.Fatalf("expected not specified template error, got: %s", err.Error())
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
tc := tc
ci.Parallel(t)
fm, err := DataFormat(tc.format, tc.template)
must.NoError(t, err)
result, err := fm.TransformData(tData)
if tc.isError {
must.ErrorContains(t, err, tc.expect)
return
}
must.NoError(t, err)
must.Eq(t, tc.expect, result)
})
}
}

2
go.mod
View File

@ -155,7 +155,7 @@ require (
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
github.com/Masterminds/sprig/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.1
github.com/Microsoft/hcsshim v0.9.5 // indirect
github.com/VividCortex/ewma v1.1.1 // indirect
github.com/agext/levenshtein v1.2.1 // indirect