From e07c9a9448282c2b9007fb1f8bfbccb11c51075f Mon Sep 17 00:00:00 2001 From: Michael Smithhisler Date: Wed, 6 May 2026 15:09:52 -0400 Subject: [PATCH 1/5] command: initial batch queue cli implementation --- api/jobs_batch_queue.go | 21 ++++++ command/commands.go | 5 ++ command/job_queue.go | 140 ++++++++++++++++++++++++++++++++++++++ command/job_queue_test.go | 38 +++++++++++ 4 files changed, 204 insertions(+) create mode 100644 api/jobs_batch_queue.go create mode 100644 command/job_queue.go create mode 100644 command/job_queue_test.go diff --git a/api/jobs_batch_queue.go b/api/jobs_batch_queue.go new file mode 100644 index 00000000000..c84b0816e3d --- /dev/null +++ b/api/jobs_batch_queue.go @@ -0,0 +1,21 @@ +package api + +type Workload struct { + JobID string + Tenant string + Priority int +} + +type BatchQueueStatusResponse struct { + Workloads []Workload +} + +// BatchQueueStatus is used to query the current batch job queue. +func (j *Jobs) BatchQueueStatus(q *QueryOptions) (*BatchQueueStatusResponse, *QueryMeta, error) { + var resp BatchQueueStatusResponse + qm, err := j.client.query("/v1/jobs/queue/status", &resp, q) + if err != nil { + return nil, nil, err + } + return &resp, qm, nil +} diff --git a/command/commands.go b/command/commands.go index 344d898e86b..de786f55bb5 100644 --- a/command/commands.go +++ b/command/commands.go @@ -501,6 +501,11 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory { Meta: meta, }, nil }, + "job queue": func() (cli.Command, error) { + return &JobQueueCommand{ + Meta: meta, + }, nil + }, "job revert": func() (cli.Command, error) { return &JobRevertCommand{ Meta: meta, diff --git a/command/job_queue.go b/command/job_queue.go new file mode 100644 index 00000000000..2b9a9a21174 --- /dev/null +++ b/command/job_queue.go @@ -0,0 +1,140 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/hashicorp/nomad/api" + "github.com/posener/complete" +) + +type JobQueueCommand struct { + Meta + forceRescheduling bool +} + +func (c *JobQueueCommand) Help() string { + helpText := ` +Usage: nomad job queue [options] + + View the current status of workloads queued in a batch job queue. + + When ACLs are enabled, this command requires a token with either TBD + capabilities. Probably at least 'list-jobs'. + +General Options: + + ` + generalOptionsUsage(usageOptsDefault) + ` + +Eval Options: + + -limit + The maximum number of workloads to return + + -verbose + Display full output + +` + return strings.TrimSpace(helpText) +} + +func (c *JobQueueCommand) Synopsis() string { + return "View the status of a batch job queue" +} + +func (c *JobQueueCommand) AutocompleteFlags() complete.Flags { + return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), + complete.Flags{ + "-verbose": complete.PredictNothing, + "-limit": complete.PredictNothing, + }) +} + +func (c *JobQueueCommand) AutocompleteArgs() complete.Predictor { + return JobPredictor(c.Meta.Client) +} + +func (c *JobQueueCommand) Name() string { return "job queue" } + +func (c *JobQueueCommand) Run(args []string) int { + var verbose bool + var limit int + flags := c.Meta.FlagSet(c.Name(), FlagSetClient) + flags.Usage = func() { c.Ui.Output(c.Help()) } + flags.BoolVar(&verbose, "verbose", false, "") + flags.IntVar(&limit, "limit", 0, "") + + if err := flags.Parse(args); err != nil { + return 1 + } + + // Get the HTTP client + client, err := c.Meta.Client() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) + return 255 + } + + // Setup the options + opts := &api.QueryOptions{} + + if limit > 0 { + opts.Params["limit"] = fmt.Sprintf("%d", limit) + } + + // Submit the request + resp, _, err := client.Jobs().BatchQueueStatus(opts) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error during batch queue request: %s", err)) + return 255 + } + + c.printOutput(resp) + return 0 +} + +func (c *JobQueueCommand) printOutput(resp *api.BatchQueueStatusResponse) { + if resp == nil { + c.Ui.Error("Empty batch queue response") + } + + headers := []string{"JobID", "Tenant", "Priority"} + + // compute column widths + col0, col1, col2 := len(headers[0]), len(headers[1]), len(headers[2]) + for _, r := range resp.Workloads { + if len(r.JobID) > col0 { + col0 = len(r.JobID) + } + if len(r.Tenant) > col1 { + col1 = len(r.Tenant) + } + // convert int to string for width calculation + l := len(fmt.Sprintf("%d", r.Priority)) + if l > col2 { + col2 = l + } + } + + headerFmt := fmt.Sprintf("%%-%ds | %%-%ds | %%-%ds\n", col0, col1, col2) + rowFmt := fmt.Sprintf("%%-%ds | %%-%ds | %%%dd\n", col0, col1, col2) + + var output strings.Builder + + // print header + fmt.Fprintf(&output, headerFmt, headers[0], headers[1], headers[2]) + + // print separator + fmt.Fprintf(&output, "%s-+-%s-+-%s\n", + strings.Repeat("-", col0), + strings.Repeat("-", col1), + strings.Repeat("-", col2), + ) + + // print rows + for _, w := range resp.Workloads { + fmt.Fprintf(&output, rowFmt, w.JobID, w.Tenant, w.Priority) + } + + c.Ui.Output(output.String()) +} diff --git a/command/job_queue_test.go b/command/job_queue_test.go new file mode 100644 index 00000000000..25325c9d9ac --- /dev/null +++ b/command/job_queue_test.go @@ -0,0 +1,38 @@ +package command + +import ( + "testing" + + "github.com/hashicorp/cli" + "github.com/hashicorp/nomad/api" + "github.com/hashicorp/nomad/ci" + "github.com/shoenig/test/must" +) + +func TestJobQueue_Implements(t *testing.T) { + ci.Parallel(t) + var _ cli.Command = &JobQueueCommand{} +} + +func TestJobQueue_printOutput(t *testing.T) { + ci.Parallel(t) + ui := cli.NewMockUi() + cmd := &JobQueueCommand{Meta: Meta{Ui: ui}} + + testResp := &api.BatchQueueStatusResponse{ + Workloads: []api.Workload{ + { + JobID: "123", + Tenant: "testTenant1", + Priority: 5, + }, + }, + } + cmd.printOutput(testResp) + + expect := "JobID | Tenant | Priority\n" + + "------+-------------+---------\n" + + "123 | testTenant1 | 5\n\n" + + must.Eq(t, expect, ui.OutputWriter.String()) +} From 1a8fad4426d56b98541fe0ee6d87b7fe77b68ac7 Mon Sep 17 00:00:00 2001 From: Michael Smithhisler Date: Wed, 6 May 2026 15:32:24 -0400 Subject: [PATCH 2/5] add copy header --- api/jobs_batch_queue.go | 3 +++ command/job_queue.go | 3 +++ command/job_queue_test.go | 3 +++ 3 files changed, 9 insertions(+) diff --git a/api/jobs_batch_queue.go b/api/jobs_batch_queue.go index c84b0816e3d..234399a3d0b 100644 --- a/api/jobs_batch_queue.go +++ b/api/jobs_batch_queue.go @@ -1,3 +1,6 @@ +// Copyright IBM Corp. 2015, 2026 +// SPDX-License-Identifier: BUSL-1.1 + package api type Workload struct { diff --git a/command/job_queue.go b/command/job_queue.go index 2b9a9a21174..44fd20c31d3 100644 --- a/command/job_queue.go +++ b/command/job_queue.go @@ -1,3 +1,6 @@ +// Copyright IBM Corp. 2015, 2026 +// SPDX-License-Identifier: BUSL-1.1 + package command import ( diff --git a/command/job_queue_test.go b/command/job_queue_test.go index 25325c9d9ac..566d5ade1ff 100644 --- a/command/job_queue_test.go +++ b/command/job_queue_test.go @@ -1,3 +1,6 @@ +// Copyright IBM Corp. 2015, 2026 +// SPDX-License-Identifier: BUSL-1.1 + package command import ( From 5165612f67b36d744e4af0e28fabf32d33e20f1c Mon Sep 17 00:00:00 2001 From: Michael Smithhisler Date: Wed, 6 May 2026 16:42:41 -0400 Subject: [PATCH 3/5] modern max calculation --- command/job_queue.go | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/command/job_queue.go b/command/job_queue.go index 44fd20c31d3..32eff3cdc54 100644 --- a/command/job_queue.go +++ b/command/job_queue.go @@ -106,17 +106,9 @@ func (c *JobQueueCommand) printOutput(resp *api.BatchQueueStatusResponse) { // compute column widths col0, col1, col2 := len(headers[0]), len(headers[1]), len(headers[2]) for _, r := range resp.Workloads { - if len(r.JobID) > col0 { - col0 = len(r.JobID) - } - if len(r.Tenant) > col1 { - col1 = len(r.Tenant) - } - // convert int to string for width calculation - l := len(fmt.Sprintf("%d", r.Priority)) - if l > col2 { - col2 = l - } + col0 = max(col0, len(r.JobID)) + col1 = max(col1, len(r.Tenant)) + col2 = max(col2, len(fmt.Sprintf("%d", r.Priority))) } headerFmt := fmt.Sprintf("%%-%ds | %%-%ds | %%-%ds\n", col0, col1, col2) From 27287e9182f247b75e5e071b9c2c5d89931979f9 Mon Sep 17 00:00:00 2001 From: Michael Smithhisler Date: Wed, 6 May 2026 16:48:27 -0400 Subject: [PATCH 4/5] remove leftover copy paste --- command/job_queue.go | 1 - 1 file changed, 1 deletion(-) diff --git a/command/job_queue.go b/command/job_queue.go index 32eff3cdc54..ea12720dd0d 100644 --- a/command/job_queue.go +++ b/command/job_queue.go @@ -13,7 +13,6 @@ import ( type JobQueueCommand struct { Meta - forceRescheduling bool } func (c *JobQueueCommand) Help() string { From 1f22de1be6240f5233a4b2c33740bc6c7d798172 Mon Sep 17 00:00:00 2001 From: Michael Smithhisler Date: Wed, 6 May 2026 17:33:39 -0400 Subject: [PATCH 5/5] switch to using columnize and add status options --- api/jobs_batch_queue.go | 4 +++- command/job_queue.go | 41 ++++++++++----------------------------- command/job_queue_test.go | 6 +++--- 3 files changed, 16 insertions(+), 35 deletions(-) diff --git a/api/jobs_batch_queue.go b/api/jobs_batch_queue.go index 234399a3d0b..bd205ff427e 100644 --- a/api/jobs_batch_queue.go +++ b/api/jobs_batch_queue.go @@ -13,8 +13,10 @@ type BatchQueueStatusResponse struct { Workloads []Workload } +type BatchQueueStatusOptions struct{} + // BatchQueueStatus is used to query the current batch job queue. -func (j *Jobs) BatchQueueStatus(q *QueryOptions) (*BatchQueueStatusResponse, *QueryMeta, error) { +func (j *Jobs) BatchQueueStatus(opts *BatchQueueStatusOptions, q *QueryOptions) (*BatchQueueStatusResponse, *QueryMeta, error) { var resp BatchQueueStatusResponse qm, err := j.client.query("/v1/jobs/queue/status", &resp, q) if err != nil { diff --git a/command/job_queue.go b/command/job_queue.go index ea12720dd0d..aba2042aa9b 100644 --- a/command/job_queue.go +++ b/command/job_queue.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/nomad/api" "github.com/posener/complete" + "github.com/ryanuber/columnize" ) type JobQueueCommand struct { @@ -78,14 +79,14 @@ func (c *JobQueueCommand) Run(args []string) int { } // Setup the options - opts := &api.QueryOptions{} + qo := &api.QueryOptions{} if limit > 0 { - opts.Params["limit"] = fmt.Sprintf("%d", limit) + qo.Params["limit"] = fmt.Sprintf("%d", limit) } // Submit the request - resp, _, err := client.Jobs().BatchQueueStatus(opts) + resp, _, err := client.Jobs().BatchQueueStatus(nil, qo) if err != nil { c.Ui.Error(fmt.Sprintf("Error during batch queue request: %s", err)) return 255 @@ -100,35 +101,13 @@ func (c *JobQueueCommand) printOutput(resp *api.BatchQueueStatusResponse) { c.Ui.Error("Empty batch queue response") } - headers := []string{"JobID", "Tenant", "Priority"} + out := make([]string, len(resp.Workloads)+2) + out[0] = "JobID|Tenant|Priority" + out[1] = "-----|------|--------" - // compute column widths - col0, col1, col2 := len(headers[0]), len(headers[1]), len(headers[2]) - for _, r := range resp.Workloads { - col0 = max(col0, len(r.JobID)) - col1 = max(col1, len(r.Tenant)) - col2 = max(col2, len(fmt.Sprintf("%d", r.Priority))) + for i, v := range resp.Workloads { + out[i+2] = fmt.Sprintf("%s|%s|%d", v.JobID, v.Tenant, v.Priority) } - headerFmt := fmt.Sprintf("%%-%ds | %%-%ds | %%-%ds\n", col0, col1, col2) - rowFmt := fmt.Sprintf("%%-%ds | %%-%ds | %%%dd\n", col0, col1, col2) - - var output strings.Builder - - // print header - fmt.Fprintf(&output, headerFmt, headers[0], headers[1], headers[2]) - - // print separator - fmt.Fprintf(&output, "%s-+-%s-+-%s\n", - strings.Repeat("-", col0), - strings.Repeat("-", col1), - strings.Repeat("-", col2), - ) - - // print rows - for _, w := range resp.Workloads { - fmt.Fprintf(&output, rowFmt, w.JobID, w.Tenant, w.Priority) - } - - c.Ui.Output(output.String()) + c.Ui.Output(columnize.Format(out, &columnize.Config{Glue: " | "})) } diff --git a/command/job_queue_test.go b/command/job_queue_test.go index 566d5ade1ff..936f6095a2a 100644 --- a/command/job_queue_test.go +++ b/command/job_queue_test.go @@ -33,9 +33,9 @@ func TestJobQueue_printOutput(t *testing.T) { } cmd.printOutput(testResp) - expect := "JobID | Tenant | Priority\n" + - "------+-------------+---------\n" + - "123 | testTenant1 | 5\n\n" + expect := "JobID | Tenant | Priority\n" + + "----- | ------ | --------\n" + + "123 | testTenant1 | 5\n" must.Eq(t, expect, ui.OutputWriter.String()) }