Skip to content

Commit

Permalink
Merge pull request #1503 from nak3/add-printer
Browse files Browse the repository at this point in the history
Support nomad CLI output with JSON and template format
  • Loading branch information
dadgar authored Aug 9, 2016
2 parents a35b748 + 5aceb8e commit 1ad8d72
Show file tree
Hide file tree
Showing 14 changed files with 488 additions and 22 deletions.
90 changes: 82 additions & 8 deletions command/alloc_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,17 @@ Alloc Status Options:
-short
Display short output. Shows only the most recent task event.
-stats
Display detailed resource usage statistics
-stats
Display detailed resource usage statistics.
-verbose
Show full information.
-json
Output the allocation in its JSON format.
-t
Format and display allocation using a Go template.
`

return strings.TrimSpace(helpText)
Expand All @@ -53,25 +59,23 @@ func (c *AllocStatusCommand) Synopsis() string {
}

func (c *AllocStatusCommand) Run(args []string) int {
var short, displayStats, verbose bool
var short, displayStats, verbose, json bool
var tmpl string

flags := c.Meta.FlagSet("alloc-status", FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.BoolVar(&short, "short", false, "")
flags.BoolVar(&verbose, "verbose", false, "")
flags.BoolVar(&displayStats, "stats", false, "")
flags.BoolVar(&json, "json", false, "")
flags.StringVar(&tmpl, "t", "", "")

if err := flags.Parse(args); err != nil {
return 1
}

// Check that we got exactly one allocation ID
args = flags.Args()
if len(args) != 1 {
c.Ui.Error(c.Help())
return 1
}
allocID := args[0]

// Get the HTTP client
client, err := c.Meta.Client()
Expand All @@ -80,6 +84,50 @@ func (c *AllocStatusCommand) Run(args []string) int {
return 1
}

// If args not specified but output format is specified, format and output the allocations data list
if len(args) == 0 {
var format string
if json && len(tmpl) > 0 {
c.Ui.Error("Both -json and -t are not allowed")
return 1
} else if json {
format = "json"
} else if len(tmpl) > 0 {
format = "template"
}
if len(format) > 0 {
allocs, _, err := client.Allocations().List(nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying allocations: %v", err))
return 1
}
// Return nothing if no allocations found
if len(allocs) == 0 {
return 0
}

f, err := DataFormat(format, tmpl)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting formatter: %s", err))
return 1
}

out, err := f.TransformData(allocs)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error formatting the data: %s", err))
return 1
}
c.Ui.Output(out)
return 0
}
}

if len(args) != 1 {
c.Ui.Error(c.Help())
return 1
}
allocID := args[0]

// Truncate the id unless full length is requested
length := shortId
if verbose {
Expand Down Expand Up @@ -130,6 +178,32 @@ func (c *AllocStatusCommand) Run(args []string) int {
return 1
}

// If output format is specified, format and output the data
var format string
if json && len(tmpl) > 0 {
c.Ui.Error("Both -json and -t are not allowed")
return 1
} else if json {
format = "json"
} else if len(tmpl) > 0 {
format = "template"
}
if len(format) > 0 {
f, err := DataFormat(format, tmpl)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting formatter: %s", err))
return 1
}

out, err := f.TransformData(alloc)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error formatting the data: %s", err))
return 1
}
c.Ui.Output(out)
return 0
}

var statsErr error
var stats *api.AllocResourceUsage
stats, statsErr = client.Allocations().Stats(alloc, nil)
Expand Down
8 changes: 8 additions & 0 deletions command/alloc_status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,13 @@ func TestAllocStatusCommand_Fails(t *testing.T) {
if out := ui.ErrorWriter.String(); !strings.Contains(out, "No allocation(s) with prefix or id") {
t.Fatalf("expected not found error, got: %s", out)
}
ui.ErrorWriter.Reset()

// Failed on both -json and -t options are specified
if code := cmd.Run([]string{"-address=" + url, "-json", "-t", "{{.ID}}"}); code != 1 {
t.Fatalf("expected exit 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Both -json and -t are not allowed") {
t.Fatalf("expected getting formatter error, got: %s", out)
}
}
65 changes: 65 additions & 0 deletions command/data_format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package command

import (
"bytes"
"encoding/json"
"fmt"
"io"
"text/template"
)

//DataFormatter is a transformer of the data.
type DataFormatter interface {
// TransformData should return transformed string data.
TransformData(interface{}) (string, error)
}

// DataFormat returns the data formatter specified format.
func DataFormat(format, tmpl string) (DataFormatter, error) {
switch format {
case "json":
if len(tmpl) > 0 {
return nil, fmt.Errorf("json format does not support template option.")
}
return &JSONFormat{}, nil
case "template":
return &TemplateFormat{tmpl}, nil
}
return nil, fmt.Errorf("Unsupported format is specified.")
}

type JSONFormat struct {
}

// TransformData returns JSON format string data.
func (p *JSONFormat) TransformData(data interface{}) (string, error) {
out, err := json.MarshalIndent(&data, "", " ")
if err != nil {
return "", err
}

return string(out), nil
}

type TemplateFormat struct {
tmpl string
}

// TransformData returns template format string data.
func (p *TemplateFormat) TransformData(data interface{}) (string, error) {
var out io.Writer = new(bytes.Buffer)
if len(p.tmpl) == 0 {
return "", fmt.Errorf("template needs to be specified the golang templates.")
}

t, err := template.New("format").Parse(p.tmpl)
if err != nil {
return "", err
}

err = t.Execute(out, data)
if err != nil {
return "", err
}
return fmt.Sprint(out), nil
}
64 changes: 64 additions & 0 deletions command/data_format_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package command

import (
"strings"
"testing"
)

type testData struct {
Region string
ID string
Name string
}

const expectJSON = `{
"Region": "global",
"ID": "1",
"Name": "example"
}`

var (
tData = testData{"global", "1", "example"}
testFormat = map[string]string{"json": "", "template": "{{.Region}}"}
expectOutput = map[string]string{"json": expectJSON, "template": "global"}
)

func TestDataFormat(t *testing.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: %s, actual: %s", expectOutput[k], result)
}
}
}

func TestInvalidJSONTemplate(t *testing.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(), "foo is not a field of struct type command.testData") {
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())
}
}
Loading

0 comments on commit 1ad8d72

Please sign in to comment.