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 ( import (
"bytes" "bytes"
"fmt" "fmt"
"io"
"text/template" "text/template"
"github.com/Masterminds/sprig/v3"
"github.com/hashicorp/go-msgpack/codec" "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.") return nil, fmt.Errorf("Unsupported format is specified.")
} }
type JSONFormat struct { type JSONFormat struct{}
}
// TransformData returns JSON format string data. // TransformData returns JSON format string data.
func (p *JSONFormat) TransformData(data interface{}) (string, error) { func (p *JSONFormat) TransformData(data interface{}) (string, error) {
var buf bytes.Buffer var buf bytes.Buffer
enc := codec.NewEncoder(&buf, jsonHandlePretty) err := codec.NewEncoder(&buf, jsonHandlePretty).Encode(data)
err := enc.Encode(data)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -57,21 +55,21 @@ type TemplateFormat struct {
// TransformData returns template format string data. // TransformData returns template format string data.
func (p *TemplateFormat) TransformData(data interface{}) (string, error) { func (p *TemplateFormat) TransformData(data interface{}) (string, error) {
var out io.Writer = new(bytes.Buffer) var out bytes.Buffer
if len(p.tmpl) == 0 { 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 { if err != nil {
return "", err return "", err
} }
err = t.Execute(out, data) err = t.Execute(&out, data)
if err != nil { if err != nil {
return "", err return "", err
} }
return fmt.Sprint(out), nil return out.String(), nil
} }
func Format(json bool, template string, data interface{}) (string, error) { 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) out, err := f.TransformData(data)
if err != nil { if err != nil {
return "", fmt.Errorf("Error formatting the data: %s", err) return "", fmt.Errorf("Error formatting the data: %w", err)
} }
return out, nil 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 package command
import ( import (
"strings"
"testing" "testing"
"github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/ci"
"github.com/shoenig/test/must"
) )
type testData struct { func TestDataFormat(t *testing.T) {
ci.Parallel(t)
type testData struct {
Region string Region string
ID string ID string
Name 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", "ID": "1",
"Name": "example", "Name": "example",
"Region": "global" "Region": "global"
}` }`
var ( var tcs = map[string]struct {
tData = testData{"global", "1", "example"} format string
testFormat = map[string]string{"json": "", "template": "{{.Region}}"} template string
expectOutput = map[string]string{"json": expectJSON, "template": "global"} expect string
) isError bool
}{
func TestDataFormat(t *testing.T) { "json_good": {
ci.Parallel(t) format: "json",
for k, v := range testFormat { template: "",
fm, err := DataFormat(k, v) expect: expectJSON,
if err != nil { },
t.Fatalf("err: %v", err) "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",
},
} }
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) result, err := fm.TransformData(tData)
if err != nil { if tc.isError {
t.Fatalf("err: %v", err) must.ErrorContains(t, err, tc.expect)
} return
if result != expectOutput[k] {
t.Fatalf("expected output:\n%s\nactual:\n%s", expectOutput[k], result)
} }
} must.NoError(t, err)
} must.Eq(t, tc.expect, 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())
} }
} }

2
go.mod
View File

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