Skip to content

Commit 5081b94

Browse files
authored
Merge pull request #1455 from hashicorp/f-job-summary
Job Summary - Part 2
2 parents 6349cd3 + 2892224 commit 5081b94

34 files changed

+1462
-127
lines changed

api/jobs.go

+31
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,15 @@ func (j *Jobs) Plan(job *Job, diff bool, q *WriteOptions) (*JobPlanResponse, *Wr
159159
return &resp, wm, nil
160160
}
161161

162+
func (j *Jobs) Summary(jobID string, q *QueryOptions) (*JobSummary, *QueryMeta, error) {
163+
var resp JobSummary
164+
qm, err := j.client.query("/v1/job/"+jobID+"/summary", &resp, q)
165+
if err != nil {
166+
return nil, nil, err
167+
}
168+
return &resp, qm, nil
169+
}
170+
162171
// periodicForceResponse is used to deserialize a force response
163172
type periodicForceResponse struct {
164173
EvalID string
@@ -199,6 +208,27 @@ type Job struct {
199208
JobModifyIndex uint64
200209
}
201210

211+
// JobSummary summarizes the state of the allocations of a job
212+
type JobSummary struct {
213+
JobID string
214+
Summary map[string]TaskGroupSummary
215+
216+
// Raft Indexes
217+
CreateIndex uint64
218+
ModifyIndex uint64
219+
}
220+
221+
// TaskGroup summarizes the state of all the allocations of a particular
222+
// TaskGroup
223+
type TaskGroupSummary struct {
224+
Queued int
225+
Complete int
226+
Failed int
227+
Running int
228+
Starting int
229+
Lost int
230+
}
231+
202232
// JobListStub is used to return a subset of information about
203233
// jobs during list operations.
204234
type JobListStub struct {
@@ -209,6 +239,7 @@ type JobListStub struct {
209239
Priority int
210240
Status string
211241
StatusDescription string
242+
JobSummary *JobSummary
212243
CreateIndex uint64
213244
ModifyIndex uint64
214245
JobModifyIndex uint64

api/jobs_test.go

+42
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,48 @@ func TestJobs_Plan(t *testing.T) {
488488
}
489489
}
490490

491+
func TestJobs_JobSummary(t *testing.T) {
492+
c, s := makeClient(t, nil, nil)
493+
defer s.Stop()
494+
jobs := c.Jobs()
495+
496+
// Trying to retrieve a job summary before the job exists
497+
// returns an error
498+
_, _, err := jobs.Summary("job1", nil)
499+
if err == nil || !strings.Contains(err.Error(), "not found") {
500+
t.Fatalf("expected not found error, got: %#v", err)
501+
}
502+
503+
// Register the job
504+
job := testJob()
505+
_, wm, err := jobs.Register(job, nil)
506+
if err != nil {
507+
t.Fatalf("err: %s", err)
508+
}
509+
assertWriteMeta(t, wm)
510+
511+
// Query the job summary again and ensure it exists
512+
result, qm, err := jobs.Summary("job1", nil)
513+
if err != nil {
514+
t.Fatalf("err: %s", err)
515+
}
516+
assertQueryMeta(t, qm)
517+
518+
expectedJobSummary := JobSummary{
519+
JobID: job.ID,
520+
Summary: map[string]TaskGroupSummary{
521+
job.TaskGroups[0].Name: {},
522+
},
523+
CreateIndex: result.CreateIndex,
524+
ModifyIndex: result.ModifyIndex,
525+
}
526+
527+
// Check that the result is what we expect
528+
if !reflect.DeepEqual(&expectedJobSummary, result) {
529+
t.Fatalf("expect: %#v, got: %#v", expectedJobSummary, result)
530+
}
531+
}
532+
491533
func TestJobs_NewBatchJob(t *testing.T) {
492534
job := NewBatchJob("job1", "myjob", "region1", 5)
493535
expect := &Job{

client/client_test.go

+14-4
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,9 @@ func TestClient_UpdateAllocStatus(t *testing.T) {
357357
alloc.ClientStatus = originalStatus
358358

359359
state := s1.State()
360+
if err := state.UpsertJobSummary(99, mock.JobSummary(alloc.JobID)); err != nil {
361+
t.Fatal(err)
362+
}
360363
state.UpsertAllocs(100, []*structs.Allocation{alloc})
361364

362365
testutil.WaitForResult(func() (bool, error) {
@@ -394,6 +397,12 @@ func TestClient_WatchAllocs(t *testing.T) {
394397
alloc2.NodeID = c1.Node().ID
395398

396399
state := s1.State()
400+
if err := state.UpsertJobSummary(998, mock.JobSummary(alloc1.JobID)); err != nil {
401+
t.Fatal(err)
402+
}
403+
if err := state.UpsertJobSummary(999, mock.JobSummary(alloc2.JobID)); err != nil {
404+
t.Fatal(err)
405+
}
397406
err := state.UpsertAllocs(100,
398407
[]*structs.Allocation{alloc1, alloc2})
399408
if err != nil {
@@ -469,8 +478,10 @@ func TestClient_SaveRestoreState(t *testing.T) {
469478
task.Config["args"] = []string{"10"}
470479

471480
state := s1.State()
472-
err := state.UpsertAllocs(100, []*structs.Allocation{alloc1})
473-
if err != nil {
481+
if err := state.UpsertJobSummary(99, mock.JobSummary(alloc1.JobID)); err != nil {
482+
t.Fatal(err)
483+
}
484+
if err := state.UpsertAllocs(100, []*structs.Allocation{alloc1}); err != nil {
474485
t.Fatalf("err: %v", err)
475486
}
476487

@@ -485,8 +496,7 @@ func TestClient_SaveRestoreState(t *testing.T) {
485496
})
486497

487498
// Shutdown the client, saves state
488-
err = c1.Shutdown()
489-
if err != nil {
499+
if err := c1.Shutdown(); err != nil {
490500
t.Fatalf("err: %v", err)
491501
}
492502

command/agent/alloc_endpoint_test.go

+16-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ func TestHTTP_AllocsList(t *testing.T) {
1616
state := s.Agent.server.State()
1717
alloc1 := mock.Alloc()
1818
alloc2 := mock.Alloc()
19+
state.UpsertJobSummary(998, mock.JobSummary(alloc1.JobID))
20+
state.UpsertJobSummary(999, mock.JobSummary(alloc2.JobID))
1921
err := state.UpsertAllocs(1000,
2022
[]*structs.Allocation{alloc1, alloc2})
2123
if err != nil {
@@ -58,13 +60,21 @@ func TestHTTP_AllocsPrefixList(t *testing.T) {
5860
httpTest(t, nil, func(s *TestServer) {
5961
// Directly manipulate the state
6062
state := s.Agent.server.State()
63+
6164
alloc1 := mock.Alloc()
6265
alloc1.ID = "aaaaaaaa-e8f7-fd38-c855-ab94ceb89706"
6366
alloc2 := mock.Alloc()
6467
alloc2.ID = "aaabbbbb-e8f7-fd38-c855-ab94ceb89706"
65-
err := state.UpsertAllocs(1000,
66-
[]*structs.Allocation{alloc1, alloc2})
67-
if err != nil {
68+
summary1 := mock.JobSummary(alloc1.JobID)
69+
summary2 := mock.JobSummary(alloc2.JobID)
70+
if err := state.UpsertJobSummary(998, summary1); err != nil {
71+
t.Fatal(err)
72+
}
73+
if err := state.UpsertJobSummary(999, summary2); err != nil {
74+
t.Fatal(err)
75+
}
76+
if err := state.UpsertAllocs(1000,
77+
[]*structs.Allocation{alloc1, alloc2}); err != nil {
6878
t.Fatalf("err: %v", err)
6979
}
7080

@@ -110,6 +120,9 @@ func TestHTTP_AllocQuery(t *testing.T) {
110120
// Directly manipulate the state
111121
state := s.Agent.server.State()
112122
alloc := mock.Alloc()
123+
if err := state.UpsertJobSummary(999, mock.JobSummary(alloc.JobID)); err != nil {
124+
t.Fatal(err)
125+
}
113126
err := state.UpsertAllocs(1000,
114127
[]*structs.Allocation{alloc})
115128
if err != nil {

command/agent/eval_endpoint_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ func TestHTTP_EvalAllocations(t *testing.T) {
111111
alloc1 := mock.Alloc()
112112
alloc2 := mock.Alloc()
113113
alloc2.EvalID = alloc1.EvalID
114+
state.UpsertJobSummary(998, mock.JobSummary(alloc1.JobID))
115+
state.UpsertJobSummary(999, mock.JobSummary(alloc2.JobID))
114116
err := state.UpsertAllocs(1000,
115117
[]*structs.Allocation{alloc1, alloc2})
116118
if err != nil {

command/agent/job_endpoint.go

+24
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ func (s *HTTPServer) JobSpecificRequest(resp http.ResponseWriter, req *http.Requ
5454
case strings.HasSuffix(path, "/plan"):
5555
jobName := strings.TrimSuffix(path, "/plan")
5656
return s.jobPlan(resp, req, jobName)
57+
case strings.HasSuffix(path, "/summary"):
58+
jobName := strings.TrimSuffix(path, "/summary")
59+
return s.jobSummaryRequest(resp, req, jobName)
5760
default:
5861
return s.jobCRUD(resp, req, path)
5962
}
@@ -241,3 +244,24 @@ func (s *HTTPServer) jobDelete(resp http.ResponseWriter, req *http.Request,
241244
setIndex(resp, out.Index)
242245
return out, nil
243246
}
247+
248+
func (s *HTTPServer) jobSummaryRequest(resp http.ResponseWriter, req *http.Request, name string) (interface{}, error) {
249+
args := structs.JobSummaryRequest{
250+
JobID: name,
251+
}
252+
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
253+
return nil, nil
254+
}
255+
256+
var out structs.JobSummaryResponse
257+
if err := s.agent.RPC("Job.Summary", &args, &out); err != nil {
258+
return nil, err
259+
}
260+
261+
setMeta(resp, &out.QueryMeta)
262+
if out.JobSummary == nil {
263+
return nil, CodedError(404, "job not found")
264+
}
265+
setIndex(resp, out.Index)
266+
return out.JobSummary, nil
267+
}

command/agent/node_endpoint_test.go

+9
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ func TestHTTP_NodeForceEval(t *testing.T) {
129129
state := s.Agent.server.State()
130130
alloc1 := mock.Alloc()
131131
alloc1.NodeID = node.ID
132+
if err := state.UpsertJobSummary(999, mock.JobSummary(alloc1.JobID)); err != nil {
133+
t.Fatal(err)
134+
}
132135
err := state.UpsertAllocs(1000, []*structs.Allocation{alloc1})
133136
if err != nil {
134137
t.Fatalf("err: %v", err)
@@ -177,6 +180,9 @@ func TestHTTP_NodeAllocations(t *testing.T) {
177180
state := s.Agent.server.State()
178181
alloc1 := mock.Alloc()
179182
alloc1.NodeID = node.ID
183+
if err := state.UpsertJobSummary(999, mock.JobSummary(alloc1.JobID)); err != nil {
184+
t.Fatal(err)
185+
}
180186
err := state.UpsertAllocs(1000, []*structs.Allocation{alloc1})
181187
if err != nil {
182188
t.Fatalf("err: %v", err)
@@ -231,6 +237,9 @@ func TestHTTP_NodeDrain(t *testing.T) {
231237
state := s.Agent.server.State()
232238
alloc1 := mock.Alloc()
233239
alloc1.NodeID = node.ID
240+
if err := state.UpsertJobSummary(999, mock.JobSummary(alloc1.JobID)); err != nil {
241+
t.Fatal(err)
242+
}
234243
err := state.UpsertAllocs(1000, []*structs.Allocation{alloc1})
235244
if err != nil {
236245
t.Fatalf("err: %v", err)

command/status.go

+23
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,29 @@ func (c *StatusCommand) outputJobInfo(client *api.Client, job *api.Job) error {
246246
return fmt.Errorf("Error querying job evaluations: %s", err)
247247
}
248248

249+
// Query the summary
250+
summary, _, err := client.Jobs().Summary(job.ID, nil)
251+
if err != nil {
252+
return fmt.Errorf("Error querying job summary: %s", err)
253+
}
254+
255+
// Format the summary
256+
c.Ui.Output(c.Colorize().Color("\n[bold]Summary[reset]"))
257+
if summary != nil {
258+
summaries := make([]string, len(summary.Summary)+1)
259+
summaries[0] = "Task Group|Queued|Starting|Running|Failed|Complete|Lost"
260+
idx := 1
261+
for tg, tgs := range summary.Summary {
262+
summaries[idx] = fmt.Sprintf("%s|%d|%d|%d|%d|%d|%d",
263+
tg, tgs.Queued, tgs.Starting,
264+
tgs.Running, tgs.Failed,
265+
tgs.Complete, tgs.Lost,
266+
)
267+
idx += 1
268+
}
269+
c.Ui.Output(formatList(summaries))
270+
}
271+
249272
// Determine latest evaluation with failures whose follow up hasn't
250273
// completed, this is done while formatting
251274
var latestFailedPlacement *api.Evaluation

command/status_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ func TestStatusCommand_Run(t *testing.T) {
6363
if !strings.Contains(out, "Allocations") {
6464
t.Fatalf("should dump allocations")
6565
}
66+
if !strings.Contains(out, "Summary") {
67+
t.Fatalf("should dump summary")
68+
}
6669
ui.OutputWriter.Reset()
6770

6871
// Query a single job showing evals

nomad/alloc_endpoint_test.go

+20-6
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@ func TestAllocEndpoint_List(t *testing.T) {
1919

2020
// Create the register request
2121
alloc := mock.Alloc()
22+
summary := mock.JobSummary(alloc.JobID)
2223
state := s1.fsm.State()
23-
err := state.UpsertAllocs(1000, []*structs.Allocation{alloc})
24-
if err != nil {
24+
25+
if err := state.UpsertJobSummary(999, summary); err != nil {
26+
t.Fatalf("err: %v", err)
27+
}
28+
if err := state.UpsertAllocs(1000, []*structs.Allocation{alloc}); err != nil {
2529
t.Fatalf("err: %v", err)
2630
}
2731

@@ -75,6 +79,10 @@ func TestAllocEndpoint_List_Blocking(t *testing.T) {
7579
// Create the alloc
7680
alloc := mock.Alloc()
7781

82+
summary := mock.JobSummary(alloc.JobID)
83+
if err := state.UpsertJobSummary(1, summary); err != nil {
84+
t.Fatalf("err: %v", err)
85+
}
7886
// Upsert alloc triggers watches
7987
time.AfterFunc(100*time.Millisecond, func() {
8088
if err := state.UpsertAllocs(2, []*structs.Allocation{alloc}); err != nil {
@@ -109,12 +117,13 @@ func TestAllocEndpoint_List_Blocking(t *testing.T) {
109117
alloc2.ID = alloc.ID
110118
alloc2.ClientStatus = structs.AllocClientStatusRunning
111119
time.AfterFunc(100*time.Millisecond, func() {
112-
if err := state.UpdateAllocsFromClient(3, []*structs.Allocation{alloc2}); err != nil {
120+
state.UpsertJobSummary(3, mock.JobSummary(alloc2.JobID))
121+
if err := state.UpdateAllocsFromClient(4, []*structs.Allocation{alloc2}); err != nil {
113122
t.Fatalf("err: %v", err)
114123
}
115124
})
116125

117-
req.MinQueryIndex = 2
126+
req.MinQueryIndex = 3
118127
start = time.Now()
119128
var resp2 structs.AllocListResponse
120129
if err := msgpackrpc.CallWithCodec(codec, "Alloc.List", req, &resp2); err != nil {
@@ -124,8 +133,8 @@ func TestAllocEndpoint_List_Blocking(t *testing.T) {
124133
if elapsed := time.Since(start); elapsed < 100*time.Millisecond {
125134
t.Fatalf("should block (returned in %s) %#v", elapsed, resp2)
126135
}
127-
if resp2.Index != 3 {
128-
t.Fatalf("Bad index: %d %d", resp2.Index, 3)
136+
if resp2.Index != 4 {
137+
t.Fatalf("Bad index: %d %d", resp2.Index, 4)
129138
}
130139
if len(resp2.Allocations) != 1 || resp.Allocations[0].ID != alloc.ID ||
131140
resp2.Allocations[0].ClientStatus != structs.AllocClientStatusRunning {
@@ -142,6 +151,7 @@ func TestAllocEndpoint_GetAlloc(t *testing.T) {
142151
// Create the register request
143152
alloc := mock.Alloc()
144153
state := s1.fsm.State()
154+
state.UpsertJobSummary(999, mock.JobSummary(alloc.JobID))
145155
err := state.UpsertAllocs(1000, []*structs.Allocation{alloc})
146156
if err != nil {
147157
t.Fatalf("err: %v", err)
@@ -178,6 +188,7 @@ func TestAllocEndpoint_GetAlloc_Blocking(t *testing.T) {
178188

179189
// First create an unrelated alloc
180190
time.AfterFunc(100*time.Millisecond, func() {
191+
state.UpsertJobSummary(99, mock.JobSummary(alloc1.JobID))
181192
err := state.UpsertAllocs(100, []*structs.Allocation{alloc1})
182193
if err != nil {
183194
t.Fatalf("err: %v", err)
@@ -186,6 +197,7 @@ func TestAllocEndpoint_GetAlloc_Blocking(t *testing.T) {
186197

187198
// Create the alloc we are watching later
188199
time.AfterFunc(200*time.Millisecond, func() {
200+
state.UpsertJobSummary(999, mock.JobSummary(alloc2.JobID))
189201
err := state.UpsertAllocs(200, []*structs.Allocation{alloc2})
190202
if err != nil {
191203
t.Fatalf("err: %v", err)
@@ -227,6 +239,8 @@ func TestAllocEndpoint_GetAllocs(t *testing.T) {
227239
alloc := mock.Alloc()
228240
alloc2 := mock.Alloc()
229241
state := s1.fsm.State()
242+
state.UpsertJobSummary(998, mock.JobSummary(alloc.JobID))
243+
state.UpsertJobSummary(999, mock.JobSummary(alloc2.JobID))
230244
err := state.UpsertAllocs(1000, []*structs.Allocation{alloc, alloc2})
231245
if err != nil {
232246
t.Fatalf("err: %v", err)

0 commit comments

Comments
 (0)