github.com/hernad/nomad@v1.6.112/command/job_run_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package command 5 6 import ( 7 "io" 8 "net/http" 9 "os" 10 "path/filepath" 11 "strings" 12 "testing" 13 "time" 14 15 "github.com/hernad/nomad/ci" 16 "github.com/hernad/nomad/testutil" 17 "github.com/mitchellh/cli" 18 "github.com/stretchr/testify/require" 19 ) 20 21 var _ cli.Command = (*JobRunCommand)(nil) 22 23 func TestRunCommand_Output_Json(t *testing.T) { 24 ci.Parallel(t) 25 ui := cli.NewMockUi() 26 cmd := &JobRunCommand{Meta: Meta{Ui: ui}} 27 28 fh, err := os.CreateTemp("", "nomad") 29 if err != nil { 30 t.Fatalf("err: %s", err) 31 } 32 defer os.Remove(fh.Name()) 33 _, err = fh.WriteString(` 34 job "job1" { 35 type = "service" 36 datacenters = [ "dc1" ] 37 group "group1" { 38 count = 1 39 task "task1" { 40 driver = "exec" 41 resources { 42 cpu = 1000 43 memory = 512 44 } 45 } 46 } 47 }`) 48 if err != nil { 49 t.Fatalf("err: %s", err) 50 } 51 if code := cmd.Run([]string{"-output", fh.Name()}); code != 0 { 52 t.Fatalf("expected exit code 0, got: %d", code) 53 } 54 if out := ui.OutputWriter.String(); !strings.Contains(out, `"Type": "service",`) { 55 t.Fatalf("Expected JSON output: %v", out) 56 } 57 } 58 59 func TestRunCommand_hcl1_hcl2_strict(t *testing.T) { 60 ci.Parallel(t) 61 62 _, _, addr := testServer(t, false, nil) 63 64 t.Run("-hcl1 implies -hcl2-strict is false", func(t *testing.T) { 65 ui := cli.NewMockUi() 66 cmd := &JobRunCommand{Meta: Meta{Ui: ui}} 67 got := cmd.Run([]string{ 68 "-hcl1", "-hcl2-strict", 69 "-address", addr, 70 "-detach", 71 "asset/example-short.nomad.hcl", 72 }) 73 require.Equal(t, 0, got, ui.ErrorWriter.String()) 74 }) 75 } 76 77 func TestRunCommand_Fails(t *testing.T) { 78 ci.Parallel(t) 79 80 // Create a server 81 s := testutil.NewTestServer(t, nil) 82 defer s.Stop() 83 84 ui := cli.NewMockUi() 85 cmd := &JobRunCommand{Meta: Meta{Ui: ui, flagAddress: "http://" + s.HTTPAddr}} 86 87 // Fails on misuse 88 if code := cmd.Run([]string{"some", "bad", "args"}); code != 1 { 89 t.Fatalf("expected exit code 1, got: %d", code) 90 } 91 if out := ui.ErrorWriter.String(); !strings.Contains(out, commandErrorText(cmd)) { 92 t.Fatalf("expected help output, got: %s", out) 93 } 94 ui.ErrorWriter.Reset() 95 96 // Fails when specified file does not exist 97 if code := cmd.Run([]string{"/unicorns/leprechauns"}); code != 1 { 98 t.Fatalf("expect exit 1, got: %d", code) 99 } 100 if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting job struct") { 101 t.Fatalf("expect getting job struct error, got: %s", out) 102 } 103 ui.ErrorWriter.Reset() 104 105 // Fails on invalid HCL 106 fh1, err := os.CreateTemp("", "nomad") 107 if err != nil { 108 t.Fatalf("err: %s", err) 109 } 110 defer os.Remove(fh1.Name()) 111 if _, err := fh1.WriteString("nope"); err != nil { 112 t.Fatalf("err: %s", err) 113 } 114 if code := cmd.Run([]string{fh1.Name()}); code != 1 { 115 t.Fatalf("expect exit 1, got: %d", code) 116 } 117 if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting job struct") { 118 t.Fatalf("expect parsing error, got: %s", out) 119 } 120 ui.ErrorWriter.Reset() 121 122 // Fails on invalid job spec 123 fh2, err := os.CreateTemp("", "nomad") 124 if err != nil { 125 t.Fatalf("err: %s", err) 126 } 127 defer os.Remove(fh2.Name()) 128 if _, err := fh2.WriteString(`job "job1" {}`); err != nil { 129 t.Fatalf("err: %s", err) 130 } 131 if code := cmd.Run([]string{fh2.Name()}); code != 1 { 132 t.Fatalf("expect exit 1, got: %d", code) 133 } 134 if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error submitting job") { 135 t.Fatalf("expect validation error, got: %s", out) 136 } 137 ui.ErrorWriter.Reset() 138 139 // Fails on connection failure (requires a valid job) 140 fh3, err := os.CreateTemp("", "nomad") 141 if err != nil { 142 t.Fatalf("err: %s", err) 143 } 144 defer os.Remove(fh3.Name()) 145 _, err = fh3.WriteString(` 146 job "job1" { 147 type = "service" 148 datacenters = [ "dc1" ] 149 group "group1" { 150 count = 1 151 task "task1" { 152 driver = "exec" 153 resources { 154 cpu = 1000 155 memory = 512 156 } 157 } 158 } 159 }`) 160 if err != nil { 161 t.Fatalf("err: %s", err) 162 } 163 if code := cmd.Run([]string{"-address=nope", fh3.Name()}); code != 1 { 164 t.Fatalf("expected exit code 1, got: %d", code) 165 } 166 if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error submitting job") { 167 t.Fatalf("expected failed query error, got: %s", out) 168 } 169 170 // Fails on invalid check-index (requires a valid job) 171 if code := cmd.Run([]string{"-check-index=bad", fh3.Name()}); code != 1 { 172 t.Fatalf("expected exit code 1, got: %d", code) 173 } 174 if out := ui.ErrorWriter.String(); !strings.Contains(out, "parsing check-index") { 175 t.Fatalf("expected parse error, got: %s", out) 176 } 177 ui.ErrorWriter.Reset() 178 179 } 180 181 func TestRunCommand_From_STDIN(t *testing.T) { 182 ci.Parallel(t) 183 stdinR, stdinW, err := os.Pipe() 184 if err != nil { 185 t.Fatalf("err: %s", err) 186 } 187 188 ui := cli.NewMockUi() 189 cmd := &JobRunCommand{ 190 Meta: Meta{Ui: ui}, 191 JobGetter: JobGetter{testStdin: stdinR}, 192 } 193 194 go func() { 195 stdinW.WriteString(` 196 job "job1" { 197 type = "service" 198 datacenters = [ "dc1" ] 199 group "group1" { 200 count = 1 201 task "task1" { 202 driver = "exec" 203 resources { 204 cpu = 1000 205 memory = 512 206 } 207 } 208 } 209 }`) 210 stdinW.Close() 211 }() 212 213 args := []string{"-address=nope", "-"} 214 if code := cmd.Run(args); code != 1 { 215 t.Fatalf("expected exit code 1, got %d: %q", code, ui.ErrorWriter.String()) 216 } 217 218 if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error submitting job") { 219 t.Fatalf("expected submission error, got: %s", out) 220 } 221 ui.ErrorWriter.Reset() 222 } 223 224 func TestRunCommand_From_URL(t *testing.T) { 225 ci.Parallel(t) 226 ui := cli.NewMockUi() 227 cmd := &JobRunCommand{ 228 Meta: Meta{Ui: ui}, 229 } 230 231 args := []string{"https://example.com/foo/bar"} 232 if code := cmd.Run(args); code != 1 { 233 t.Fatalf("expected exit code 1, got %d: %q", code, ui.ErrorWriter.String()) 234 } 235 236 if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting jobfile") { 237 t.Fatalf("expected error getting jobfile, got: %s", out) 238 } 239 } 240 241 // TestRunCommand_JSON asserts that `nomad job run -json` accepts JSON jobs 242 // with or without a top level Job key. 243 func TestRunCommand_JSON(t *testing.T) { 244 ci.Parallel(t) 245 run := func(args ...string) (stdout string, stderr string, code int) { 246 ui := cli.NewMockUi() 247 cmd := &JobRunCommand{ 248 Meta: Meta{Ui: ui}, 249 } 250 t.Logf("run: nomad job run %s", strings.Join(args, " ")) 251 code = cmd.Run(args) 252 return ui.OutputWriter.String(), ui.ErrorWriter.String(), code 253 } 254 255 // Agent startup is slow, do some work while we wait 256 agentReady := make(chan string) 257 go func() { 258 _, _, addr := testServer(t, false, nil) 259 agentReady <- addr 260 }() 261 262 // First convert HCL -> JSON with -output 263 stdout, stderr, code := run("-output", "asset/example-short.nomad.hcl") 264 require.Zero(t, code, stderr) 265 require.Empty(t, stderr) 266 require.NotEmpty(t, stdout) 267 t.Logf("run -output==> %s...", stdout[:12]) 268 269 jsonFile := filepath.Join(t.TempDir(), "redis.json") 270 require.NoError(t, os.WriteFile(jsonFile, []byte(stdout), 0o640)) 271 272 // Wait for agent to start and get its address 273 addr := "" 274 select { 275 case addr = <-agentReady: 276 case <-time.After(10 * time.Second): 277 t.Fatalf("timed out waiting for agent to start") 278 } 279 280 // Submit JSON 281 stdout, stderr, code = run("-detach", "-address", addr, "-json", jsonFile) 282 require.Zero(t, code, stderr) 283 require.Empty(t, stderr) 284 285 // Read the JSON from the API as it omits the Job envelope and 286 // therefore differs from -output 287 resp, err := http.Get(addr + "/v1/job/example") 288 require.NoError(t, err) 289 buf, err := io.ReadAll(resp.Body) 290 require.NoError(t, err) 291 require.NoError(t, resp.Body.Close()) 292 require.NotEmpty(t, buf) 293 t.Logf("/v1/job/example==> %s...", string(buf[:12])) 294 require.NoError(t, os.WriteFile(jsonFile, buf, 0o640)) 295 296 // Submit JSON 297 stdout, stderr, code = run("-detach", "-address", addr, "-json", jsonFile) 298 require.Zerof(t, code, "stderr: %s\njson: %s\n", stderr, string(buf)) 299 require.Empty(t, stderr) 300 require.NotEmpty(t, stdout) 301 }