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

show preemptions in nomad plan CLI #4823

Merged
merged 4 commits into from
Nov 8, 2018
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
2 changes: 2 additions & 0 deletions api/allocations.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,10 @@ type AllocationListStub struct {
ID string
EvalID string
Name string
Namespace string
NodeID string
JobID string
JobType string
JobVersion uint64
TaskGroup string
DesiredStatus string
Expand Down
69 changes: 69 additions & 0 deletions command/job_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ When running the job with the check-index flag, the job will only be run if the
server side version matches the job modify index returned. If the index has
changed, another user has modified the job and the plan's results are
potentially invalid.`

// preemptionDisplayThreshold is an upper bound used to limit and summarize
// the details of preempted jobs in the output
preemptionDisplayThreshold = 10
)

type JobPlanCommand struct {
Expand Down Expand Up @@ -173,11 +177,76 @@ func (c *JobPlanCommand) Run(args []string) int {
c.Colorize().Color(fmt.Sprintf("[bold][yellow]Job Warnings:\n%s[reset]\n", resp.Warnings)))
}

// Print preemptions if there are any
if resp.Annotations != nil && len(resp.Annotations.PreemptedAllocs) > 0 {
c.addPreemptions(resp)
}

// Print the job index info
c.Ui.Output(c.Colorize().Color(formatJobModifyIndex(resp.JobModifyIndex, path)))
return getExitCode(resp)
}

// addPreemptions shows details about preempted allocations
func (c *JobPlanCommand) addPreemptions(resp *api.JobPlanResponse) {
c.Ui.Output(c.Colorize().Color("[bold][yellow]Preemptions:\n[reset]"))
if len(resp.Annotations.PreemptedAllocs) < preemptionDisplayThreshold {
var allocs []string
allocs = append(allocs, fmt.Sprintf("Alloc ID|Job ID|Task Group"))
for _, alloc := range resp.Annotations.PreemptedAllocs {
allocs = append(allocs, fmt.Sprintf("%s|%s|%s", alloc.ID, alloc.JobID, alloc.TaskGroup))
}
c.Ui.Output(formatList(allocs))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Feel free to return early here and remove else. It would reduce nesting and make the big else clause easier to follow.

return
}
// Display in a summary format if the list is too large
// Group by job type and job ids
allocDetails := make(map[string]map[namespaceIdPair]int)
numJobs := 0
for _, alloc := range resp.Annotations.PreemptedAllocs {
id := namespaceIdPair{alloc.JobID, alloc.Namespace}
countMap := allocDetails[alloc.JobType]
if countMap == nil {
countMap = make(map[namespaceIdPair]int)
}
cnt, ok := countMap[id]
if !ok {
// First time we are seeing this job, increment counter
numJobs++
}
countMap[id] = cnt + 1
allocDetails[alloc.JobType] = countMap
}

// Show counts grouped by job ID if its less than a threshold
var output []string
if numJobs < preemptionDisplayThreshold {
output = append(output, fmt.Sprintf("Job ID|Namespace|Job Type|Preemptions"))
for jobType, jobCounts := range allocDetails {
for jobId, count := range jobCounts {
output = append(output, fmt.Sprintf("%s|%s|%s|%d", jobId.id, jobId.namespace, jobType, count))
}
}
} else {
// Show counts grouped by job type
output = append(output, fmt.Sprintf("Job Type|Preemptions"))
for jobType, jobCounts := range allocDetails {
total := 0
for _, count := range jobCounts {
total += count
}
output = append(output, fmt.Sprintf("%s|%d", jobType, total))
}
}
c.Ui.Output(formatList(output))

}

type namespaceIdPair struct {
id string
namespace string
}

// getExitCode returns 0:
// * 0: No allocations created or destroyed.
// * 1: Allocations created or destroyed.
Expand Down
86 changes: 86 additions & 0 deletions command/job_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import (
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
"testing"

"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/testutil"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)

func TestPlanCommand_Implements(t *testing.T) {
Expand Down Expand Up @@ -169,3 +172,86 @@ func TestPlanCommand_From_URL(t *testing.T) {
t.Fatalf("expected error getting jobfile, got: %s", out)
}
}

func TestPlanCommad_Preemptions(t *testing.T) {
t.Parallel()
ui := new(cli.MockUi)
cmd := &JobPlanCommand{Meta: Meta{Ui: ui}}
require := require.New(t)

// Only one preempted alloc
resp1 := &api.JobPlanResponse{
Annotations: &api.PlanAnnotations{
PreemptedAllocs: []*api.AllocationListStub{
{
ID: "alloc1",
JobID: "jobID1",
TaskGroup: "meta",
JobType: "batch",
Namespace: "test",
},
},
},
}
cmd.addPreemptions(resp1)
out := ui.OutputWriter.String()
require.Contains(out, "Alloc ID")
require.Contains(out, "alloc1")

// Less than 10 unique job ids
var preemptedAllocs []*api.AllocationListStub
for i := 0; i < 12; i++ {
job_id := "job" + strconv.Itoa(i%4)
alloc := &api.AllocationListStub{
ID: "alloc",
JobID: job_id,
TaskGroup: "meta",
JobType: "batch",
Namespace: "test",
}
preemptedAllocs = append(preemptedAllocs, alloc)
}

resp2 := &api.JobPlanResponse{
Annotations: &api.PlanAnnotations{
PreemptedAllocs: preemptedAllocs,
},
}
ui.OutputWriter.Reset()
cmd.addPreemptions(resp2)
out = ui.OutputWriter.String()
require.Contains(out, "Job ID")
require.Contains(out, "Namespace")

// More than 10 unique job IDs
preemptedAllocs = make([]*api.AllocationListStub, 0)
var job_type string
for i := 0; i < 20; i++ {
job_id := "job" + strconv.Itoa(i)
if i%2 == 0 {
job_type = "service"
} else {
job_type = "batch"
}
alloc := &api.AllocationListStub{
ID: "alloc",
JobID: job_id,
TaskGroup: "meta",
JobType: job_type,
Namespace: "test",
}
preemptedAllocs = append(preemptedAllocs, alloc)
}

resp3 := &api.JobPlanResponse{
Annotations: &api.PlanAnnotations{
PreemptedAllocs: preemptedAllocs,
},
}
ui.OutputWriter.Reset()
cmd.addPreemptions(resp3)
out = ui.OutputWriter.String()
require.Contains(out, "Job Type")
require.Contains(out, "batch")
require.Contains(out, "service")
}
4 changes: 4 additions & 0 deletions nomad/structs/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -7538,8 +7538,10 @@ func (a *Allocation) Stub() *AllocListStub {
ID: a.ID,
EvalID: a.EvalID,
Name: a.Name,
Namespace: a.Namespace,
NodeID: a.NodeID,
JobID: a.JobID,
JobType: a.Job.Type,
JobVersion: a.Job.Version,
TaskGroup: a.TaskGroup,
DesiredStatus: a.DesiredStatus,
Expand All @@ -7563,8 +7565,10 @@ type AllocListStub struct {
ID string
EvalID string
Name string
Namespace string
NodeID string
JobID string
JobType string
JobVersion uint64
TaskGroup string
DesiredStatus string
Expand Down