github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/command/helpers_test.go (about) 1 package command 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "net/http" 9 "os" 10 "reflect" 11 "strings" 12 "testing" 13 "time" 14 15 "github.com/hashicorp/nomad/api" 16 "github.com/hashicorp/nomad/ci" 17 "github.com/hashicorp/nomad/helper/flatmap" 18 "github.com/hashicorp/nomad/helper/pointer" 19 "github.com/kr/pretty" 20 "github.com/mitchellh/cli" 21 "github.com/stretchr/testify/require" 22 ) 23 24 func TestHelpers_FormatKV(t *testing.T) { 25 ci.Parallel(t) 26 in := []string{"alpha|beta", "charlie|delta", "echo|"} 27 out := formatKV(in) 28 29 expect := "alpha = beta\n" 30 expect += "charlie = delta\n" 31 expect += "echo = <none>" 32 33 if out != expect { 34 t.Fatalf("expect: %s, got: %s", expect, out) 35 } 36 } 37 38 func TestHelpers_FormatList(t *testing.T) { 39 ci.Parallel(t) 40 in := []string{"alpha|beta||delta"} 41 out := formatList(in) 42 43 expect := "alpha beta <none> delta" 44 45 if out != expect { 46 t.Fatalf("expect: %s, got: %s", expect, out) 47 } 48 } 49 50 func TestHelpers_NodeID(t *testing.T) { 51 ci.Parallel(t) 52 srv, _, _ := testServer(t, false, nil) 53 defer srv.Shutdown() 54 55 meta := Meta{Ui: cli.NewMockUi()} 56 client, err := meta.Client() 57 if err != nil { 58 t.FailNow() 59 } 60 61 // This is because there is no client 62 if _, err := getLocalNodeID(client); err == nil { 63 t.Fatalf("getLocalNodeID() should fail") 64 } 65 } 66 67 func TestHelpers_LineLimitReader_NoTimeLimit(t *testing.T) { 68 ci.Parallel(t) 69 helloString := `hello 70 world 71 this 72 is 73 a 74 test` 75 76 noLines := "jskdfhjasdhfjkajkldsfdlsjkahfkjdsafa" 77 78 cases := []struct { 79 Input string 80 Output string 81 Lines int 82 SearchLimit int 83 }{ 84 { 85 Input: helloString, 86 Output: helloString, 87 Lines: 6, 88 SearchLimit: 1000, 89 }, 90 { 91 Input: helloString, 92 Output: `world 93 this 94 is 95 a 96 test`, 97 Lines: 5, 98 SearchLimit: 1000, 99 }, 100 { 101 Input: helloString, 102 Output: `test`, 103 Lines: 1, 104 SearchLimit: 1000, 105 }, 106 { 107 Input: helloString, 108 Output: "", 109 Lines: 0, 110 SearchLimit: 1000, 111 }, 112 { 113 Input: helloString, 114 Output: helloString, 115 Lines: 6, 116 SearchLimit: 1, // Exceed the limit 117 }, 118 { 119 Input: noLines, 120 Output: noLines, 121 Lines: 10, 122 SearchLimit: 1000, 123 }, 124 { 125 Input: noLines, 126 Output: noLines, 127 Lines: 10, 128 SearchLimit: 2, 129 }, 130 } 131 132 for i, c := range cases { 133 in := ioutil.NopCloser(strings.NewReader(c.Input)) 134 limit := NewLineLimitReader(in, c.Lines, c.SearchLimit, 0) 135 outBytes, err := ioutil.ReadAll(limit) 136 if err != nil { 137 t.Fatalf("case %d failed: %v", i, err) 138 } 139 140 out := string(outBytes) 141 if out != c.Output { 142 t.Fatalf("case %d: got %q; want %q", i, out, c.Output) 143 } 144 } 145 } 146 147 type testReadCloser struct { 148 data chan []byte 149 } 150 151 func (t *testReadCloser) Read(p []byte) (n int, err error) { 152 select { 153 case b, ok := <-t.data: 154 if !ok { 155 return 0, io.EOF 156 } 157 158 return copy(p, b), nil 159 case <-time.After(10 * time.Millisecond): 160 return 0, nil 161 } 162 } 163 164 func (t *testReadCloser) Close() error { 165 close(t.data) 166 return nil 167 } 168 169 func TestHelpers_LineLimitReader_TimeLimit(t *testing.T) { 170 ci.Parallel(t) 171 // Create the test reader 172 in := &testReadCloser{data: make(chan []byte)} 173 174 // Set up the reader such that it won't hit the line/buffer limit and could 175 // only terminate if it hits the time limit 176 limit := NewLineLimitReader(in, 1000, 1000, 100*time.Millisecond) 177 178 expected := []byte("hello world") 179 180 errCh := make(chan error) 181 resultCh := make(chan []byte) 182 go func() { 183 defer close(resultCh) 184 defer close(errCh) 185 outBytes, err := ioutil.ReadAll(limit) 186 if err != nil { 187 errCh <- fmt.Errorf("ReadAll failed: %v", err) 188 return 189 } 190 resultCh <- outBytes 191 }() 192 193 // Send the data 194 in.data <- expected 195 in.Close() 196 197 select { 198 case err := <-errCh: 199 if err != nil { 200 t.Fatalf("ReadAll: %v", err) 201 } 202 case outBytes := <-resultCh: 203 if !reflect.DeepEqual(outBytes, expected) { 204 t.Fatalf("got:%s, expected,%s", string(outBytes), string(expected)) 205 } 206 case <-time.After(1 * time.Second): 207 t.Fatalf("did not exit by time limit") 208 } 209 } 210 211 const ( 212 job = `job "job1" { 213 type = "service" 214 datacenters = ["dc1"] 215 group "group1" { 216 count = 1 217 task "task1" { 218 driver = "exec" 219 resources {} 220 } 221 restart { 222 attempts = 10 223 mode = "delay" 224 interval = "15s" 225 } 226 } 227 }` 228 ) 229 230 var ( 231 expectedApiJob = &api.Job{ 232 ID: pointer.Of("job1"), 233 Name: pointer.Of("job1"), 234 Type: pointer.Of("service"), 235 Datacenters: []string{"dc1"}, 236 TaskGroups: []*api.TaskGroup{ 237 { 238 Name: pointer.Of("group1"), 239 Count: pointer.Of(1), 240 RestartPolicy: &api.RestartPolicy{ 241 Attempts: pointer.Of(10), 242 Interval: pointer.Of(15 * time.Second), 243 Mode: pointer.Of("delay"), 244 }, 245 246 Tasks: []*api.Task{ 247 { 248 Driver: "exec", 249 Name: "task1", 250 Resources: &api.Resources{}, 251 }, 252 }, 253 }, 254 }, 255 } 256 ) 257 258 // Test APIJob with local jobfile 259 func TestJobGetter_LocalFile(t *testing.T) { 260 ci.Parallel(t) 261 fh, err := ioutil.TempFile("", "nomad") 262 if err != nil { 263 t.Fatalf("err: %s", err) 264 } 265 defer os.Remove(fh.Name()) 266 _, err = fh.WriteString(job) 267 if err != nil { 268 t.Fatalf("err: %s", err) 269 } 270 271 j := &JobGetter{} 272 aj, err := j.ApiJob(fh.Name()) 273 if err != nil { 274 t.Fatalf("err: %s", err) 275 } 276 277 if !reflect.DeepEqual(expectedApiJob, aj) { 278 eflat := flatmap.Flatten(expectedApiJob, nil, false) 279 aflat := flatmap.Flatten(aj, nil, false) 280 t.Fatalf("got:\n%v\nwant:\n%v", aflat, eflat) 281 } 282 } 283 284 // TestJobGetter_LocalFile_InvalidHCL2 asserts that a custom message is emited 285 // if the file is a valid HCL1 but not HCL2 286 func TestJobGetter_LocalFile_InvalidHCL2(t *testing.T) { 287 ci.Parallel(t) 288 289 cases := []struct { 290 name string 291 hcl string 292 expectHCL1Message bool 293 }{ 294 { 295 "invalid HCL", 296 "nothing", 297 false, 298 }, 299 { 300 "invalid HCL2", 301 `job "example" { 302 meta { "key.with.dot" = "b" } 303 }`, 304 true, 305 }, 306 } 307 308 for _, c := range cases { 309 t.Run(c.name, func(t *testing.T) { 310 fh, err := ioutil.TempFile("", "nomad") 311 require.NoError(t, err) 312 defer os.Remove(fh.Name()) 313 defer fh.Close() 314 315 _, err = fh.WriteString(c.hcl) 316 require.NoError(t, err) 317 318 j := &JobGetter{} 319 _, err = j.ApiJob(fh.Name()) 320 require.Error(t, err) 321 322 exptMessage := "Failed to parse using HCL 2. Use the HCL 1" 323 if c.expectHCL1Message { 324 require.Contains(t, err.Error(), exptMessage) 325 } else { 326 require.NotContains(t, err.Error(), exptMessage) 327 } 328 }) 329 } 330 } 331 332 // TestJobGetter_HCL2_Variables asserts variable arguments from CLI 333 // and varfiles are both honored 334 func TestJobGetter_HCL2_Variables(t *testing.T) { 335 336 hcl := ` 337 variables { 338 var1 = "default-val" 339 var2 = "default-val" 340 var3 = "default-val" 341 var4 = "default-val" 342 } 343 344 job "example" { 345 datacenters = ["${var.var1}", "${var.var2}", "${var.var3}", "${var.var4}"] 346 } 347 ` 348 t.Setenv("NOMAD_VAR_var4", "from-envvar") 349 350 cliArgs := []string{`var2=from-cli`} 351 fileVars := `var3 = "from-varfile"` 352 expected := []string{"default-val", "from-cli", "from-varfile", "from-envvar"} 353 354 hclf, err := ioutil.TempFile("", "hcl") 355 require.NoError(t, err) 356 defer os.Remove(hclf.Name()) 357 defer hclf.Close() 358 359 _, err = hclf.WriteString(hcl) 360 require.NoError(t, err) 361 362 vf, err := ioutil.TempFile("", "var.hcl") 363 require.NoError(t, err) 364 defer os.Remove(vf.Name()) 365 defer vf.Close() 366 367 _, err = vf.WriteString(fileVars + "\n") 368 require.NoError(t, err) 369 370 j, err := (&JobGetter{}).ApiJobWithArgs(hclf.Name(), cliArgs, []string{vf.Name()}, true) 371 require.NoError(t, err) 372 373 require.NotNil(t, j) 374 require.Equal(t, expected, j.Datacenters) 375 } 376 377 func TestJobGetter_HCL2_Variables_StrictFalse(t *testing.T) { 378 379 hcl := ` 380 variables { 381 var1 = "default-val" 382 var2 = "default-val" 383 var3 = "default-val" 384 var4 = "default-val" 385 } 386 387 job "example" { 388 datacenters = ["${var.var1}", "${var.var2}", "${var.var3}", "${var.var4}"] 389 } 390 ` 391 392 t.Setenv("NOMAD_VAR_var4", "from-envvar") 393 394 // Both the CLI and var file contain variables that are not used with the 395 // template and therefore would error, if hcl2-strict was true. 396 cliArgs := []string{`var2=from-cli`, `unsedVar1=from-cli`} 397 fileVars := ` 398 var3 = "from-varfile" 399 unsedVar2 = "from-varfile" 400 ` 401 expected := []string{"default-val", "from-cli", "from-varfile", "from-envvar"} 402 403 hclf, err := ioutil.TempFile("", "hcl") 404 require.NoError(t, err) 405 defer os.Remove(hclf.Name()) 406 defer hclf.Close() 407 408 _, err = hclf.WriteString(hcl) 409 require.NoError(t, err) 410 411 vf, err := ioutil.TempFile("", "var.hcl") 412 require.NoError(t, err) 413 defer os.Remove(vf.Name()) 414 defer vf.Close() 415 416 _, err = vf.WriteString(fileVars + "\n") 417 require.NoError(t, err) 418 419 j, err := (&JobGetter{}).ApiJobWithArgs(hclf.Name(), cliArgs, []string{vf.Name()}, false) 420 require.NoError(t, err) 421 422 require.NotNil(t, j) 423 require.Equal(t, expected, j.Datacenters) 424 } 425 426 // Test StructJob with jobfile from HTTP Server 427 func TestJobGetter_HTTPServer(t *testing.T) { 428 ci.Parallel(t) 429 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 430 fmt.Fprintf(w, job) 431 }) 432 go http.ListenAndServe("127.0.0.1:12345", nil) 433 434 // Wait until HTTP Server starts certainly 435 time.Sleep(100 * time.Millisecond) 436 437 j := &JobGetter{} 438 aj, err := j.ApiJob("http://127.0.0.1:12345/") 439 if err != nil { 440 t.Fatalf("err: %s", err) 441 } 442 if !reflect.DeepEqual(expectedApiJob, aj) { 443 for _, d := range pretty.Diff(expectedApiJob, aj) { 444 t.Logf(d) 445 } 446 t.Fatalf("Unexpected file") 447 } 448 } 449 450 func TestJobGetter_Validate(t *testing.T) { 451 cases := []struct { 452 name string 453 jg JobGetter 454 errContains string 455 }{ 456 { 457 "StrictAndHCL1", 458 JobGetter{ 459 HCL1: true, 460 Strict: true, 461 }, 462 "HCLv1 and HCLv2 strict", 463 }, 464 { 465 "JSONandHCL1", 466 JobGetter{ 467 HCL1: true, 468 JSON: true, 469 }, 470 "HCL and JSON", 471 }, 472 { 473 "VarsAndHCL1", 474 JobGetter{ 475 HCL1: true, 476 Vars: []string{"foo"}, 477 }, 478 "variables with HCLv1", 479 }, 480 { 481 "VarFilesAndHCL1", 482 JobGetter{ 483 HCL1: true, 484 VarFiles: []string{"foo.var"}, 485 }, 486 "variables with HCLv1", 487 }, 488 { 489 "VarsAndJSON", 490 JobGetter{ 491 JSON: true, 492 Vars: []string{"foo"}, 493 }, 494 "variables with JSON", 495 }, 496 { 497 "VarFilesAndJSON", 498 JobGetter{ 499 JSON: true, 500 VarFiles: []string{"foo.var"}, 501 }, 502 "variables with JSON files", 503 }, 504 { 505 "JSON_OK", 506 JobGetter{ 507 JSON: true, 508 }, 509 "", 510 }, 511 } 512 513 for _, tc := range cases { 514 t.Run(tc.name, func(t *testing.T) { 515 err := tc.jg.Validate() 516 517 switch tc.errContains { 518 case "": 519 require.NoError(t, err) 520 default: 521 require.ErrorContains(t, err, tc.errContains) 522 } 523 524 }) 525 } 526 } 527 528 func TestPrettyTimeDiff(t *testing.T) { 529 // Grab the time and truncate to the nearest second. This allows our tests 530 // to be deterministic since we don't have to worry about rounding. 531 now := time.Now().Truncate(time.Second) 532 533 test_cases := []struct { 534 t1 time.Time 535 t2 time.Time 536 exp string 537 }{ 538 {now, time.Unix(0, 0), ""}, // This is the upgrade path case 539 {now, now.Add(-10 * time.Millisecond), "0s ago"}, 540 {now, now.Add(-740 * time.Second), "12m20s ago"}, 541 {now, now.Add(-12 * time.Minute), "12m ago"}, 542 {now, now.Add(-60 * time.Minute), "1h ago"}, 543 {now, now.Add(-80 * time.Minute), "1h20m ago"}, 544 {now, now.Add(-6 * time.Hour), "6h ago"}, 545 {now.Add(-6 * time.Hour), now, "6h from now"}, 546 {now, now.Add(-22165 * time.Second), "6h9m ago"}, 547 {now, now.Add(-100 * time.Hour), "4d4h ago"}, 548 {now, now.Add(-438000 * time.Minute), "10mo4d ago"}, 549 {now, now.Add(-20460 * time.Hour), "2y4mo ago"}, 550 } 551 for _, tc := range test_cases { 552 t.Run(tc.exp, func(t *testing.T) { 553 out := prettyTimeDiff(tc.t2, tc.t1) 554 if out != tc.exp { 555 t.Fatalf("expected :%v but got :%v", tc.exp, out) 556 } 557 }) 558 } 559 560 var t1 time.Time 561 out := prettyTimeDiff(t1, time.Now()) 562 563 if out != "" { 564 t.Fatalf("Expected empty output but got:%v", out) 565 } 566 567 } 568 569 // TestUiErrorWriter asserts that writer buffers and 570 func TestUiErrorWriter(t *testing.T) { 571 ci.Parallel(t) 572 573 var outBuf, errBuf bytes.Buffer 574 ui := &cli.BasicUi{ 575 Writer: &outBuf, 576 ErrorWriter: &errBuf, 577 } 578 579 w := &uiErrorWriter{ui: ui} 580 581 inputs := []string{ 582 "some line\n", 583 "multiple\nlines\r\nhere", 584 " with followup\nand", 585 " more lines ", 586 " without new line ", 587 "until here\nand then", 588 "some more", 589 } 590 591 partialAcc := "" 592 for _, in := range inputs { 593 n, err := w.Write([]byte(in)) 594 require.NoError(t, err) 595 require.Equal(t, len(in), n) 596 597 // assert that writer emits partial result until last new line 598 partialAcc += strings.ReplaceAll(in, "\r\n", "\n") 599 lastNL := strings.LastIndex(partialAcc, "\n") 600 require.Equal(t, partialAcc[:lastNL+1], errBuf.String()) 601 } 602 603 require.Empty(t, outBuf.String()) 604 605 // note that the \r\n got replaced by \n 606 expectedErr := "some line\nmultiple\nlines\nhere with followup\nand more lines without new line until here\n" 607 require.Equal(t, expectedErr, errBuf.String()) 608 609 // close emits the final line 610 err := w.Close() 611 require.NoError(t, err) 612 613 expectedErr += "and thensome more\n" 614 require.Equal(t, expectedErr, errBuf.String()) 615 }