diff --git a/api/allocations.go b/api/allocations.go index 6d860362b..6f5f90220 100644 --- a/api/allocations.go +++ b/api/allocations.go @@ -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 diff --git a/command/job_plan.go b/command/job_plan.go index 5625f9fe4..0340cc47f 100644 --- a/command/job_plan.go +++ b/command/job_plan.go @@ -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 { @@ -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)) + 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. diff --git a/command/job_plan_test.go b/command/job_plan_test.go index 8efefcba6..34f4b31fc 100644 --- a/command/job_plan_test.go +++ b/command/job_plan_test.go @@ -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) { @@ -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") +} diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index f999739ae..d970e522b 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -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, @@ -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