From 6bb8e5aa58abe9526f6fab1066107479c510f628 Mon Sep 17 00:00:00 2001 From: Preetha Appan Date: Tue, 30 Oct 2018 18:26:23 -0500 Subject: [PATCH 1/4] Show preemption output in plan CLI --- api/allocations.go | 2 + command/job_plan.go | 64 +++++++++++++++++++++++++++++ command/job_plan_test.go | 87 ++++++++++++++++++++++++++++++++++++++++ nomad/structs/structs.go | 4 ++ 4 files changed, 157 insertions(+) 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..8d033c021 100644 --- a/command/job_plan.go +++ b/command/job_plan.go @@ -20,6 +20,7 @@ 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.` + preemptionShowByJobIdThreshold = 10 ) type JobPlanCommand struct { @@ -173,11 +174,74 @@ 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) < preemptionShowByJobIdThreshold { + 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)) + } else { + // 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 + } + var output []string + // Show counts grouped by job ID if its less than a threshold + if numJobs < preemptionShowByJobIdThreshold { + 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..a7edb87cf 100644 --- a/command/job_plan_test.go +++ b/command/job_plan_test.go @@ -7,8 +7,12 @@ import ( "strings" "testing" + "strconv" + + "github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/testutil" "github.com/mitchellh/cli" + require2 "github.com/stretchr/testify/require" ) func TestPlanCommand_Implements(t *testing.T) { @@ -169,3 +173,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 := require2.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) + job_type := "batch" + 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 From 4c97c5ea0cfc5d49dfd8a6875faee739cb07c9ca Mon Sep 17 00:00:00 2001 From: Preetha Appan Date: Mon, 5 Nov 2018 10:46:24 -0600 Subject: [PATCH 2/4] Comments --- command/job_plan.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/command/job_plan.go b/command/job_plan.go index 8d033c021..43b779d24 100644 --- a/command/job_plan.go +++ b/command/job_plan.go @@ -20,7 +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.` - preemptionShowByJobIdThreshold = 10 + + // preemptionDisplayThreshold is an upper bound used to limit and summarize + // the details of preempted jobs in the output + preemptionDisplayThreshold = 10 ) type JobPlanCommand struct { @@ -187,7 +190,7 @@ func (c *JobPlanCommand) Run(args []string) int { // 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) < preemptionShowByJobIdThreshold { + 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 { @@ -213,9 +216,10 @@ func (c *JobPlanCommand) addPreemptions(resp *api.JobPlanResponse) { countMap[id] = cnt + 1 allocDetails[alloc.JobType] = countMap } - var output []string + // Show counts grouped by job ID if its less than a threshold - if numJobs < preemptionShowByJobIdThreshold { + 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 { From c5757f44eec7982a6a23f619ee6879a098ee1e83 Mon Sep 17 00:00:00 2001 From: Preetha Appan Date: Tue, 6 Nov 2018 09:52:32 -0600 Subject: [PATCH 3/4] review feedback --- command/job_plan.go | 83 ++++++++++++++++++++-------------------- command/job_plan_test.go | 7 ++-- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/command/job_plan.go b/command/job_plan.go index 43b779d24..0340cc47f 100644 --- a/command/job_plan.go +++ b/command/job_plan.go @@ -197,48 +197,49 @@ func (c *JobPlanCommand) addPreemptions(resp *api.JobPlanResponse) { allocs = append(allocs, fmt.Sprintf("%s|%s|%s", alloc.ID, alloc.JobID, alloc.TaskGroup)) } c.Ui.Output(formatList(allocs)) - } else { - // 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)) + 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 { diff --git a/command/job_plan_test.go b/command/job_plan_test.go index a7edb87cf..5f74dcaef 100644 --- a/command/job_plan_test.go +++ b/command/job_plan_test.go @@ -4,15 +4,14 @@ import ( "fmt" "io/ioutil" "os" + "strconv" "strings" "testing" - "strconv" - "github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/testutil" "github.com/mitchellh/cli" - require2 "github.com/stretchr/testify/require" + "github.com/stretchr/testify/require" ) func TestPlanCommand_Implements(t *testing.T) { @@ -178,7 +177,7 @@ func TestPlanCommad_Preemptions(t *testing.T) { t.Parallel() ui := new(cli.MockUi) cmd := &JobPlanCommand{Meta: Meta{Ui: ui}} - require := require2.New(t) + require := require.New(t) // Only one preempted alloc resp1 := &api.JobPlanResponse{ From 1d7ebb98eec5aeb173c30c2dd6e5b75918eabc89 Mon Sep 17 00:00:00 2001 From: Preetha Appan Date: Thu, 8 Nov 2018 09:04:49 -0600 Subject: [PATCH 4/4] Fix vet error --- command/job_plan_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/job_plan_test.go b/command/job_plan_test.go index 5f74dcaef..34f4b31fc 100644 --- a/command/job_plan_test.go +++ b/command/job_plan_test.go @@ -225,7 +225,7 @@ func TestPlanCommad_Preemptions(t *testing.T) { // More than 10 unique job IDs preemptedAllocs = make([]*api.AllocationListStub, 0) - job_type := "batch" + var job_type string for i := 0; i < 20; i++ { job_id := "job" + strconv.Itoa(i) if i%2 == 0 {