1package command
2
3import (
4	"fmt"
5	"regexp"
6	"strings"
7	"testing"
8	"time"
9
10	"github.com/hashicorp/nomad/helper/uuid"
11	"github.com/hashicorp/nomad/nomad/mock"
12	"github.com/hashicorp/nomad/nomad/structs"
13	"github.com/hashicorp/nomad/testutil"
14	"github.com/mitchellh/cli"
15	"github.com/posener/complete"
16	"github.com/stretchr/testify/assert"
17	"github.com/stretchr/testify/require"
18)
19
20func TestAllocStatusCommand_Implements(t *testing.T) {
21	t.Parallel()
22	var _ cli.Command = &AllocStatusCommand{}
23}
24
25func TestAllocStatusCommand_Fails(t *testing.T) {
26	t.Parallel()
27	srv, _, url := testServer(t, false, nil)
28	defer srv.Shutdown()
29
30	ui := new(cli.MockUi)
31	cmd := &AllocStatusCommand{Meta: Meta{Ui: ui}}
32
33	// Fails on misuse
34	if code := cmd.Run([]string{"some", "bad", "args"}); code != 1 {
35		t.Fatalf("expected exit code 1, got: %d", code)
36	}
37	if out := ui.ErrorWriter.String(); !strings.Contains(out, commandErrorText(cmd)) {
38		t.Fatalf("expected help output, got: %s", out)
39	}
40	ui.ErrorWriter.Reset()
41
42	// Fails on connection failure
43	if code := cmd.Run([]string{"-address=nope", "foobar"}); code != 1 {
44		t.Fatalf("expected exit code 1, got: %d", code)
45	}
46	if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error querying allocation") {
47		t.Fatalf("expected failed query error, got: %s", out)
48	}
49	ui.ErrorWriter.Reset()
50
51	// Fails on missing alloc
52	if code := cmd.Run([]string{"-address=" + url, "26470238-5CF2-438F-8772-DC67CFB0705C"}); code != 1 {
53		t.Fatalf("expected exit 1, got: %d", code)
54	}
55	if out := ui.ErrorWriter.String(); !strings.Contains(out, "No allocation(s) with prefix or id") {
56		t.Fatalf("expected not found error, got: %s", out)
57	}
58	ui.ErrorWriter.Reset()
59
60	// Fail on identifier with too few characters
61	if code := cmd.Run([]string{"-address=" + url, "2"}); code != 1 {
62		t.Fatalf("expected exit 1, got: %d", code)
63	}
64	if out := ui.ErrorWriter.String(); !strings.Contains(out, "must contain at least two characters.") {
65		t.Fatalf("expected too few characters error, got: %s", out)
66	}
67	ui.ErrorWriter.Reset()
68
69	// Identifiers with uneven length should produce a query result
70	if code := cmd.Run([]string{"-address=" + url, "123"}); code != 1 {
71		t.Fatalf("expected exit 1, got: %d", code)
72	}
73	if out := ui.ErrorWriter.String(); !strings.Contains(out, "No allocation(s) with prefix or id") {
74		t.Fatalf("expected not found error, got: %s", out)
75	}
76	ui.ErrorWriter.Reset()
77
78	// Failed on both -json and -t options are specified
79	if code := cmd.Run([]string{"-address=" + url, "-json", "-t", "{{.ID}}"}); code != 1 {
80		t.Fatalf("expected exit 1, got: %d", code)
81	}
82	if out := ui.ErrorWriter.String(); !strings.Contains(out, "Both json and template formatting are not allowed") {
83		t.Fatalf("expected getting formatter error, got: %s", out)
84	}
85}
86
87func TestAllocStatusCommand_Run(t *testing.T) {
88	t.Parallel()
89	srv, client, url := testServer(t, true, nil)
90	defer srv.Shutdown()
91
92	// Wait for a node to be ready
93	testutil.WaitForResult(func() (bool, error) {
94		nodes, _, err := client.Nodes().List(nil)
95		if err != nil {
96			return false, err
97		}
98		for _, node := range nodes {
99			if _, ok := node.Drivers["mock_driver"]; ok &&
100				node.Status == structs.NodeStatusReady {
101				return true, nil
102			}
103		}
104		return false, fmt.Errorf("no ready nodes")
105	}, func(err error) {
106		t.Fatalf("err: %v", err)
107	})
108
109	ui := new(cli.MockUi)
110	cmd := &AllocStatusCommand{Meta: Meta{Ui: ui}}
111
112	jobID := "job1_sfx"
113	job1 := testJob(jobID)
114	resp, _, err := client.Jobs().Register(job1, nil)
115	if err != nil {
116		t.Fatalf("err: %s", err)
117	}
118	if code := waitForSuccess(ui, client, fullId, t, resp.EvalID); code != 0 {
119		t.Fatalf("status code non zero saw %d", code)
120	}
121	// get an alloc id
122	allocId1 := ""
123	nodeName := ""
124	if allocs, _, err := client.Jobs().Allocations(jobID, false, nil); err == nil {
125		if len(allocs) > 0 {
126			allocId1 = allocs[0].ID
127			nodeName = allocs[0].NodeName
128		}
129	}
130	if allocId1 == "" {
131		t.Fatal("unable to find an allocation")
132	}
133
134	if code := cmd.Run([]string{"-address=" + url, allocId1}); code != 0 {
135		t.Fatalf("expected exit 0, got: %d", code)
136	}
137	out := ui.OutputWriter.String()
138	if !strings.Contains(out, "Created") {
139		t.Fatalf("expected to have 'Created' but saw: %s", out)
140	}
141
142	if !strings.Contains(out, "Modified") {
143		t.Fatalf("expected to have 'Modified' but saw: %s", out)
144	}
145
146	nodeNameRegexpStr := fmt.Sprintf(`\nNode Name\s+= %s\n`, regexp.QuoteMeta(nodeName))
147	require.Regexp(t, regexp.MustCompile(nodeNameRegexpStr), out)
148
149	ui.OutputWriter.Reset()
150
151	if code := cmd.Run([]string{"-address=" + url, "-verbose", allocId1}); code != 0 {
152		t.Fatalf("expected exit 0, got: %d", code)
153	}
154	out = ui.OutputWriter.String()
155	if !strings.Contains(out, allocId1) {
156		t.Fatal("expected to find alloc id in output")
157	}
158	if !strings.Contains(out, "Created") {
159		t.Fatalf("expected to have 'Created' but saw: %s", out)
160	}
161	ui.OutputWriter.Reset()
162
163	// Try the query with an even prefix that includes the hyphen
164	if code := cmd.Run([]string{"-address=" + url, allocId1[:13]}); code != 0 {
165		t.Fatalf("expected exit 0, got: %d", code)
166	}
167	out = ui.OutputWriter.String()
168	if !strings.Contains(out, "Created") {
169		t.Fatalf("expected to have 'Created' but saw: %s", out)
170	}
171	ui.OutputWriter.Reset()
172
173	if code := cmd.Run([]string{"-address=" + url, "-verbose", allocId1}); code != 0 {
174		t.Fatalf("expected exit 0, got: %d", code)
175	}
176	out = ui.OutputWriter.String()
177	if !strings.Contains(out, allocId1) {
178		t.Fatal("expected to find alloc id in output")
179	}
180	ui.OutputWriter.Reset()
181
182}
183
184func TestAllocStatusCommand_RescheduleInfo(t *testing.T) {
185	t.Parallel()
186	srv, client, url := testServer(t, true, nil)
187	defer srv.Shutdown()
188
189	// Wait for a node to be ready
190	testutil.WaitForResult(func() (bool, error) {
191		nodes, _, err := client.Nodes().List(nil)
192		if err != nil {
193			return false, err
194		}
195		for _, node := range nodes {
196			if node.Status == structs.NodeStatusReady {
197				return true, nil
198			}
199		}
200		return false, fmt.Errorf("no ready nodes")
201	}, func(err error) {
202		t.Fatalf("err: %v", err)
203	})
204
205	ui := new(cli.MockUi)
206	cmd := &AllocStatusCommand{Meta: Meta{Ui: ui}}
207	// Test reschedule attempt info
208	require := require.New(t)
209	state := srv.Agent.Server().State()
210	a := mock.Alloc()
211	a.Metrics = &structs.AllocMetric{}
212	nextAllocId := uuid.Generate()
213	a.NextAllocation = nextAllocId
214	a.RescheduleTracker = &structs.RescheduleTracker{
215		Events: []*structs.RescheduleEvent{
216			{
217				RescheduleTime: time.Now().Add(-2 * time.Minute).UTC().UnixNano(),
218				PrevAllocID:    uuid.Generate(),
219				PrevNodeID:     uuid.Generate(),
220			},
221		},
222	}
223	require.Nil(state.UpsertAllocs(1000, []*structs.Allocation{a}))
224
225	if code := cmd.Run([]string{"-address=" + url, a.ID}); code != 0 {
226		t.Fatalf("expected exit 0, got: %d", code)
227	}
228	out := ui.OutputWriter.String()
229	require.Contains(out, "Replacement Alloc ID")
230	require.Regexp(regexp.MustCompile(".*Reschedule Attempts\\s*=\\s*1/2"), out)
231}
232
233func TestAllocStatusCommand_ScoreMetrics(t *testing.T) {
234	t.Parallel()
235	srv, client, url := testServer(t, true, nil)
236	defer srv.Shutdown()
237
238	// Wait for a node to be ready
239	testutil.WaitForResult(func() (bool, error) {
240		nodes, _, err := client.Nodes().List(nil)
241		if err != nil {
242			return false, err
243		}
244		for _, node := range nodes {
245			if node.Status == structs.NodeStatusReady {
246				return true, nil
247			}
248		}
249		return false, fmt.Errorf("no ready nodes")
250	}, func(err error) {
251		t.Fatalf("err: %v", err)
252	})
253
254	ui := new(cli.MockUi)
255	cmd := &AllocStatusCommand{Meta: Meta{Ui: ui}}
256	// Test node metrics
257	require := require.New(t)
258	state := srv.Agent.Server().State()
259	a := mock.Alloc()
260	mockNode1 := mock.Node()
261	mockNode2 := mock.Node()
262	a.Metrics = &structs.AllocMetric{
263		ScoreMetaData: []*structs.NodeScoreMeta{
264			{
265				NodeID: mockNode1.ID,
266				Scores: map[string]float64{
267					"binpack":       0.77,
268					"node-affinity": 0.5,
269				},
270			},
271			{
272				NodeID: mockNode2.ID,
273				Scores: map[string]float64{
274					"binpack":       0.75,
275					"node-affinity": 0.33,
276				},
277			},
278		},
279	}
280	require.Nil(state.UpsertAllocs(1000, []*structs.Allocation{a}))
281
282	if code := cmd.Run([]string{"-address=" + url, "-verbose", a.ID}); code != 0 {
283		t.Fatalf("expected exit 0, got: %d", code)
284	}
285	out := ui.OutputWriter.String()
286	require.Contains(out, "Placement Metrics")
287	require.Contains(out, mockNode1.ID)
288	require.Contains(out, mockNode2.ID)
289
290	// assert we sort headers alphabetically
291	require.Contains(out, "binpack  node-affinity")
292	require.Contains(out, "final score")
293}
294
295func TestAllocStatusCommand_AutocompleteArgs(t *testing.T) {
296	assert := assert.New(t)
297	t.Parallel()
298
299	srv, _, url := testServer(t, true, nil)
300	defer srv.Shutdown()
301
302	ui := new(cli.MockUi)
303	cmd := &AllocStatusCommand{Meta: Meta{Ui: ui, flagAddress: url}}
304
305	// Create a fake alloc
306	state := srv.Agent.Server().State()
307	a := mock.Alloc()
308	assert.Nil(state.UpsertAllocs(1000, []*structs.Allocation{a}))
309
310	prefix := a.ID[:5]
311	args := complete.Args{Last: prefix}
312	predictor := cmd.AutocompleteArgs()
313
314	res := predictor.Predict(args)
315	assert.Equal(1, len(res))
316	assert.Equal(a.ID, res[0])
317}
318