Merge pull request #1503 from nak3/add-printer

Support nomad CLI output with JSON and template format
This commit is contained in:
Alex Dadgar
2016-08-09 09:54:25 -07:00
committed by GitHub
14 changed files with 488 additions and 22 deletions

View File

@@ -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)
@@ -53,13 +59,16 @@ 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
@@ -67,11 +76,6 @@ func (c *AllocStatusCommand) Run(args []string) int {
// 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()
@@ -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 {
@@ -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)

View File

@@ -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
command/data_format.go Normal file
View File

@@ -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
}

View File

@@ -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())
}
}

View File

@@ -31,6 +31,12 @@ Eval Status Options:
-verbose
Show full information.
-json
Output the evaluation in its JSON format.
-t
Format and display evaluation using a Go template.
`
return strings.TrimSpace(helpText)
@@ -41,12 +47,15 @@ func (c *EvalStatusCommand) Synopsis() string {
}
func (c *EvalStatusCommand) Run(args []string) int {
var monitor, verbose bool
var monitor, verbose, json bool
var tmpl string
flags := c.Meta.FlagSet("eval-status", FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.BoolVar(&monitor, "monitor", false, "")
flags.BoolVar(&verbose, "verbose", false, "")
flags.BoolVar(&json, "json", false, "")
flags.StringVar(&tmpl, "t", "", "")
if err := flags.Parse(args); err != nil {
return 1
@@ -54,11 +63,6 @@ func (c *EvalStatusCommand) Run(args []string) int {
// Check that we got exactly one evaluation ID
args = flags.Args()
if len(args) != 1 {
c.Ui.Error(c.Help())
return 1
}
evalID := args[0]
// Get the HTTP client
client, err := c.Meta.Client()
@@ -67,6 +71,51 @@ func (c *EvalStatusCommand) Run(args []string) int {
return 1
}
// If args not specified but output format is specified, format and output the evaluations 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 {
evals, _, err := client.Evaluations().List(nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying evaluations: %v", err))
return 1
}
// Return nothing if no evaluations found
if len(evals) == 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(evals)
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
}
evalID := args[0]
// Truncate the id unless full length is requested
length := shortId
if verbose {
@@ -93,6 +142,7 @@ func (c *EvalStatusCommand) Run(args []string) int {
c.Ui.Error(fmt.Sprintf("No evaluation(s) with prefix or id %q found", evalID))
return 1
}
if len(evals) > 1 {
// Format the evals
out := make([]string, len(evals)+1)
@@ -124,6 +174,29 @@ func (c *EvalStatusCommand) Run(args []string) int {
return 1
}
// If output format is specified, format and output the data
var format string
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(eval)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error formatting the data: %s", err))
return 1
}
c.Ui.Output(out)
return 0
}
failureString, failures := evalFailureStatus(eval)
triggerNoun, triggerSubj := getTriggerDetails(eval)
statusDesc := eval.StatusDescription

View File

@@ -43,4 +43,14 @@ func TestEvalStatusCommand_Fails(t *testing.T) {
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error querying evaluation") {
t.Fatalf("expected failed query 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)
}
}

View File

@@ -20,8 +20,16 @@ Usage: nomad inspect [options] <job>
General Options:
` + generalOptionsUsage()
` + generalOptionsUsage() + `
Inspect Options:
-json
Output the evaluation in its JSON format.
-t
Format and display evaluation using a Go template.
`
return strings.TrimSpace(helpText)
}
@@ -30,20 +38,18 @@ func (c *InspectCommand) Synopsis() string {
}
func (c *InspectCommand) Run(args []string) int {
var ojson bool
var tmpl string
flags := c.Meta.FlagSet("inspect", FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.BoolVar(&ojson, "json", false, "")
flags.StringVar(&tmpl, "t", "", "")
if err := flags.Parse(args); err != nil {
return 1
}
// Check that we got exactly one job
args = flags.Args()
if len(args) != 1 {
c.Ui.Error(c.Help())
return 1
}
jobID := args[0]
// Get the HTTP client
client, err := c.Meta.Client()
@@ -52,6 +58,50 @@ func (c *InspectCommand) Run(args []string) int {
return 1
}
// If args not specified but output format is specified, format and output the jobs data list
if len(args) == 0 {
var format string
if ojson && len(tmpl) > 0 {
c.Ui.Error("Both -json and -t are not allowed")
return 1
} else if ojson {
format = "json"
} else if len(tmpl) > 0 {
format = "template"
}
if len(format) > 0 {
jobs, _, err := client.Jobs().List(nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying jobs: %v", err))
return 1
}
f, err := DataFormat(format, tmpl)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting formatter: %s", err))
return 1
}
// Return nothing if no jobs found
if len(jobs) == 0 {
return 0
}
out, err := f.TransformData(jobs)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error formatting the data: %s", err))
return 1
}
c.Ui.Output(out)
return 0
}
}
// Check that we got exactly one job
if len(args) != 1 {
c.Ui.Error(c.Help())
return 1
}
jobID := args[0]
// Check if the job exists
jobs, _, err := client.Jobs().PrefixList(jobID)
if err != nil {
@@ -83,6 +133,29 @@ func (c *InspectCommand) Run(args []string) int {
return 1
}
// If output format is specified, format and output the data
var format string
if ojson {
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(job)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error formatting the data: %s", err))
return 1
}
c.Ui.Output(out)
return 0
}
// Print the contents of the job
req := api.RegisterJobRequest{Job: job}
buf, err := json.MarshalIndent(req, "", " ")

View File

@@ -43,4 +43,13 @@ func TestInspectCommand_Fails(t *testing.T) {
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error inspecting job") {
t.Fatalf("expected failed query 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)
}
}

View File

@@ -30,6 +30,8 @@ type NodeStatusCommand struct {
list_allocs bool
self bool
stats bool
json bool
tmpl string
}
func (c *NodeStatusCommand) Help() string {
@@ -66,6 +68,12 @@ Node Status Options:
-verbose
Display full information.
-json
Output the node in its JSON format.
-t
Format and display node using a Go template.
`
return strings.TrimSpace(helpText)
}
@@ -83,6 +91,8 @@ func (c *NodeStatusCommand) Run(args []string) int {
flags.BoolVar(&c.list_allocs, "allocs", false, "")
flags.BoolVar(&c.self, "self", false, "")
flags.BoolVar(&c.stats, "stats", false, "")
flags.BoolVar(&c.json, "json", false, "")
flags.StringVar(&c.tmpl, "t", "", "")
if err := flags.Parse(args); err != nil {
return 1
@@ -110,6 +120,17 @@ func (c *NodeStatusCommand) Run(args []string) int {
// Use list mode if no node name was provided
if len(args) == 0 && !c.self {
// If output format is specified, format and output the node data list
var format string
if c.json && len(c.tmpl) > 0 {
c.Ui.Error("Both -json and -t are not allowed")
return 1
} else if c.json {
format = "json"
} else if len(c.tmpl) > 0 {
format = "template"
}
// Query the node info
nodes, _, err := client.Nodes().List(nil)
if err != nil {
@@ -122,6 +143,22 @@ func (c *NodeStatusCommand) Run(args []string) int {
return 0
}
if len(format) > 0 {
f, err := DataFormat(format, c.tmpl)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting formatter: %s", err))
return 1
}
out, err := f.TransformData(nodes)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error formatting the data: %s", err))
return 1
}
c.Ui.Output(out)
return 0
}
// Format the nodes list
out := make([]string, len(nodes)+1)
if c.list_allocs {
@@ -129,6 +166,7 @@ func (c *NodeStatusCommand) Run(args []string) int {
} else {
out[0] = "ID|DC|Name|Class|Drain|Status"
}
for i, node := range nodes {
if c.list_allocs {
numAllocs, err := getRunningAllocs(client, node.ID)
@@ -216,6 +254,32 @@ func (c *NodeStatusCommand) Run(args []string) int {
return 1
}
// If output format is specified, format and output the data
var format string
if c.json && len(c.tmpl) > 0 {
c.Ui.Error("Both -json and -t are not allowed")
return 1
} else if c.json {
format = "json"
} else if len(c.tmpl) > 0 {
format = "template"
}
if len(format) > 0 {
f, err := DataFormat(format, c.tmpl)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting formatter: %s", err))
return 1
}
out, err := f.TransformData(node)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error formatting the data: %s", err))
return 1
}
c.Ui.Output(out)
return 0
}
return c.formatNode(client, node)
}

View File

@@ -200,4 +200,13 @@ func TestNodeStatusCommand_Fails(t *testing.T) {
if out := ui.ErrorWriter.String(); !strings.Contains(out, "must contain at least two characters.") {
t.Fatalf("expected too few characters 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)
}
}

View File

@@ -31,6 +31,8 @@ allocations and information will be displayed.
* `-short`: Display short output. Shows only the most recent task event.
* `-verbose`: Show full information.
* `-json` : Output the allocation in its JSON format.
* `-t` : Format and display allocation using a Go template.
## Examples

View File

@@ -48,6 +48,10 @@ indicated by exit code 1.
* `-verbose`: Show full information.
* `-json` : Output the evaluation in its JSON format.
* `-t` : Format and display evaluation using a Go template.
## Examples
Show the status of an evaluation that has placement failures

View File

@@ -25,6 +25,13 @@ version of a job Nomad is running.
<%= general_options_usage %>
## Inspect Options
* `-short`: Display short output. Used only when a single node is being queried.
Drops verbose node allocation data from the output.
* `-verbose`: Show full information.
## Examples
Inspect a submitted job:

View File

@@ -45,6 +45,10 @@ information will be displayed. If running the command on a Nomad Client, the
* `-verbose`: Show full information.
* `-json` : Output the node in its JSON format.
* `-t` : Format and display node using a Go template.
## Examples