1package command
2
3import (
4	"fmt"
5	"regexp"
6	"strings"
7	"testing"
8	"time"
9
10	"github.com/hashicorp/nomad/api"
11	"github.com/hashicorp/nomad/command/agent"
12	"github.com/hashicorp/nomad/nomad/mock"
13	"github.com/hashicorp/nomad/nomad/structs"
14	"github.com/hashicorp/nomad/testutil"
15	"github.com/mitchellh/cli"
16	"github.com/posener/complete"
17	"github.com/stretchr/testify/assert"
18	"github.com/stretchr/testify/require"
19)
20
21func TestJobStatusCommand_Implements(t *testing.T) {
22	t.Parallel()
23	var _ cli.Command = &JobStatusCommand{}
24}
25
26func TestJobStatusCommand_Run(t *testing.T) {
27	t.Parallel()
28	srv, client, url := testServer(t, true, nil)
29	defer srv.Shutdown()
30	testutil.WaitForResult(func() (bool, error) {
31		nodes, _, err := client.Nodes().List(nil)
32		if err != nil {
33			return false, err
34		}
35		if len(nodes) == 0 {
36			return false, fmt.Errorf("missing node")
37		}
38		if _, ok := nodes[0].Drivers["mock_driver"]; !ok {
39			return false, fmt.Errorf("mock_driver not ready")
40		}
41		return true, nil
42	}, func(err error) {
43		t.Fatalf("err: %s", err)
44	})
45
46	ui := new(cli.MockUi)
47	cmd := &JobStatusCommand{Meta: Meta{Ui: ui}}
48
49	// Should return blank for no jobs
50	if code := cmd.Run([]string{"-address=" + url}); code != 0 {
51		t.Fatalf("expected exit 0, got: %d", code)
52	}
53
54	// Check for this awkward nil string, since a nil bytes.Buffer
55	// returns this purposely, and mitchellh/cli has a nil pointer
56	// if nothing was ever output.
57	exp := "No running jobs"
58	if out := strings.TrimSpace(ui.OutputWriter.String()); out != exp {
59		t.Fatalf("expected %q; got: %q", exp, out)
60	}
61
62	// Register two jobs
63	job1 := testJob("job1_sfx")
64	resp, _, err := client.Jobs().Register(job1, nil)
65	if err != nil {
66		t.Fatalf("err: %s", err)
67	}
68	if code := waitForSuccess(ui, client, fullId, t, resp.EvalID); code != 0 {
69		t.Fatalf("status code non zero saw %d", code)
70	}
71
72	job2 := testJob("job2_sfx")
73	resp2, _, err := client.Jobs().Register(job2, nil)
74	if err != nil {
75		t.Fatalf("err: %s", err)
76	}
77	if code := waitForSuccess(ui, client, fullId, t, resp2.EvalID); code != 0 {
78		t.Fatalf("status code non zero saw %d", code)
79	}
80
81	// Query again and check the result
82	if code := cmd.Run([]string{"-address=" + url}); code != 0 {
83		t.Fatalf("expected exit 0, got: %d", code)
84	}
85	out := ui.OutputWriter.String()
86	if !strings.Contains(out, "job1_sfx") || !strings.Contains(out, "job2_sfx") {
87		t.Fatalf("expected job1_sfx and job2_sfx, got: %s", out)
88	}
89	ui.OutputWriter.Reset()
90
91	// Query a single job
92	if code := cmd.Run([]string{"-address=" + url, "job2_sfx"}); code != 0 {
93		t.Fatalf("expected exit 0, got: %d", code)
94	}
95	out = ui.OutputWriter.String()
96	if strings.Contains(out, "job1_sfx") || !strings.Contains(out, "job2_sfx") {
97		t.Fatalf("expected only job2_sfx, got: %s", out)
98	}
99	if !strings.Contains(out, "Allocations") {
100		t.Fatalf("should dump allocations")
101	}
102	if !strings.Contains(out, "Summary") {
103		t.Fatalf("should dump summary")
104	}
105	ui.OutputWriter.Reset()
106
107	// Query a single job showing evals
108	if code := cmd.Run([]string{"-address=" + url, "-evals", "job2_sfx"}); code != 0 {
109		t.Fatalf("expected exit 0, got: %d", code)
110	}
111	out = ui.OutputWriter.String()
112	if strings.Contains(out, "job1_sfx") || !strings.Contains(out, "job2_sfx") {
113		t.Fatalf("expected only job2_sfx, got: %s", out)
114	}
115	if !strings.Contains(out, "Evaluations") {
116		t.Fatalf("should dump evaluations")
117	}
118	if !strings.Contains(out, "Allocations") {
119		t.Fatalf("should dump allocations")
120	}
121	ui.OutputWriter.Reset()
122
123	// Query a single job in verbose mode
124	if code := cmd.Run([]string{"-address=" + url, "-verbose", "job2_sfx"}); code != 0 {
125		t.Fatalf("expected exit 0, got: %d", code)
126	}
127
128	nodeName := ""
129	if allocs, _, err := client.Jobs().Allocations("job2_sfx", false, nil); err == nil {
130		if len(allocs) > 0 {
131			nodeName = allocs[0].NodeName
132		}
133	}
134
135	out = ui.OutputWriter.String()
136	if strings.Contains(out, "job1_sfx") || !strings.Contains(out, "job2_sfx") {
137		t.Fatalf("expected only job2_sfx, got: %s", out)
138	}
139	if !strings.Contains(out, "Evaluations") {
140		t.Fatalf("should dump evaluations")
141	}
142	if !strings.Contains(out, "Allocations") {
143		t.Fatalf("should dump allocations")
144	}
145	if !strings.Contains(out, "Created") {
146		t.Fatal("should have created header")
147	}
148	if !strings.Contains(out, "Modified") {
149		t.Fatal("should have modified header")
150	}
151
152	// string calculations based on 1-byte chars, not using runes
153	allocationsTableName := "Allocations\n"
154	allocationsTableStr := strings.Split(out, allocationsTableName)[1]
155	nodeNameHeaderStr := "Node Name"
156	nodeNameHeaderIndex := strings.Index(allocationsTableStr, nodeNameHeaderStr)
157	nodeNameRegexpStr := fmt.Sprintf(`.*%s.*\n.{%d}%s`, nodeNameHeaderStr, nodeNameHeaderIndex, regexp.QuoteMeta(nodeName))
158	require.Regexp(t, regexp.MustCompile(nodeNameRegexpStr), out)
159
160	ui.ErrorWriter.Reset()
161	ui.OutputWriter.Reset()
162
163	// Query jobs with prefix match
164	if code := cmd.Run([]string{"-address=" + url, "job"}); code != 1 {
165		t.Fatalf("expected exit 1, got: %d", code)
166	}
167	out = ui.ErrorWriter.String()
168	if !strings.Contains(out, "job1_sfx") || !strings.Contains(out, "job2_sfx") {
169		t.Fatalf("expected job1_sfx and job2_sfx, got: %s", out)
170	}
171	ui.ErrorWriter.Reset()
172	ui.OutputWriter.Reset()
173
174	// Query a single job with prefix match
175	if code := cmd.Run([]string{"-address=" + url, "job1"}); code != 0 {
176		t.Fatalf("expected exit 0, got: %d", code)
177	}
178	out = ui.OutputWriter.String()
179	if !strings.Contains(out, "job1_sfx") || strings.Contains(out, "job2_sfx") {
180		t.Fatalf("expected only job1_sfx, got: %s", out)
181	}
182
183	if !strings.Contains(out, "Created") {
184		t.Fatal("should have created header")
185	}
186
187	if !strings.Contains(out, "Modified") {
188		t.Fatal("should have modified header")
189	}
190	ui.OutputWriter.Reset()
191
192	// Query in short view mode
193	if code := cmd.Run([]string{"-address=" + url, "-short", "job2"}); code != 0 {
194		t.Fatalf("expected exit 0, got: %d", code)
195	}
196	out = ui.OutputWriter.String()
197	if !strings.Contains(out, "job2") {
198		t.Fatalf("expected job2, got: %s", out)
199	}
200	if strings.Contains(out, "Evaluations") {
201		t.Fatalf("should not dump evaluations")
202	}
203	if strings.Contains(out, "Allocations") {
204		t.Fatalf("should not dump allocations")
205	}
206	if strings.Contains(out, resp.EvalID) {
207		t.Fatalf("should not contain full identifiers, got %s", out)
208	}
209	ui.OutputWriter.Reset()
210
211	// Request full identifiers
212	if code := cmd.Run([]string{"-address=" + url, "-verbose", "job1"}); code != 0 {
213		t.Fatalf("expected exit 0, got: %d", code)
214	}
215	out = ui.OutputWriter.String()
216	if !strings.Contains(out, resp.EvalID) {
217		t.Fatalf("should contain full identifiers, got %s", out)
218	}
219}
220
221func TestJobStatusCommand_Fails(t *testing.T) {
222	t.Parallel()
223	ui := new(cli.MockUi)
224	cmd := &JobStatusCommand{Meta: Meta{Ui: ui}}
225
226	// Fails on misuse
227	if code := cmd.Run([]string{"some", "bad", "args"}); code != 1 {
228		t.Fatalf("expected exit code 1, got: %d", code)
229	}
230	if out := ui.ErrorWriter.String(); !strings.Contains(out, commandErrorText(cmd)) {
231		t.Fatalf("expected help output, got: %s", out)
232	}
233	ui.ErrorWriter.Reset()
234
235	// Fails on connection failure
236	if code := cmd.Run([]string{"-address=nope"}); code != 1 {
237		t.Fatalf("expected exit code 1, got: %d", code)
238	}
239	if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error querying jobs") {
240		t.Fatalf("expected failed query error, got: %s", out)
241	}
242}
243
244func TestJobStatusCommand_AutocompleteArgs(t *testing.T) {
245	assert := assert.New(t)
246	t.Parallel()
247
248	srv, _, url := testServer(t, true, nil)
249	defer srv.Shutdown()
250
251	ui := new(cli.MockUi)
252	cmd := &JobStatusCommand{Meta: Meta{Ui: ui, flagAddress: url}}
253
254	// Create a fake job
255	state := srv.Agent.Server().State()
256	j := mock.Job()
257	assert.Nil(state.UpsertJob(1000, j))
258
259	prefix := j.ID[:len(j.ID)-5]
260	args := complete.Args{Last: prefix}
261	predictor := cmd.AutocompleteArgs()
262
263	res := predictor.Predict(args)
264	assert.Equal(1, len(res))
265	assert.Equal(j.ID, res[0])
266}
267
268func TestJobStatusCommand_WithAccessPolicy(t *testing.T) {
269	assert := assert.New(t)
270	t.Parallel()
271
272	config := func(c *agent.Config) {
273		c.ACL.Enabled = true
274	}
275
276	srv, client, url := testServer(t, true, config)
277	defer srv.Shutdown()
278
279	// Bootstrap an initial ACL token
280	token := srv.RootToken
281	assert.NotNil(token, "failed to bootstrap ACL token")
282
283	// Wait for client ready
284	client.SetSecretID(token.SecretID)
285	testutil.WaitForResult(func() (bool, error) {
286		nodes, _, err := client.Nodes().List(nil)
287		if err != nil {
288			return false, err
289		}
290		if len(nodes) == 0 {
291			return false, fmt.Errorf("missing node")
292		}
293		if _, ok := nodes[0].Drivers["mock_driver"]; !ok {
294			return false, fmt.Errorf("mock_driver not ready")
295		}
296		return true, nil
297	}, func(err error) {
298		t.Fatalf("err: %s", err)
299	})
300
301	// Register a job
302	j := testJob("job1_sfx")
303
304	invalidToken := mock.ACLToken()
305
306	ui := new(cli.MockUi)
307	cmd := &JobStatusCommand{Meta: Meta{Ui: ui, flagAddress: url}}
308
309	// registering a job without a token fails
310	client.SetSecretID(invalidToken.SecretID)
311	resp, _, err := client.Jobs().Register(j, nil)
312	assert.NotNil(err)
313
314	// registering a job with a valid token succeeds
315	client.SetSecretID(token.SecretID)
316	resp, _, err = client.Jobs().Register(j, nil)
317	assert.Nil(err)
318	code := waitForSuccess(ui, client, fullId, t, resp.EvalID)
319	assert.Equal(0, code)
320
321	// Request Job List without providing a valid token
322	code = cmd.Run([]string{"-address=" + url, "-token=" + invalidToken.SecretID, "-short"})
323	assert.Equal(1, code)
324
325	// Request Job List with a valid token
326	code = cmd.Run([]string{"-address=" + url, "-token=" + token.SecretID, "-short"})
327	assert.Equal(0, code)
328
329	out := ui.OutputWriter.String()
330	if !strings.Contains(out, *j.ID) {
331		t.Fatalf("should contain full identifiers, got %s", out)
332	}
333}
334
335func TestJobStatusCommand_RescheduleEvals(t *testing.T) {
336	t.Parallel()
337	srv, client, url := testServer(t, true, nil)
338	defer srv.Shutdown()
339
340	// Wait for a node to be ready
341	testutil.WaitForResult(func() (bool, error) {
342		nodes, _, err := client.Nodes().List(nil)
343		if err != nil {
344			return false, err
345		}
346		for _, node := range nodes {
347			if node.Status == structs.NodeStatusReady {
348				return true, nil
349			}
350		}
351		return false, fmt.Errorf("no ready nodes")
352	}, func(err error) {
353		t.Fatalf("err: %v", err)
354	})
355
356	ui := new(cli.MockUi)
357	cmd := &JobStatusCommand{Meta: Meta{Ui: ui, flagAddress: url}}
358
359	require := require.New(t)
360	state := srv.Agent.Server().State()
361
362	// Create state store objects for job, alloc and followup eval with a future WaitUntil value
363	j := mock.Job()
364	require.Nil(state.UpsertJob(900, j))
365
366	e := mock.Eval()
367	e.WaitUntil = time.Now().Add(1 * time.Hour)
368	require.Nil(state.UpsertEvals(902, []*structs.Evaluation{e}))
369	a := mock.Alloc()
370	a.Job = j
371	a.JobID = j.ID
372	a.TaskGroup = j.TaskGroups[0].Name
373	a.FollowupEvalID = e.ID
374	a.Metrics = &structs.AllocMetric{}
375	a.DesiredStatus = structs.AllocDesiredStatusRun
376	a.ClientStatus = structs.AllocClientStatusRunning
377	require.Nil(state.UpsertAllocs(1000, []*structs.Allocation{a}))
378
379	// Query jobs with prefix match
380	if code := cmd.Run([]string{"-address=" + url, j.ID}); code != 0 {
381		t.Fatalf("expected exit 0, got: %d", code)
382	}
383	out := ui.OutputWriter.String()
384	require.Contains(out, "Future Rescheduling Attempts")
385	require.Contains(out, e.ID[:8])
386}
387
388func waitForSuccess(ui cli.Ui, client *api.Client, length int, t *testing.T, evalId string) int {
389	mon := newMonitor(ui, client, length)
390	monErr := mon.monitor(evalId, false)
391	return monErr
392}
393