cli: Add nomad job allocs command (#11242)
This commit is contained in:
parent
3e0bad5a41
commit
76b05f3cd2
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
cli: Add `nomad job allocs` command
|
||||
```
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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])
|
||||
}
|
|
@ -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
|
|
@ -359,6 +359,10 @@
|
|||
"title": "Overview",
|
||||
"path": "commands/job"
|
||||
},
|
||||
{
|
||||
"title": "allocs",
|
||||
"path": "commands/job/allocs"
|
||||
},
|
||||
{
|
||||
"title": "deployments",
|
||||
"path": "commands/job/deployments"
|
||||
|
|
Loading…
Reference in New Issue