cli: Add nomad job allocs command (#11242)

This commit is contained in:
Dave May 2021-10-12 16:30:36 -04:00 committed by GitHub
parent 3e0bad5a41
commit 76b05f3cd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 421 additions and 0 deletions

3
.changelog/11242.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
cli: Add `nomad job allocs` command
```

View File

@ -300,6 +300,11 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory {
Meta: meta,
}, nil
},
"job allocs": func() (cli.Command, error) {
return &JobAllocsCommand{
Meta: meta,
}, nil
},
"job deployments": func() (cli.Command, error) {
return &JobDeploymentsCommand{
Meta: meta,

159
command/job_allocs.go Normal file
View File

@ -0,0 +1,159 @@
package command
import (
"fmt"
"strings"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/api/contexts"
"github.com/posener/complete"
)
type JobAllocsCommand struct {
Meta
}
func (c *JobAllocsCommand) Help() string {
helpText := `
Usage: nomad job allocs [options] <job>
Display allocations for a particular job.
When ACLs are enabled, this command requires a token with the 'read-job' and
'list-jobs' capabilities for the job's namespace.
General Options:
` + generalOptionsUsage(usageOptsDefault) + `
Allocs Options:
-all
Display all allocations matching the job ID, even those from an older
instance of the job.
-json
Output the allocations in a JSON format.
-t
Format and display allocations using a Go template.
-verbose
Display full information.
`
return strings.TrimSpace(helpText)
}
func (c *JobAllocsCommand) Synopsis() string {
return "List allocations for a job"
}
func (c *JobAllocsCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-json": complete.PredictNothing,
"-t": complete.PredictAnything,
"-verbose": complete.PredictNothing,
"-all": complete.PredictNothing,
})
}
func (c *JobAllocsCommand) 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.Jobs, nil)
if err != nil {
return []string{}
}
return resp.Matches[contexts.Jobs]
})
}
func (c *JobAllocsCommand) Name() string { return "job allocations" }
func (c *JobAllocsCommand) Run(args []string) int {
var json, verbose, all bool
var tmpl string
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.BoolVar(&verbose, "verbose", false, "")
flags.BoolVar(&all, "all", 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 job
args = flags.Args()
if len(args) != 1 {
c.Ui.Error("This command takes one argument: <job>")
c.Ui.Error(commandErrorText(c))
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 1
}
jobID := strings.TrimSpace(args[0])
// Check if the job exists
jobs, _, err := client.Jobs().PrefixList(jobID)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error listing jobs: %s", err))
return 1
}
if len(jobs) == 0 {
c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID))
return 1
}
if len(jobs) > 1 {
if jobID != jobs[0].ID {
c.Ui.Error(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs, c.allNamespaces())))
return 1
}
if c.allNamespaces() && jobs[0].ID == jobs[1].ID {
c.Ui.Error(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs, c.allNamespaces())))
return 1
}
}
jobID = jobs[0].ID
q := &api.QueryOptions{Namespace: jobs[0].JobSummary.Namespace}
allocs, _, err := client.Jobs().Allocations(jobID, all, q)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error retrieving allocations: %s", err))
return 1
}
if json || len(tmpl) > 0 {
out, err := Format(json, tmpl, allocs)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
c.Ui.Output(out)
return 0
}
// Truncate the id unless full length is requested
length := shortId
if verbose {
length = fullId
}
c.Ui.Output(formatAllocListStubs(allocs, verbose, length))
return 0
}

174
command/job_allocs_test.go Normal file
View File

@ -0,0 +1,174 @@
package command
import (
"testing"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/nomad/mock"
"github.com/mitchellh/cli"
"github.com/posener/complete"
"github.com/stretchr/testify/require"
)
func TestJobAllocsCommand_Implements(t *testing.T) {
t.Parallel()
var _ cli.Command = &JobAllocsCommand{}
}
func TestJobAllocsCommand_Fails(t *testing.T) {
t.Parallel()
srv, _, url := testServer(t, true, nil)
defer srv.Shutdown()
ui := cli.NewMockUi()
cmd := &JobAllocsCommand{Meta: Meta{Ui: ui}}
// Fails on misuse
code := cmd.Run([]string{"some", "bad", "args"})
outerr := ui.ErrorWriter.String()
require.Equalf(t, 1, code, "expected exit code 1, got: %d", code)
require.Containsf(t, outerr, commandErrorText(cmd), "expected help output, got: %s", outerr)
ui.ErrorWriter.Reset()
// Bad address
code = cmd.Run([]string{"-address=nope", "foo"})
outerr = ui.ErrorWriter.String()
require.Equalf(t, 1, code, "expected exit code 1, got: %d", code)
require.Containsf(t, outerr, "Error listing jobs", "expected failed query error, got: %s", outerr)
ui.ErrorWriter.Reset()
// Bad job name
code = cmd.Run([]string{"-address=" + url, "foo"})
outerr = ui.ErrorWriter.String()
require.Equalf(t, 1, code, "expected exit 1, got: %d", code)
require.Containsf(t, outerr, "No job(s) with prefix or id \"foo\" found", "expected no job found, got: %s", outerr)
ui.ErrorWriter.Reset()
}
func TestJobAllocsCommand_Run(t *testing.T) {
t.Parallel()
srv, _, url := testServer(t, true, nil)
defer srv.Shutdown()
ui := cli.NewMockUi()
cmd := &JobAllocsCommand{Meta: Meta{Ui: ui}}
// Create a job without an allocation
job := mock.Job()
state := srv.Agent.Server().State()
require.Nil(t, state.UpsertJob(structs.MsgTypeTestSetup, 100, job))
// Should display no match if the job doesn't have allocations
code := cmd.Run([]string{"-address=" + url, job.ID})
out := ui.OutputWriter.String()
require.Equalf(t, 0, code, "expected exit 0, got: %d", code)
require.Containsf(t, out, "No allocations placed", "expected no allocations placed, got: %s", out)
ui.OutputWriter.Reset()
// Inject an allocation
a := mock.Alloc()
a.Job = job
a.JobID = job.ID
a.TaskGroup = job.TaskGroups[0].Name
a.Metrics = &structs.AllocMetric{}
a.DesiredStatus = structs.AllocDesiredStatusRun
a.ClientStatus = structs.AllocClientStatusRunning
require.Nil(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 200, []*structs.Allocation{a}))
// Should now display the alloc
code = cmd.Run([]string{"-address=" + url, "-verbose", job.ID})
out = ui.OutputWriter.String()
outerr := ui.ErrorWriter.String()
require.Equalf(t, 0, code, "expected exit 0, got: %d", code)
require.Emptyf(t, outerr, "expected no error output, got: \n\n%s", outerr)
require.Containsf(t, out, a.ID, "expected alloc output, got: %s", out)
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
}
func TestJobAllocsCommand_Template(t *testing.T) {
t.Parallel()
srv, _, url := testServer(t, true, nil)
defer srv.Shutdown()
ui := cli.NewMockUi()
cmd := &JobAllocsCommand{Meta: Meta{Ui: ui}}
// Create a job
job := mock.Job()
state := srv.Agent.Server().State()
require.Nil(t, state.UpsertJob(structs.MsgTypeTestSetup, 100, job))
// Inject a running allocation
a := mock.Alloc()
a.Job = job
a.JobID = job.ID
a.TaskGroup = job.TaskGroups[0].Name
a.Metrics = &structs.AllocMetric{}
a.DesiredStatus = structs.AllocDesiredStatusRun
a.ClientStatus = structs.AllocClientStatusRunning
require.Nil(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 200, []*structs.Allocation{a}))
// Inject a pending allocation
b := mock.Alloc()
b.Job = job
b.JobID = job.ID
b.TaskGroup = job.TaskGroups[0].Name
b.Metrics = &structs.AllocMetric{}
b.DesiredStatus = structs.AllocDesiredStatusRun
b.ClientStatus = structs.AllocClientStatusPending
require.Nil(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 300, []*structs.Allocation{b}))
// Should display an AllocacitonListStub object
code := cmd.Run([]string{"-address=" + url, "-t", "'{{printf \"%#+v\" .}}'", job.ID})
out := ui.OutputWriter.String()
outerr := ui.ErrorWriter.String()
require.Equalf(t, 0, code, "expected exit 0, got: %d", code)
require.Emptyf(t, outerr, "expected no error output, got: \n\n%s", outerr)
require.Containsf(t, out, "api.AllocationListStub", "expected alloc output, got: %s", out)
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
// Should display only the running allocation ID
code = cmd.Run([]string{"-address=" + url, "-t", "'{{ range . }}{{ if eq .ClientStatus \"running\" }}{{ println .ID }}{{ end }}{{ end }}'", job.ID})
out = ui.OutputWriter.String()
outerr = ui.ErrorWriter.String()
require.Equalf(t, 0, code, "expected exit 0, got: %d", code)
require.Emptyf(t, outerr, "expected no error output, got: \n\n%s", outerr)
require.Containsf(t, out, a.ID, "expected ID of alloc a, got: %s", out)
require.NotContainsf(t, out, b.ID, "should not contain ID of alloc b, got: %s", out)
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
}
func TestJobAllocsCommand_AutocompleteArgs(t *testing.T) {
t.Parallel()
srv, _, url := testServer(t, true, nil)
defer srv.Shutdown()
ui := cli.NewMockUi()
cmd := &JobAllocsCommand{Meta: Meta{Ui: ui, flagAddress: url}}
// Create a fake job
state := srv.Agent.Server().State()
j := mock.Job()
require.Nil(t, state.UpsertJob(structs.MsgTypeTestSetup, 1000, j))
prefix := j.ID[:len(j.ID)-5]
args := complete.Args{Last: prefix}
predictor := cmd.AutocompleteArgs()
res := predictor.Predict(args)
require.Equal(t, 1, len(res))
require.Equal(t, j.ID, res[0])
}

View File

@ -0,0 +1,76 @@
---
layout: docs
page_title: 'Commands: job allocs'
description: |
The allocs command is used to list allocations for a job.
---
# Command: job allocs
The `job allocs` command is used to display the allocations for a
particular job.
## Usage
```plaintext
nomad job allocs [options] <job>
```
The `job allocs` command requires a single argument, the job ID or an ID
prefix of a job to display the list of allocations for.
When ACLs are enabled, this command requires a token with the `read-job` and
`list-jobs` capabilities for the job's namespace.
## General Options
@include 'general_options.mdx'
## Allocs Options
- `-all`: Display all allocations matching the job ID, even those from an
older instance of the job.
- `-json`: Output the allocations in JSON format.
- `-t`: Format and display the allocations using a Go template.
- `-verbose`: Show full information.
## Examples
List the allocations for a particular job:
```shell-session
$ nomad job allocs example
ID Node ID Task Group Version Desired Status Created Modified
c2b4606d 35085106 cache 2 run running 21s ago 10s ago
c413424b 35085106 cache 2 run pending 1m8s ago 11s ago
```
Verbose listing of allocations for a particular job:
```shell-session
$ nomad job allocs -verbose example
ID Eval ID Node ID Node Name Task Group Version Desired Status Created Modified
c2b4606d-1b02-0d8d-5fdd-031167cd4c91 5e2badb6-b7cf-5177-8281-8fe14f7193d2 35085106-9480-b465-a348-deb745024394 ubuntu cache 2 run running 2021-09-23T14:45:09-04:00 2021-09-23T14:45:19-04:00
c413424b-d80e-9bc6-ea92-a02b336eaaf5 5e2badb6-b7cf-5177-8281-8fe14f7193d2 35085106-9480-b465-a348-deb745024394 ubuntu cache 2 run pending 2021-09-23T14:44:22-04:00 2021-09-23T14:45:19-04:00
```
Format job allocations using a Go template:
```shell-session
$ nomad job allocs -t '{{ range . }}{{ println .ID }}{{ end }}' example
c2b4606d-1b02-0d8d-5fdd-031167cd4c91
c413424b-d80e-9bc6-ea92-a02b336eaaf5
```
Use a Go template to filter only allocations which are running
```shell-session
$ nomad job allocs -t '{{ range . }}{{ if eq .ClientStatus "running" }}{{ println .ID }}{{ end }}{{ end }}' example
c2b4606d-1b02-0d8d-5fdd-031167cd4c91
```
Refer to the [Format Nomad Command Output With Templates][format_tutorial]
tutorial for more examples of using Go templates to format Nomad CLI output.
[format_tutorial]: https://learn.hashicorp.com/tutorials/nomad/format-output-with-templates

View File

@ -359,6 +359,10 @@
"title": "Overview",
"path": "commands/job"
},
{
"title": "allocs",
"path": "commands/job/allocs"
},
{
"title": "deployments",
"path": "commands/job/deployments"