From 536e3c52820fe5f4d153db0cc423127d99696ae6 Mon Sep 17 00:00:00 2001 From: Tim Gross Date: Wed, 15 Dec 2021 11:58:38 -0500 Subject: [PATCH] `nomad eval list` command (#11675) Use the new filtering and pagination capabilities of the `Eval.List` RPC to provide filtering and pagination at the command line. Also includes note that `nomad eval status -json` is deprecated and will be replaced with a single evaluation view in a future version of Nomad. --- .changelog/11675.txt | 3 + command/commands.go | 5 + command/eval_list.go | 213 ++++++++++++++++++ command/eval_list_test.go | 60 +++++ website/content/docs/commands/eval/index.mdx | 23 ++ website/content/docs/commands/eval/list.mdx | 51 +++++ .../{eval-status.mdx => eval/status.mdx} | 5 +- .../content/docs/upgrade/upgrade-specific.mdx | 10 + website/data/docs-nav-data.json | 17 +- website/redirects.js | 5 + 10 files changed, 389 insertions(+), 3 deletions(-) create mode 100644 .changelog/11675.txt create mode 100644 command/eval_list.go create mode 100644 command/eval_list_test.go create mode 100644 website/content/docs/commands/eval/index.mdx create mode 100644 website/content/docs/commands/eval/list.mdx rename website/content/docs/commands/{eval-status.mdx => eval/status.mdx} (91%) diff --git a/.changelog/11675.txt b/.changelog/11675.txt new file mode 100644 index 000000000..d5f12c500 --- /dev/null +++ b/.changelog/11675.txt @@ -0,0 +1,3 @@ +```release-note:improvement +cli: Added a `nomad eval list` command. +``` diff --git a/command/commands.go b/command/commands.go index 87f46b8f4..9b58cdd8f 100644 --- a/command/commands.go +++ b/command/commands.go @@ -255,6 +255,11 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory { Meta: meta, }, nil }, + "eval list": func() (cli.Command, error) { + return &EvalListCommand{ + Meta: meta, + }, nil + }, "eval status": func() (cli.Command, error) { return &EvalStatusCommand{ Meta: meta, diff --git a/command/eval_list.go b/command/eval_list.go new file mode 100644 index 000000000..20ea04477 --- /dev/null +++ b/command/eval_list.go @@ -0,0 +1,213 @@ +package command + +import ( + "fmt" + "os" + "strings" + + "github.com/hashicorp/nomad/api" + "github.com/hashicorp/nomad/api/contexts" + "github.com/posener/complete" +) + +type EvalListCommand struct { + Meta +} + +func (c *EvalListCommand) Help() string { + helpText := ` +Usage: nomad eval list [options] + + List is used to list the set of evaluations processed by Nomad. + +General Options: + + ` + generalOptionsUsage(usageOptsDefault) + ` + +Eval List Options: + + -verbose + Show full information. + + -per-page + How many results to show per page. + + -page-token + Where to start pagination. + + -job + Only show evaluations for this job ID. + + -status + Only show evaluations with this status. + + -json + Output the evaluation in its JSON format. + + -t + Format and display evaluation using a Go template. +` + + return strings.TrimSpace(helpText) +} + +func (c *EvalListCommand) Synopsis() string { + return "List the set of evaluations processed by Nomad" +} + +func (c *EvalListCommand) AutocompleteFlags() complete.Flags { + return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), + complete.Flags{ + "-json": complete.PredictNothing, + "-t": complete.PredictAnything, + "-verbose": complete.PredictNothing, + "-job": complete.PredictAnything, + "-status": complete.PredictAnything, + "-per-page": complete.PredictAnything, + "-page-token": complete.PredictAnything, + }) +} + +func (c *EvalListCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictFunc(func(a complete.Args) []string { + client, err := c.Meta.Client() + if err != nil { + return nil + } + + resp, _, err := client.Search().PrefixSearch(a.Last, contexts.Evals, nil) + if err != nil { + return []string{} + } + return resp.Matches[contexts.Evals] + }) +} + +func (c *EvalListCommand) Name() string { return "eval list" } + +func (c *EvalListCommand) Run(args []string) int { + var monitor, verbose, json bool + var perPage int + var tmpl, pageToken, filterJobID, filterStatus string + + flags := c.Meta.FlagSet(c.Name(), 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", "", "") + flags.IntVar(&perPage, "per-page", 0, "") + flags.StringVar(&pageToken, "page-token", "", "") + flags.StringVar(&filterJobID, "job", "", "") + flags.StringVar(&filterStatus, "status", "", "") + + if err := flags.Parse(args); err != nil { + return 1 + } + + // Check that we got no arguments + args = flags.Args() + if l := len(args); l != 0 { + c.Ui.Error("This command takes no arguments") + c.Ui.Error(commandErrorText(c)) + return 1 + } + + client, err := c.Meta.Client() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) + return 1 + } + + opts := &api.QueryOptions{ + PerPage: int32(perPage), + NextToken: pageToken, + Params: map[string]string{}, + } + if filterJobID != "" { + opts.Params["job"] = filterJobID + } + if filterStatus != "" { + opts.Params["status"] = filterStatus + } + + evals, qm, err := client.Evaluations().List(opts) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error querying evaluations: %v", err)) + return 1 + } + + // If args not specified but output format is specified, format + // and output the evaluations data list + if json || len(tmpl) > 0 { + out, err := Format(json, tmpl, evals) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + c.Ui.Output(out) + return 0 + } + + if len(evals) == 0 { + c.Ui.Output("No evals found") + return 0 + } + + // Truncate the id unless full length is requested + length := shortId + if verbose { + length = fullId + } + + out := make([]string, len(evals)+1) + out[0] = "ID|Priority|Triggered By|Job ID|Status|Placement Failures" + for i, eval := range evals { + failures, _ := evalFailureStatus(eval) + out[i+1] = fmt.Sprintf("%s|%d|%s|%s|%s|%s", + limit(eval.ID, length), + eval.Priority, + eval.TriggeredBy, + eval.JobID, + eval.Status, + failures, + ) + } + c.Ui.Output(formatList(out)) + + if qm.NextToken != "" { + c.Ui.Output(fmt.Sprintf(` +Results have been paginated. To get the next page run: + +%s -page-token %s`, argsWithoutPageToken(os.Args), qm.NextToken)) + } + + return 0 +} + +// argsWithoutPageToken strips out of the -page-token argument and +// returns the joined string +func argsWithoutPageToken(osArgs []string) string { + args := []string{} + i := 0 + for { + if i >= len(osArgs) { + break + } + arg := osArgs[i] + + if strings.HasPrefix(arg, "-page-token") { + if strings.Contains(arg, "=") { + i += 1 + } else { + i += 2 + } + continue + } + + args = append(args, arg) + i++ + } + return strings.Join(args, " ") +} diff --git a/command/eval_list_test.go b/command/eval_list_test.go new file mode 100644 index 000000000..0984b3ac8 --- /dev/null +++ b/command/eval_list_test.go @@ -0,0 +1,60 @@ +package command + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEvalList_ArgsWithoutPageToken(t *testing.T) { + + cases := []struct { + cli string + expected string + }{ + { + cli: "nomad eval list -page-token=abcdef", + expected: "nomad eval list", + }, + { + cli: "nomad eval list -page-token abcdef", + expected: "nomad eval list", + }, + { + cli: "nomad eval list -per-page 3 -page-token abcdef", + expected: "nomad eval list -per-page 3", + }, + { + cli: "nomad eval list -page-token abcdef -per-page 3", + expected: "nomad eval list -per-page 3", + }, + { + cli: "nomad eval list -per-page=3 -page-token abcdef", + expected: "nomad eval list -per-page=3", + }, + { + cli: "nomad eval list -verbose -page-token abcdef", + expected: "nomad eval list -verbose", + }, + { + cli: "nomad eval list -page-token abcdef -verbose", + expected: "nomad eval list -verbose", + }, + { + cli: "nomad eval list -verbose -page-token abcdef -per-page 3", + expected: "nomad eval list -verbose -per-page 3", + }, + { + cli: "nomad eval list -page-token abcdef -verbose -per-page 3", + expected: "nomad eval list -verbose -per-page 3", + }, + } + + for _, tc := range cases { + args := strings.Split(tc.cli, " ") + assert.Equal(t, tc.expected, argsWithoutPageToken(args), + "for input: %s", tc.cli) + } + +} diff --git a/website/content/docs/commands/eval/index.mdx b/website/content/docs/commands/eval/index.mdx new file mode 100644 index 000000000..504cd8a17 --- /dev/null +++ b/website/content/docs/commands/eval/index.mdx @@ -0,0 +1,23 @@ +--- +layout: docs +page_title: 'Commands: eval' +description: | + The eval command is used to interact with evals. +--- + +# Command: eval + +The `eval` command is used to interact with evals. + +## Usage + +Usage: `nomad eval [options]` + +Run `nomad eval -h` for help on that subcommand. The following +subcommands are available: + +- [`eval list`][list] - List all evals +- [`eval status`][status] - Display the status of a eval + +[list]: /docs/commands/eval/list 'List all evals' +[status]: /docs/commands/eval/status 'Display the status of a eval' diff --git a/website/content/docs/commands/eval/list.mdx b/website/content/docs/commands/eval/list.mdx new file mode 100644 index 000000000..467fbb90b --- /dev/null +++ b/website/content/docs/commands/eval/list.mdx @@ -0,0 +1,51 @@ +--- +layout: docs +page_title: 'Commands: eval list' +description: | + The eval list command is used to list evaluations. +--- + +# Command: eval list + +The `eval list` command is used list all evaluations. + +## Usage + +```plaintext +nomad eval list [options] +``` + +The `eval list` command requires no arguments. + +When ACLs are enabled, this command requires a token with the `read-job` +capability for the requested namespace. + +## General Options + +@include 'general_options.mdx' + +## List Options + +- `-verbose`: Show full information. +- `-per-page`: How many results to show per page. +- `-page-token`: Where to start pagination. +- `-job`: Only show evaluations for this job ID. +- `-status`: Only show evaluations with this status. +- `-json`: Output the evaluation in its JSON format. +- `-t`: Format and display evaluation using a Go template. + +## Examples + +List all tracked evaluations: + +```shell-session +$ nomad eval list -per-page 3 -status complete +ID Priority Triggered By Job ID Status Placement Failures +456e37aa 50 deployment-watcher example complete false +1a1eafe6 50 alloc-stop example complete false +3411e37b 50 job-register example complete false + +Results have been paginated. To get the next page run: + +nomad eval list -page-token 9ecffbba-73be-d909-5d7e-ac2694c10e0c +``` diff --git a/website/content/docs/commands/eval-status.mdx b/website/content/docs/commands/eval/status.mdx similarity index 91% rename from website/content/docs/commands/eval-status.mdx rename to website/content/docs/commands/eval/status.mdx index 6d93efc46..f89bb603b 100644 --- a/website/content/docs/commands/eval-status.mdx +++ b/website/content/docs/commands/eval/status.mdx @@ -45,7 +45,10 @@ indicated by exit code 1. - `-monitor`: Monitor an outstanding evaluation - `-verbose`: Show full information. -- `-json` : Output the evaluation in its JSON format. +- `-json` : Output a list of all evaluations in JSON format. This + behavior is deprecated and has been replaced by `nomad eval list + -json`. In Nomad 1.4.0 the behavior of this option will change to + output only the selected evaluation in JSON. - `-t` : Format and display evaluation using a Go template. ## Examples diff --git a/website/content/docs/upgrade/upgrade-specific.mdx b/website/content/docs/upgrade/upgrade-specific.mdx index 1035bd56d..90b5a95a2 100644 --- a/website/content/docs/upgrade/upgrade-specific.mdx +++ b/website/content/docs/upgrade/upgrade-specific.mdx @@ -13,6 +13,16 @@ upgrade. However, specific versions of Nomad may have more details provided for their upgrades as a result of new features or changed behavior. This page is used to document those details separately from the standard upgrade flow. +## Nomad 1.2.4 + +#### `nomad eval status -json` deprecated + +Nomad 1.2.4 includes a new `nomad eval list` command that has the +option to display the results in JSON format with the `-json` +flag. This replaces the existing `nomad eval status -json` option. In +Nomad 1.4.0, `nomad eval status -json` will be changed to display only +the selected evaluation in JSON format. + ## Nomad 1.2.2 ### Panic on node class filtering for system and sysbatch jobs fixed diff --git a/website/data/docs-nav-data.json b/website/data/docs-nav-data.json index 51ce0a2af..28f67624c 100644 --- a/website/data/docs-nav-data.json +++ b/website/data/docs-nav-data.json @@ -353,8 +353,21 @@ ] }, { - "title": "eval status", - "path": "commands/eval-status" + "title": "eval", + "routes": [ + { + "title": "Overview", + "path": "commands/eval" + }, + { + "title": "list", + "path": "commands/eval/list" + }, + { + "title": "status", + "path": "commands/eval/status" + } + ] }, { "title": "job", diff --git a/website/redirects.js b/website/redirects.js index 48a206b7f..0ed8488b8 100644 --- a/website/redirects.js +++ b/website/redirects.js @@ -589,6 +589,11 @@ module.exports = [ destination: '/docs/commands/alloc/status', permanent: true, }, + { + source: '/docs/commands/eval-status', + destination: '/docs/commands/eval/status', + permanent: true, + }, { source: '/docs/commands/fs', destination: '/docs/commands/alloc/fs',