github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/command/job_plan_test.go (about) 1 package command 2 3 import ( 4 "io/ioutil" 5 "os" 6 "strconv" 7 "strings" 8 "testing" 9 10 "github.com/hashicorp/nomad/api" 11 "github.com/hashicorp/nomad/ci" 12 "github.com/hashicorp/nomad/testutil" 13 "github.com/mitchellh/cli" 14 "github.com/shoenig/test/must" 15 "github.com/stretchr/testify/require" 16 ) 17 18 func TestPlanCommand_Implements(t *testing.T) { 19 ci.Parallel(t) 20 var _ cli.Command = &JobRunCommand{} 21 } 22 23 func TestPlanCommand_Fails(t *testing.T) { 24 ci.Parallel(t) 25 26 // Create a server 27 s := testutil.NewTestServer(t, nil) 28 defer s.Stop() 29 30 ui := cli.NewMockUi() 31 cmd := &JobPlanCommand{Meta: Meta{Ui: ui, flagAddress: "http://" + s.HTTPAddr}} 32 33 // Fails on misuse 34 if code := cmd.Run([]string{"some", "bad", "args"}); code != 255 { 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 when specified file does not exist 43 if code := cmd.Run([]string{"/unicorns/leprechauns"}); code != 255 { 44 t.Fatalf("expect exit 255, got: %d", code) 45 } 46 if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting job struct") { 47 t.Fatalf("expect getting job struct error, got: %s", out) 48 } 49 ui.ErrorWriter.Reset() 50 51 // Fails on invalid HCL 52 fh1, err := ioutil.TempFile("", "nomad") 53 if err != nil { 54 t.Fatalf("err: %s", err) 55 } 56 defer os.Remove(fh1.Name()) 57 if _, err := fh1.WriteString("nope"); err != nil { 58 t.Fatalf("err: %s", err) 59 } 60 if code := cmd.Run([]string{fh1.Name()}); code != 255 { 61 t.Fatalf("expect exit 255, got: %d", code) 62 } 63 if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting job struct") { 64 t.Fatalf("expect parsing error, got: %s", out) 65 } 66 ui.ErrorWriter.Reset() 67 68 // Fails on invalid job spec 69 fh2, err := ioutil.TempFile("", "nomad") 70 if err != nil { 71 t.Fatalf("err: %s", err) 72 } 73 defer os.Remove(fh2.Name()) 74 if _, err := fh2.WriteString(`job "job1" {}`); err != nil { 75 t.Fatalf("err: %s", err) 76 } 77 if code := cmd.Run([]string{fh2.Name()}); code != 255 { 78 t.Fatalf("expect exit 255, got: %d", code) 79 } 80 if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error during plan") { 81 t.Fatalf("expect validation error, got: %s", out) 82 } 83 ui.ErrorWriter.Reset() 84 85 // Fails on connection failure (requires a valid job) 86 fh3, err := ioutil.TempFile("", "nomad") 87 if err != nil { 88 t.Fatalf("err: %s", err) 89 } 90 defer os.Remove(fh3.Name()) 91 _, err = fh3.WriteString(` 92 job "job1" { 93 type = "service" 94 datacenters = [ "dc1" ] 95 group "group1" { 96 count = 1 97 task "task1" { 98 driver = "exec" 99 resources { 100 cpu = 1000 101 memory = 512 102 } 103 } 104 } 105 }`) 106 if err != nil { 107 t.Fatalf("err: %s", err) 108 } 109 if code := cmd.Run([]string{"-address=nope", fh3.Name()}); code != 255 { 110 t.Fatalf("expected exit code 255, got: %d", code) 111 } 112 if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error during plan") { 113 t.Fatalf("expected failed query error, got: %s", out) 114 } 115 } 116 117 func TestPlanCommand_hcl1_hcl2_strict(t *testing.T) { 118 ci.Parallel(t) 119 120 _, _, addr := testServer(t, false, nil) 121 122 t.Run("-hcl1 implies -hcl2-strict is false", func(t *testing.T) { 123 ui := cli.NewMockUi() 124 cmd := &JobPlanCommand{Meta: Meta{Ui: ui}} 125 got := cmd.Run([]string{ 126 "-hcl1", "-hcl2-strict", 127 "-address", addr, 128 "assets/example-short.nomad", 129 }) 130 // Exit code 1 here means that an alloc will be created, which is 131 // expected. 132 require.Equal(t, 1, got) 133 }) 134 } 135 136 func TestPlanCommand_From_STDIN(t *testing.T) { 137 ci.Parallel(t) 138 stdinR, stdinW, err := os.Pipe() 139 if err != nil { 140 t.Fatalf("err: %s", err) 141 } 142 143 ui := cli.NewMockUi() 144 cmd := &JobPlanCommand{ 145 Meta: Meta{Ui: ui}, 146 JobGetter: JobGetter{testStdin: stdinR}, 147 } 148 149 go func() { 150 stdinW.WriteString(` 151 job "job1" { 152 type = "service" 153 datacenters = [ "dc1" ] 154 group "group1" { 155 count = 1 156 task "task1" { 157 driver = "exec" 158 resources { 159 cpu = 1000 160 memory = 512 161 } 162 } 163 } 164 }`) 165 stdinW.Close() 166 }() 167 168 args := []string{"-"} 169 if code := cmd.Run(args); code != 255 { 170 t.Fatalf("expected exit code 255, got %d: %q", code, ui.ErrorWriter.String()) 171 } 172 173 if out := ui.ErrorWriter.String(); !strings.Contains(out, "connection refused") { 174 t.Fatalf("expected connection refused error, got: %s", out) 175 } 176 ui.ErrorWriter.Reset() 177 } 178 179 func TestPlanCommand_From_Files(t *testing.T) { 180 181 // Create a Vault server 182 v := testutil.NewTestVault(t) 183 defer v.Stop() 184 185 // Create a Nomad server 186 s := testutil.NewTestServer(t, func(c *testutil.TestServerConfig) { 187 c.Vault.Address = v.HTTPAddr 188 c.Vault.Enabled = true 189 c.Vault.AllowUnauthenticated = false 190 c.Vault.Token = v.RootToken 191 }) 192 defer s.Stop() 193 194 t.Run("fail to place", func(t *testing.T) { 195 ui := cli.NewMockUi() 196 cmd := &JobPlanCommand{Meta: Meta{Ui: ui}} 197 args := []string{"-address", "http://" + s.HTTPAddr, "testdata/example-basic.nomad"} 198 code := cmd.Run(args) 199 require.Equal(t, 1, code) // no client running, fail to place 200 must.StrContains(t, ui.OutputWriter.String(), "WARNING: Failed to place all allocations.") 201 }) 202 203 t.Run("vault no token", func(t *testing.T) { 204 ui := cli.NewMockUi() 205 cmd := &JobPlanCommand{Meta: Meta{Ui: ui}} 206 args := []string{"-address", "http://" + s.HTTPAddr, "testdata/example-vault.nomad"} 207 code := cmd.Run(args) 208 must.Eq(t, 255, code) 209 must.StrContains(t, ui.ErrorWriter.String(), "* Vault used in the job but missing Vault token") 210 }) 211 212 t.Run("vault bad token via flag", func(t *testing.T) { 213 ui := cli.NewMockUi() 214 cmd := &JobPlanCommand{Meta: Meta{Ui: ui}} 215 args := []string{"-address", "http://" + s.HTTPAddr, "-vault-token=abc123", "testdata/example-vault.nomad"} 216 code := cmd.Run(args) 217 must.Eq(t, 255, code) 218 must.StrContains(t, ui.ErrorWriter.String(), "* bad token") 219 }) 220 221 t.Run("vault bad token via env", func(t *testing.T) { 222 t.Setenv("VAULT_TOKEN", "abc123") 223 ui := cli.NewMockUi() 224 cmd := &JobPlanCommand{Meta: Meta{Ui: ui}} 225 args := []string{"-address", "http://" + s.HTTPAddr, "testdata/example-vault.nomad"} 226 code := cmd.Run(args) 227 must.Eq(t, 255, code) 228 must.StrContains(t, ui.ErrorWriter.String(), "* bad token") 229 }) 230 } 231 232 func TestPlanCommand_From_URL(t *testing.T) { 233 ci.Parallel(t) 234 ui := cli.NewMockUi() 235 cmd := &JobPlanCommand{ 236 Meta: Meta{Ui: ui}, 237 } 238 239 args := []string{"https://example.com/foo/bar"} 240 if code := cmd.Run(args); code != 255 { 241 t.Fatalf("expected exit code 255, got %d: %q", code, ui.ErrorWriter.String()) 242 } 243 244 if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting jobfile") { 245 t.Fatalf("expected error getting jobfile, got: %s", out) 246 } 247 } 248 249 func TestPlanCommad_Preemptions(t *testing.T) { 250 ci.Parallel(t) 251 ui := cli.NewMockUi() 252 cmd := &JobPlanCommand{Meta: Meta{Ui: ui}} 253 require := require.New(t) 254 255 // Only one preempted alloc 256 resp1 := &api.JobPlanResponse{ 257 Annotations: &api.PlanAnnotations{ 258 PreemptedAllocs: []*api.AllocationListStub{ 259 { 260 ID: "alloc1", 261 JobID: "jobID1", 262 TaskGroup: "meta", 263 JobType: "batch", 264 Namespace: "test", 265 }, 266 }, 267 }, 268 } 269 cmd.addPreemptions(resp1) 270 out := ui.OutputWriter.String() 271 require.Contains(out, "Alloc ID") 272 require.Contains(out, "alloc1") 273 274 // Less than 10 unique job ids 275 var preemptedAllocs []*api.AllocationListStub 276 for i := 0; i < 12; i++ { 277 job_id := "job" + strconv.Itoa(i%4) 278 alloc := &api.AllocationListStub{ 279 ID: "alloc", 280 JobID: job_id, 281 TaskGroup: "meta", 282 JobType: "batch", 283 Namespace: "test", 284 } 285 preemptedAllocs = append(preemptedAllocs, alloc) 286 } 287 288 resp2 := &api.JobPlanResponse{ 289 Annotations: &api.PlanAnnotations{ 290 PreemptedAllocs: preemptedAllocs, 291 }, 292 } 293 ui.OutputWriter.Reset() 294 cmd.addPreemptions(resp2) 295 out = ui.OutputWriter.String() 296 require.Contains(out, "Job ID") 297 require.Contains(out, "Namespace") 298 299 // More than 10 unique job IDs 300 preemptedAllocs = make([]*api.AllocationListStub, 0) 301 var job_type string 302 for i := 0; i < 20; i++ { 303 job_id := "job" + strconv.Itoa(i) 304 if i%2 == 0 { 305 job_type = "service" 306 } else { 307 job_type = "batch" 308 } 309 alloc := &api.AllocationListStub{ 310 ID: "alloc", 311 JobID: job_id, 312 TaskGroup: "meta", 313 JobType: job_type, 314 Namespace: "test", 315 } 316 preemptedAllocs = append(preemptedAllocs, alloc) 317 } 318 319 resp3 := &api.JobPlanResponse{ 320 Annotations: &api.PlanAnnotations{ 321 PreemptedAllocs: preemptedAllocs, 322 }, 323 } 324 ui.OutputWriter.Reset() 325 cmd.addPreemptions(resp3) 326 out = ui.OutputWriter.String() 327 require.Contains(out, "Job Type") 328 require.Contains(out, "batch") 329 require.Contains(out, "service") 330 } 331 332 func TestPlanCommad_JSON(t *testing.T) { 333 ui := cli.NewMockUi() 334 cmd := &JobPlanCommand{ 335 Meta: Meta{Ui: ui}, 336 } 337 338 args := []string{ 339 "-address=http://nope", 340 "-json", 341 "testdata/example-short.json", 342 } 343 code := cmd.Run(args) 344 require.Equal(t, 255, code) 345 require.Contains(t, ui.ErrorWriter.String(), "Error during plan: Put") 346 }