diff --git a/.changelog/9053.txt b/.changelog/9053.txt new file mode 100644 index 000000000..588538e75 --- /dev/null +++ b/.changelog/9053.txt @@ -0,0 +1,3 @@ +```release-note:improvement +cli: Added sprig function support for `-t` templates +``` diff --git a/command/data_format.go b/command/data_format.go index cb9a0761a..9a6141b81 100644 --- a/command/data_format.go +++ b/command/data_format.go @@ -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 +} diff --git a/command/data_format_test.go b/command/data_format_test.go index 103f8fb7c..c17e06742 100644 --- a/command/data_format_test.go +++ b/command/data_format_test.go @@ -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) + }) } } diff --git a/go.mod b/go.mod index 16dbaf6b7..a91301de1 100644 --- a/go.mod +++ b/go.mod @@ -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