1package command
2
3import (
4	"bytes"
5	"fmt"
6	"strings"
7	"testing"
8
9	"github.com/hashicorp/nomad/helper/uuid"
10	"github.com/hashicorp/nomad/nomad/mock"
11	"github.com/hashicorp/nomad/nomad/structs"
12	"github.com/hashicorp/nomad/testutil"
13	"github.com/mitchellh/cli"
14	"github.com/posener/complete"
15	"github.com/stretchr/testify/assert"
16	"github.com/stretchr/testify/require"
17)
18
19// static check
20var _ cli.Command = &AllocExecCommand{}
21
22func TestAllocExecCommand_Fails(t *testing.T) {
23	t.Parallel()
24	srv, client, url := testServer(t, true, nil)
25	defer srv.Shutdown()
26
27	cases := []struct {
28		name          string
29		args          []string
30		expectedError string
31	}{
32		{
33			"misuse",
34			[]string{"bad"},
35			commandErrorText(&AllocExecCommand{}),
36		},
37		{
38			"connection failure",
39			[]string{"-address=nope", "26470238-5CF2-438F-8772-DC67CFB0705C", "/bin/bash"},
40			"Error querying allocation",
41		},
42		{
43			"not found alloc",
44			[]string{"-address=" + url, "26470238-5CF2-438F-8772-DC67CFB0705C", "/bin/bash"},
45			"No allocation(s) with prefix or id",
46		},
47		{
48			"not found job",
49			[]string{"-address=" + url, "-job", "example", "/bin/bash"},
50			`job "example" doesn't exist`,
51		},
52		{
53			"too short allocis",
54			[]string{"-address=" + url, "2", "/bin/bash"},
55			"Alloc ID must contain at least two characters",
56		},
57		{
58			"missing command",
59			[]string{"-address=" + url, "26470238-5CF2-438F-8772-DC67CFB0705C"},
60			"A command is required",
61		},
62		{
63			"long escaped char",
64			[]string{"-address=" + url, "-e", "long_escape", "26470238-5CF2-438F-8772-DC67CFB0705C", "/bin/bash"},
65			"a single character",
66		},
67	}
68
69	for _, c := range cases {
70		t.Run(c.name, func(t *testing.T) {
71			ui := new(cli.MockUi)
72			cmd := &AllocExecCommand{Meta: Meta{Ui: ui}}
73
74			code := cmd.Run(c.args)
75			require.Equal(t, 1, code)
76
77			require.Contains(t, ui.ErrorWriter.String(), c.expectedError)
78
79			ui.ErrorWriter.Reset()
80			ui.OutputWriter.Reset()
81
82		})
83	}
84
85	// Wait for a node to be ready
86	testutil.WaitForResult(func() (bool, error) {
87		nodes, _, err := client.Nodes().List(nil)
88		if err != nil {
89			return false, err
90		}
91		for _, node := range nodes {
92			if _, ok := node.Drivers["mock_driver"]; ok &&
93				node.Status == structs.NodeStatusReady {
94				return true, nil
95			}
96		}
97		return false, fmt.Errorf("no ready nodes")
98	}, func(err error) {
99		require.NoError(t, err)
100	})
101
102	t.Run("non existent task", func(t *testing.T) {
103		ui := new(cli.MockUi)
104		cmd := &AllocExecCommand{Meta: Meta{Ui: ui}}
105
106		jobID := "job1_sfx"
107		job1 := testJob(jobID)
108		resp, _, err := client.Jobs().Register(job1, nil)
109		require.NoError(t, err)
110		code := waitForSuccess(ui, client, fullId, t, resp.EvalID)
111		require.Zero(t, code, "status code not zero")
112
113		// get an alloc id
114		allocId1 := ""
115		if allocs, _, err := client.Jobs().Allocations(jobID, false, nil); err == nil {
116			if len(allocs) > 0 {
117				allocId1 = allocs[0].ID
118			}
119		}
120		require.NotEmpty(t, allocId1, "unable to find allocation")
121
122		// by alloc
123		require.Equal(t, 1, cmd.Run([]string{"-address=" + url, "-task=nonexistenttask1", allocId1, "/bin/bash"}))
124		require.Contains(t, ui.ErrorWriter.String(), "Could not find task named: nonexistenttask1")
125		ui.ErrorWriter.Reset()
126
127		// by jobID
128		require.Equal(t, 1, cmd.Run([]string{"-address=" + url, "-task=nonexistenttask2", "-job", jobID, "/bin/bash"}))
129		require.Contains(t, ui.ErrorWriter.String(), "Could not find task named: nonexistenttask2")
130		ui.ErrorWriter.Reset()
131	})
132
133}
134
135func TestAllocExecCommand_AutocompleteArgs(t *testing.T) {
136	assert := assert.New(t)
137	t.Parallel()
138
139	srv, _, url := testServer(t, true, nil)
140	defer srv.Shutdown()
141
142	ui := new(cli.MockUi)
143	cmd := &AllocExecCommand{Meta: Meta{Ui: ui, flagAddress: url}}
144
145	// Create a fake alloc
146	state := srv.Agent.Server().State()
147	a := mock.Alloc()
148	assert.Nil(state.UpsertAllocs(1000, []*structs.Allocation{a}))
149
150	prefix := a.ID[:5]
151	args := complete.Args{Last: prefix}
152	predictor := cmd.AutocompleteArgs()
153
154	res := predictor.Predict(args)
155	assert.Equal(1, len(res))
156	assert.Equal(a.ID, res[0])
157}
158
159func TestAllocExecCommand_Run(t *testing.T) {
160	t.Parallel()
161	srv, client, url := testServer(t, true, nil)
162	defer srv.Shutdown()
163
164	// Wait for a node to be ready
165	testutil.WaitForResult(func() (bool, error) {
166		nodes, _, err := client.Nodes().List(nil)
167		if err != nil {
168			return false, err
169		}
170
171		for _, node := range nodes {
172			if _, ok := node.Drivers["mock_driver"]; ok &&
173				node.Status == structs.NodeStatusReady {
174				return true, nil
175			}
176		}
177		return false, fmt.Errorf("no ready nodes")
178	}, func(err error) {
179		require.NoError(t, err)
180	})
181
182	jobID := uuid.Generate()
183	job := testJob(jobID)
184	job.TaskGroups[0].Tasks[0].Config = map[string]interface{}{
185		"run_for": "10s",
186		"exec_command": map[string]interface{}{
187			"run_for":       "1ms",
188			"exit_code":     21,
189			"stdout_string": "sample stdout output\n",
190			"stderr_string": "sample stderr output\n",
191		},
192	}
193	resp, _, err := client.Jobs().Register(job, nil)
194	require.NoError(t, err)
195
196	evalUi := new(cli.MockUi)
197	code := waitForSuccess(evalUi, client, fullId, t, resp.EvalID)
198	require.Equal(t, 0, code, "failed to get status - output: %v", evalUi.ErrorWriter.String())
199
200	allocId := ""
201
202	testutil.WaitForResult(func() (bool, error) {
203		allocs, _, err := client.Jobs().Allocations(jobID, false, nil)
204		if err != nil {
205			return false, fmt.Errorf("failed to get allocations: %v", err)
206		}
207
208		if len(allocs) < 0 {
209			return false, fmt.Errorf("no allocations yet")
210		}
211
212		alloc := allocs[0]
213		if alloc.ClientStatus != "running" {
214			return false, fmt.Errorf("alloc is not running yet: %v", alloc.ClientStatus)
215		}
216
217		allocId = alloc.ID
218		return true, nil
219	}, func(err error) {
220		require.NoError(t, err)
221
222	})
223
224	cases := []struct {
225		name    string
226		command string
227		stdin   string
228
229		stdout   string
230		stderr   string
231		exitCode int
232	}{
233		{
234			name:     "basic stdout/err",
235			command:  "simplecommand",
236			stdin:    "",
237			stdout:   "sample stdout output",
238			stderr:   "sample stderr output",
239			exitCode: 21,
240		},
241		{
242			name:     "notty: streamining input",
243			command:  "showinput",
244			stdin:    "hello from stdin",
245			stdout:   "TTY: false\nStdin:\nhello from stdin",
246			exitCode: 0,
247		},
248	}
249
250	for _, c := range cases {
251		t.Run("by id: "+c.name, func(t *testing.T) {
252			ui := new(cli.MockUi)
253			var stdout, stderr bufferCloser
254
255			cmd := &AllocExecCommand{
256				Meta:   Meta{Ui: ui},
257				Stdin:  strings.NewReader(c.stdin),
258				Stdout: &stdout,
259				Stderr: &stderr,
260			}
261
262			code = cmd.Run([]string{"-address=" + url, allocId, c.command})
263			assert.Equal(t, c.exitCode, code)
264			assert.Equal(t, c.stdout, strings.TrimSpace(stdout.String()))
265			assert.Equal(t, c.stderr, strings.TrimSpace(stderr.String()))
266		})
267		t.Run("by job: "+c.name, func(t *testing.T) {
268			ui := new(cli.MockUi)
269			var stdout, stderr bufferCloser
270
271			cmd := &AllocExecCommand{
272				Meta:   Meta{Ui: ui},
273				Stdin:  strings.NewReader(c.stdin),
274				Stdout: &stdout,
275				Stderr: &stderr,
276			}
277
278			code = cmd.Run([]string{"-address=" + url, "-job", jobID, c.command})
279			assert.Equal(t, c.exitCode, code)
280			assert.Equal(t, c.stdout, strings.TrimSpace(stdout.String()))
281			assert.Equal(t, c.stderr, strings.TrimSpace(stderr.String()))
282		})
283	}
284}
285
286type bufferCloser struct {
287	bytes.Buffer
288}
289
290func (b *bufferCloser) Close() error {
291	return nil
292}
293