1package command
2
3import (
4	"fmt"
5	"io/ioutil"
6	"os"
7	"strconv"
8	"strings"
9	"testing"
10
11	"github.com/hashicorp/nomad/api"
12	"github.com/hashicorp/nomad/testutil"
13	"github.com/mitchellh/cli"
14	"github.com/stretchr/testify/require"
15)
16
17func TestPlanCommand_Implements(t *testing.T) {
18	t.Parallel()
19	var _ cli.Command = &JobRunCommand{}
20}
21
22func TestPlanCommand_Fails(t *testing.T) {
23	t.Parallel()
24	ui := new(cli.MockUi)
25	cmd := &JobPlanCommand{Meta: Meta{Ui: ui}}
26
27	// Create a server
28	s := testutil.NewTestServer(t, nil)
29	defer s.Stop()
30	os.Setenv("NOMAD_ADDR", fmt.Sprintf("http://%s", s.HTTPAddr))
31
32	// Fails on misuse
33	if code := cmd.Run([]string{"some", "bad", "args"}); code != 255 {
34		t.Fatalf("expected exit code 1, got: %d", code)
35	}
36	if out := ui.ErrorWriter.String(); !strings.Contains(out, commandErrorText(cmd)) {
37		t.Fatalf("expected help output, got: %s", out)
38	}
39	ui.ErrorWriter.Reset()
40
41	// Fails when specified file does not exist
42	if code := cmd.Run([]string{"/unicorns/leprechauns"}); code != 255 {
43		t.Fatalf("expect exit 255, got: %d", code)
44	}
45	if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting job struct") {
46		t.Fatalf("expect getting job struct error, got: %s", out)
47	}
48	ui.ErrorWriter.Reset()
49
50	// Fails on invalid HCL
51	fh1, err := ioutil.TempFile("", "nomad")
52	if err != nil {
53		t.Fatalf("err: %s", err)
54	}
55	defer os.Remove(fh1.Name())
56	if _, err := fh1.WriteString("nope"); err != nil {
57		t.Fatalf("err: %s", err)
58	}
59	if code := cmd.Run([]string{fh1.Name()}); code != 255 {
60		t.Fatalf("expect exit 255, got: %d", code)
61	}
62	if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting job struct") {
63		t.Fatalf("expect parsing error, got: %s", out)
64	}
65	ui.ErrorWriter.Reset()
66
67	// Fails on invalid job spec
68	fh2, err := ioutil.TempFile("", "nomad")
69	if err != nil {
70		t.Fatalf("err: %s", err)
71	}
72	defer os.Remove(fh2.Name())
73	if _, err := fh2.WriteString(`job "job1" {}`); err != nil {
74		t.Fatalf("err: %s", err)
75	}
76	if code := cmd.Run([]string{fh2.Name()}); code != 255 {
77		t.Fatalf("expect exit 255, got: %d", code)
78	}
79	if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error during plan") {
80		t.Fatalf("expect validation error, got: %s", out)
81	}
82	ui.ErrorWriter.Reset()
83
84	// Fails on connection failure (requires a valid job)
85	fh3, err := ioutil.TempFile("", "nomad")
86	if err != nil {
87		t.Fatalf("err: %s", err)
88	}
89	defer os.Remove(fh3.Name())
90	_, err = fh3.WriteString(`
91job "job1" {
92	type = "service"
93	datacenters = [ "dc1" ]
94	group "group1" {
95		count = 1
96		task "task1" {
97			driver = "exec"
98			resources = {
99				cpu = 1000
100				memory = 512
101			}
102		}
103	}
104}`)
105	if err != nil {
106		t.Fatalf("err: %s", err)
107	}
108	if code := cmd.Run([]string{"-address=nope", fh3.Name()}); code != 255 {
109		t.Fatalf("expected exit code 255, got: %d", code)
110	}
111	if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error during plan") {
112		t.Fatalf("expected failed query error, got: %s", out)
113	}
114}
115
116func TestPlanCommand_From_STDIN(t *testing.T) {
117	t.Parallel()
118	stdinR, stdinW, err := os.Pipe()
119	if err != nil {
120		t.Fatalf("err: %s", err)
121	}
122
123	ui := new(cli.MockUi)
124	cmd := &JobPlanCommand{
125		Meta:      Meta{Ui: ui},
126		JobGetter: JobGetter{testStdin: stdinR},
127	}
128
129	go func() {
130		stdinW.WriteString(`
131job "job1" {
132  type = "service"
133  datacenters = [ "dc1" ]
134  group "group1" {
135                count = 1
136                task "task1" {
137                        driver = "exec"
138                        resources = {
139                                cpu = 1000
140                                memory = 512
141                        }
142                }
143        }
144}`)
145		stdinW.Close()
146	}()
147
148	args := []string{"-"}
149	if code := cmd.Run(args); code != 255 {
150		t.Fatalf("expected exit code 255, got %d: %q", code, ui.ErrorWriter.String())
151	}
152
153	if out := ui.ErrorWriter.String(); !strings.Contains(out, "connection refused") {
154		t.Fatalf("expected connection refused error, got: %s", out)
155	}
156	ui.ErrorWriter.Reset()
157}
158
159func TestPlanCommand_From_URL(t *testing.T) {
160	t.Parallel()
161	ui := new(cli.MockUi)
162	cmd := &JobPlanCommand{
163		Meta: Meta{Ui: ui},
164	}
165
166	args := []string{"https://example.com/foo/bar"}
167	if code := cmd.Run(args); code != 255 {
168		t.Fatalf("expected exit code 255, got %d: %q", code, ui.ErrorWriter.String())
169	}
170
171	if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting jobfile") {
172		t.Fatalf("expected error getting jobfile, got: %s", out)
173	}
174}
175
176func TestPlanCommad_Preemptions(t *testing.T) {
177	t.Parallel()
178	ui := new(cli.MockUi)
179	cmd := &JobPlanCommand{Meta: Meta{Ui: ui}}
180	require := require.New(t)
181
182	// Only one preempted alloc
183	resp1 := &api.JobPlanResponse{
184		Annotations: &api.PlanAnnotations{
185			PreemptedAllocs: []*api.AllocationListStub{
186				{
187					ID:        "alloc1",
188					JobID:     "jobID1",
189					TaskGroup: "meta",
190					JobType:   "batch",
191					Namespace: "test",
192				},
193			},
194		},
195	}
196	cmd.addPreemptions(resp1)
197	out := ui.OutputWriter.String()
198	require.Contains(out, "Alloc ID")
199	require.Contains(out, "alloc1")
200
201	// Less than 10 unique job ids
202	var preemptedAllocs []*api.AllocationListStub
203	for i := 0; i < 12; i++ {
204		job_id := "job" + strconv.Itoa(i%4)
205		alloc := &api.AllocationListStub{
206			ID:        "alloc",
207			JobID:     job_id,
208			TaskGroup: "meta",
209			JobType:   "batch",
210			Namespace: "test",
211		}
212		preemptedAllocs = append(preemptedAllocs, alloc)
213	}
214
215	resp2 := &api.JobPlanResponse{
216		Annotations: &api.PlanAnnotations{
217			PreemptedAllocs: preemptedAllocs,
218		},
219	}
220	ui.OutputWriter.Reset()
221	cmd.addPreemptions(resp2)
222	out = ui.OutputWriter.String()
223	require.Contains(out, "Job ID")
224	require.Contains(out, "Namespace")
225
226	// More than 10 unique job IDs
227	preemptedAllocs = make([]*api.AllocationListStub, 0)
228	var job_type string
229	for i := 0; i < 20; i++ {
230		job_id := "job" + strconv.Itoa(i)
231		if i%2 == 0 {
232			job_type = "service"
233		} else {
234			job_type = "batch"
235		}
236		alloc := &api.AllocationListStub{
237			ID:        "alloc",
238			JobID:     job_id,
239			TaskGroup: "meta",
240			JobType:   job_type,
241			Namespace: "test",
242		}
243		preemptedAllocs = append(preemptedAllocs, alloc)
244	}
245
246	resp3 := &api.JobPlanResponse{
247		Annotations: &api.PlanAnnotations{
248			PreemptedAllocs: preemptedAllocs,
249		},
250	}
251	ui.OutputWriter.Reset()
252	cmd.addPreemptions(resp3)
253	out = ui.OutputWriter.String()
254	require.Contains(out, "Job Type")
255	require.Contains(out, "batch")
256	require.Contains(out, "service")
257}
258