-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
cli: Add nomad job allocs command (#11242)
- Loading branch information
Showing
6 changed files
with
421 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```release-note:improvement | ||
cli: Add `nomad job allocs` command | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]) | ||
} |
Oops, something went wrong.