Skip to content

Commit

Permalink
cli: Add nomad job allocs command (#11242)
Browse files Browse the repository at this point in the history
  • Loading branch information
davemay99 committed Oct 12, 2021
1 parent 713094f commit f545ac1
Show file tree
Hide file tree
Showing 6 changed files with 421 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .changelog/11242.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
cli: Add `nomad job allocs` command
```
5 changes: 5 additions & 0 deletions command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
159 changes: 159 additions & 0 deletions command/job_allocs.go
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
}
174 changes: 174 additions & 0 deletions command/job_allocs_test.go
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])
}
Loading

0 comments on commit f545ac1

Please sign in to comment.