diff --git a/command/alloc_status.go b/command/alloc_status.go index ebf83163e..46f1f18db 100644 --- a/command/alloc_status.go +++ b/command/alloc_status.go @@ -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) diff --git a/command/alloc_status_test.go b/command/alloc_status_test.go index e8405ae46..b284ee15a 100644 --- a/command/alloc_status_test.go +++ b/command/alloc_status_test.go @@ -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) + } } diff --git a/command/data_format.go b/command/data_format.go new file mode 100644 index 000000000..c11887b9b --- /dev/null +++ b/command/data_format.go @@ -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 +} diff --git a/command/data_format_test.go b/command/data_format_test.go new file mode 100644 index 000000000..5f4ba50ee --- /dev/null +++ b/command/data_format_test.go @@ -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()) + } +} diff --git a/command/eval_status.go b/command/eval_status.go index 8a87e96bd..0fcde9aa1 100644 --- a/command/eval_status.go +++ b/command/eval_status.go @@ -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 diff --git a/command/eval_status_test.go b/command/eval_status_test.go index 29ea4634f..b945cf770 100644 --- a/command/eval_status_test.go +++ b/command/eval_status_test.go @@ -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) + } + } diff --git a/command/inspect.go b/command/inspect.go index 12c596400..066f79bbc 100644 --- a/command/inspect.go +++ b/command/inspect.go @@ -20,8 +20,16 @@ Usage: nomad inspect [options] 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, "", " ") diff --git a/command/inspect_test.go b/command/inspect_test.go index ba7539abe..8d16106a3 100644 --- a/command/inspect_test.go +++ b/command/inspect_test.go @@ -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) + } } diff --git a/command/node_status.go b/command/node_status.go index 36e1ad4df..1c33873a6 100644 --- a/command/node_status.go +++ b/command/node_status.go @@ -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) } diff --git a/command/node_status_test.go b/command/node_status_test.go index 604b3625c..8becbf012 100644 --- a/command/node_status_test.go +++ b/command/node_status_test.go @@ -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) + } } diff --git a/website/source/docs/commands/alloc-status.html.md.erb b/website/source/docs/commands/alloc-status.html.md.erb index 131d6ec11..11a592016 100644 --- a/website/source/docs/commands/alloc-status.html.md.erb +++ b/website/source/docs/commands/alloc-status.html.md.erb @@ -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 diff --git a/website/source/docs/commands/eval-status.html.md.erb b/website/source/docs/commands/eval-status.html.md.erb index 90d705357..792717175 100644 --- a/website/source/docs/commands/eval-status.html.md.erb +++ b/website/source/docs/commands/eval-status.html.md.erb @@ -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 diff --git a/website/source/docs/commands/inspect.html.md.erb b/website/source/docs/commands/inspect.html.md.erb index 8fc5db6b1..e29974001 100644 --- a/website/source/docs/commands/inspect.html.md.erb +++ b/website/source/docs/commands/inspect.html.md.erb @@ -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: diff --git a/website/source/docs/commands/node-status.html.md.erb b/website/source/docs/commands/node-status.html.md.erb index 8682a97a9..d55628359 100644 --- a/website/source/docs/commands/node-status.html.md.erb +++ b/website/source/docs/commands/node-status.html.md.erb @@ -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