Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli: Add nomad job allocs command #11242

Merged
merged 6 commits into from
Oct 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -297,6 +297,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