github.com/hhrutter/nomad@v0.6.0-rc2.0.20170723054333-80c4b03f0705/nomad/structs/structs_test.go (about) 1 package structs 2 3 import ( 4 "fmt" 5 "reflect" 6 "strings" 7 "testing" 8 "time" 9 10 "github.com/hashicorp/consul/api" 11 "github.com/hashicorp/go-multierror" 12 "github.com/kr/pretty" 13 ) 14 15 func TestJob_Validate(t *testing.T) { 16 j := &Job{} 17 err := j.Validate() 18 mErr := err.(*multierror.Error) 19 if !strings.Contains(mErr.Errors[0].Error(), "job region") { 20 t.Fatalf("err: %s", err) 21 } 22 if !strings.Contains(mErr.Errors[1].Error(), "job ID") { 23 t.Fatalf("err: %s", err) 24 } 25 if !strings.Contains(mErr.Errors[2].Error(), "job name") { 26 t.Fatalf("err: %s", err) 27 } 28 if !strings.Contains(mErr.Errors[3].Error(), "job type") { 29 t.Fatalf("err: %s", err) 30 } 31 if !strings.Contains(mErr.Errors[4].Error(), "priority") { 32 t.Fatalf("err: %s", err) 33 } 34 if !strings.Contains(mErr.Errors[5].Error(), "datacenters") { 35 t.Fatalf("err: %s", err) 36 } 37 if !strings.Contains(mErr.Errors[6].Error(), "task groups") { 38 t.Fatalf("err: %s", err) 39 } 40 41 j = &Job{ 42 Type: "invalid-job-type", 43 } 44 err = j.Validate() 45 if expected := `Invalid job type: "invalid-job-type"`; !strings.Contains(err.Error(), expected) { 46 t.Errorf("expected %s but found: %v", expected, err) 47 } 48 49 j = &Job{ 50 Type: JobTypeService, 51 Periodic: &PeriodicConfig{ 52 Enabled: true, 53 }, 54 } 55 err = j.Validate() 56 mErr = err.(*multierror.Error) 57 if !strings.Contains(mErr.Error(), "Periodic") { 58 t.Fatalf("err: %s", err) 59 } 60 61 j = &Job{ 62 Region: "global", 63 ID: GenerateUUID(), 64 Name: "my-job", 65 Type: JobTypeService, 66 Priority: 50, 67 Datacenters: []string{"dc1"}, 68 TaskGroups: []*TaskGroup{ 69 &TaskGroup{ 70 Name: "web", 71 RestartPolicy: &RestartPolicy{ 72 Interval: 5 * time.Minute, 73 Delay: 10 * time.Second, 74 Attempts: 10, 75 }, 76 }, 77 &TaskGroup{ 78 Name: "web", 79 RestartPolicy: &RestartPolicy{ 80 Interval: 5 * time.Minute, 81 Delay: 10 * time.Second, 82 Attempts: 10, 83 }, 84 }, 85 &TaskGroup{ 86 RestartPolicy: &RestartPolicy{ 87 Interval: 5 * time.Minute, 88 Delay: 10 * time.Second, 89 Attempts: 10, 90 }, 91 }, 92 }, 93 } 94 err = j.Validate() 95 mErr = err.(*multierror.Error) 96 if !strings.Contains(mErr.Errors[0].Error(), "2 redefines 'web' from group 1") { 97 t.Fatalf("err: %s", err) 98 } 99 if !strings.Contains(mErr.Errors[1].Error(), "group 3 missing name") { 100 t.Fatalf("err: %s", err) 101 } 102 if !strings.Contains(mErr.Errors[2].Error(), "Task group web validation failed") { 103 t.Fatalf("err: %s", err) 104 } 105 } 106 107 func TestJob_Warnings(t *testing.T) { 108 cases := []struct { 109 Name string 110 Job *Job 111 Expected []string 112 }{ 113 { 114 Name: "Higher counts for update stanza", 115 Expected: []string{"max parallel count is greater"}, 116 Job: &Job{ 117 Type: JobTypeService, 118 TaskGroups: []*TaskGroup{ 119 { 120 Name: "foo", 121 Count: 2, 122 Update: &UpdateStrategy{ 123 MaxParallel: 10, 124 }, 125 }, 126 }, 127 }, 128 }, 129 } 130 131 for _, c := range cases { 132 t.Run(c.Name, func(t *testing.T) { 133 warnings := c.Job.Warnings() 134 if warnings == nil { 135 if len(c.Expected) == 0 { 136 return 137 } else { 138 t.Fatal("Got no warnings when they were expected") 139 } 140 } 141 142 a := warnings.Error() 143 for _, e := range c.Expected { 144 if !strings.Contains(a, e) { 145 t.Fatalf("Got warnings %q; didn't contain %q", a, e) 146 } 147 } 148 }) 149 } 150 } 151 152 func TestJob_Canonicalize_Update(t *testing.T) { 153 cases := []struct { 154 Name string 155 Job *Job 156 Expected *Job 157 Warnings []string 158 }{ 159 { 160 Name: "One task group", 161 Warnings: []string{"conversion to new update stanza"}, 162 Job: &Job{ 163 Type: JobTypeService, 164 Update: UpdateStrategy{ 165 MaxParallel: 2, 166 Stagger: 10 * time.Second, 167 }, 168 TaskGroups: []*TaskGroup{ 169 { 170 Name: "foo", 171 Count: 2, 172 }, 173 }, 174 }, 175 Expected: &Job{ 176 Type: JobTypeService, 177 Update: UpdateStrategy{ 178 MaxParallel: 2, 179 Stagger: 10 * time.Second, 180 }, 181 TaskGroups: []*TaskGroup{ 182 { 183 Name: "foo", 184 Count: 2, 185 RestartPolicy: NewRestartPolicy(JobTypeService), 186 EphemeralDisk: DefaultEphemeralDisk(), 187 Update: &UpdateStrategy{ 188 Stagger: 30 * time.Second, 189 MaxParallel: 2, 190 HealthCheck: UpdateStrategyHealthCheck_Checks, 191 MinHealthyTime: 10 * time.Second, 192 HealthyDeadline: 5 * time.Minute, 193 AutoRevert: false, 194 Canary: 0, 195 }, 196 }, 197 }, 198 }, 199 }, 200 { 201 Name: "One task group batch", 202 Warnings: []string{"Update stanza is disallowed for batch jobs"}, 203 Job: &Job{ 204 Type: JobTypeBatch, 205 Update: UpdateStrategy{ 206 MaxParallel: 2, 207 Stagger: 10 * time.Second, 208 }, 209 TaskGroups: []*TaskGroup{ 210 { 211 Name: "foo", 212 Count: 2, 213 }, 214 }, 215 }, 216 Expected: &Job{ 217 Type: JobTypeBatch, 218 Update: UpdateStrategy{}, 219 TaskGroups: []*TaskGroup{ 220 { 221 Name: "foo", 222 Count: 2, 223 RestartPolicy: NewRestartPolicy(JobTypeBatch), 224 EphemeralDisk: DefaultEphemeralDisk(), 225 }, 226 }, 227 }, 228 }, 229 { 230 Name: "One task group batch - new spec", 231 Warnings: []string{"Update stanza is disallowed for batch jobs"}, 232 Job: &Job{ 233 Type: JobTypeBatch, 234 Update: UpdateStrategy{ 235 Stagger: 2 * time.Second, 236 MaxParallel: 2, 237 Canary: 2, 238 MinHealthyTime: 2 * time.Second, 239 HealthyDeadline: 10 * time.Second, 240 HealthCheck: UpdateStrategyHealthCheck_Checks, 241 }, 242 TaskGroups: []*TaskGroup{ 243 { 244 Name: "foo", 245 Count: 2, 246 Update: &UpdateStrategy{ 247 Stagger: 2 * time.Second, 248 MaxParallel: 2, 249 Canary: 2, 250 MinHealthyTime: 2 * time.Second, 251 HealthyDeadline: 10 * time.Second, 252 HealthCheck: UpdateStrategyHealthCheck_Checks, 253 }, 254 }, 255 }, 256 }, 257 Expected: &Job{ 258 Type: JobTypeBatch, 259 Update: UpdateStrategy{}, 260 TaskGroups: []*TaskGroup{ 261 { 262 Name: "foo", 263 Count: 2, 264 RestartPolicy: NewRestartPolicy(JobTypeBatch), 265 EphemeralDisk: DefaultEphemeralDisk(), 266 }, 267 }, 268 }, 269 }, 270 { 271 Name: "One task group service - new spec", 272 Job: &Job{ 273 Type: JobTypeService, 274 Update: UpdateStrategy{ 275 Stagger: 2 * time.Second, 276 MaxParallel: 2, 277 Canary: 2, 278 MinHealthyTime: 2 * time.Second, 279 HealthyDeadline: 10 * time.Second, 280 HealthCheck: UpdateStrategyHealthCheck_Checks, 281 }, 282 TaskGroups: []*TaskGroup{ 283 { 284 Name: "foo", 285 Count: 2, 286 Update: &UpdateStrategy{ 287 Stagger: 2 * time.Second, 288 MaxParallel: 2, 289 Canary: 2, 290 MinHealthyTime: 2 * time.Second, 291 HealthyDeadline: 10 * time.Second, 292 HealthCheck: UpdateStrategyHealthCheck_Checks, 293 }, 294 }, 295 }, 296 }, 297 Expected: &Job{ 298 Type: JobTypeService, 299 Update: UpdateStrategy{ 300 Stagger: 2 * time.Second, 301 MaxParallel: 2, 302 Canary: 2, 303 MinHealthyTime: 2 * time.Second, 304 HealthyDeadline: 10 * time.Second, 305 HealthCheck: UpdateStrategyHealthCheck_Checks, 306 }, 307 TaskGroups: []*TaskGroup{ 308 { 309 Name: "foo", 310 Count: 2, 311 RestartPolicy: NewRestartPolicy(JobTypeService), 312 EphemeralDisk: DefaultEphemeralDisk(), 313 Update: &UpdateStrategy{ 314 Stagger: 2 * time.Second, 315 MaxParallel: 2, 316 Canary: 2, 317 MinHealthyTime: 2 * time.Second, 318 HealthyDeadline: 10 * time.Second, 319 HealthCheck: UpdateStrategyHealthCheck_Checks, 320 }, 321 }, 322 }, 323 }, 324 }, 325 { 326 Name: "One task group; too high of parallelism", 327 Warnings: []string{"conversion to new update stanza"}, 328 Job: &Job{ 329 Type: JobTypeService, 330 Update: UpdateStrategy{ 331 MaxParallel: 200, 332 Stagger: 10 * time.Second, 333 }, 334 TaskGroups: []*TaskGroup{ 335 { 336 Name: "foo", 337 Count: 2, 338 }, 339 }, 340 }, 341 Expected: &Job{ 342 Type: JobTypeService, 343 Update: UpdateStrategy{ 344 MaxParallel: 200, 345 Stagger: 10 * time.Second, 346 }, 347 TaskGroups: []*TaskGroup{ 348 { 349 Name: "foo", 350 Count: 2, 351 RestartPolicy: NewRestartPolicy(JobTypeService), 352 EphemeralDisk: DefaultEphemeralDisk(), 353 Update: &UpdateStrategy{ 354 Stagger: 30 * time.Second, 355 MaxParallel: 2, 356 HealthCheck: UpdateStrategyHealthCheck_Checks, 357 MinHealthyTime: 10 * time.Second, 358 HealthyDeadline: 5 * time.Minute, 359 AutoRevert: false, 360 Canary: 0, 361 }, 362 }, 363 }, 364 }, 365 }, 366 { 367 Name: "Multiple task group; rounding", 368 Warnings: []string{"conversion to new update stanza"}, 369 Job: &Job{ 370 Type: JobTypeService, 371 Update: UpdateStrategy{ 372 MaxParallel: 2, 373 Stagger: 10 * time.Second, 374 }, 375 TaskGroups: []*TaskGroup{ 376 { 377 Name: "foo", 378 Count: 2, 379 }, 380 { 381 Name: "bar", 382 Count: 14, 383 }, 384 { 385 Name: "foo", 386 Count: 26, 387 }, 388 }, 389 }, 390 Expected: &Job{ 391 Type: JobTypeService, 392 Update: UpdateStrategy{ 393 MaxParallel: 2, 394 Stagger: 10 * time.Second, 395 }, 396 TaskGroups: []*TaskGroup{ 397 { 398 Name: "foo", 399 Count: 2, 400 RestartPolicy: NewRestartPolicy(JobTypeService), 401 EphemeralDisk: DefaultEphemeralDisk(), 402 Update: &UpdateStrategy{ 403 Stagger: 30 * time.Second, 404 MaxParallel: 1, 405 HealthCheck: UpdateStrategyHealthCheck_Checks, 406 MinHealthyTime: 10 * time.Second, 407 HealthyDeadline: 5 * time.Minute, 408 AutoRevert: false, 409 Canary: 0, 410 }, 411 }, 412 { 413 Name: "bar", 414 Count: 14, 415 RestartPolicy: NewRestartPolicy(JobTypeService), 416 EphemeralDisk: DefaultEphemeralDisk(), 417 Update: &UpdateStrategy{ 418 Stagger: 30 * time.Second, 419 MaxParallel: 1, 420 HealthCheck: UpdateStrategyHealthCheck_Checks, 421 MinHealthyTime: 10 * time.Second, 422 HealthyDeadline: 5 * time.Minute, 423 AutoRevert: false, 424 Canary: 0, 425 }, 426 }, 427 { 428 Name: "foo", 429 Count: 26, 430 EphemeralDisk: DefaultEphemeralDisk(), 431 RestartPolicy: NewRestartPolicy(JobTypeService), 432 Update: &UpdateStrategy{ 433 Stagger: 30 * time.Second, 434 MaxParallel: 3, 435 HealthCheck: UpdateStrategyHealthCheck_Checks, 436 MinHealthyTime: 10 * time.Second, 437 HealthyDeadline: 5 * time.Minute, 438 AutoRevert: false, 439 Canary: 0, 440 }, 441 }, 442 }, 443 }, 444 }, 445 } 446 447 for _, c := range cases { 448 t.Run(c.Name, func(t *testing.T) { 449 warnings := c.Job.Canonicalize() 450 if !reflect.DeepEqual(c.Job, c.Expected) { 451 t.Fatalf("Diff %#v", pretty.Diff(c.Job, c.Expected)) 452 } 453 454 wErr := "" 455 if warnings != nil { 456 wErr = warnings.Error() 457 } 458 for _, w := range c.Warnings { 459 if !strings.Contains(wErr, w) { 460 t.Fatalf("Wanted warning %q: got %q", w, wErr) 461 } 462 } 463 464 if len(c.Warnings) == 0 && warnings != nil { 465 t.Fatalf("Wanted no warnings: got %q", wErr) 466 } 467 }) 468 } 469 } 470 471 func TestJob_SpecChanged(t *testing.T) { 472 // Get a base test job 473 base := testJob() 474 475 // Only modify the indexes/mutable state of the job 476 mutatedBase := base.Copy() 477 mutatedBase.Status = "foo" 478 mutatedBase.ModifyIndex = base.ModifyIndex + 100 479 480 // changed contains a spec change that should be detected 481 change := base.Copy() 482 change.Priority = 99 483 484 cases := []struct { 485 Name string 486 Original *Job 487 New *Job 488 Changed bool 489 }{ 490 { 491 Name: "Same job except mutable indexes", 492 Changed: false, 493 Original: base, 494 New: mutatedBase, 495 }, 496 { 497 Name: "Different", 498 Changed: true, 499 Original: base, 500 New: change, 501 }, 502 } 503 504 for _, c := range cases { 505 t.Run(c.Name, func(t *testing.T) { 506 if actual := c.Original.SpecChanged(c.New); actual != c.Changed { 507 t.Fatalf("SpecChanged() returned %v; want %v", actual, c.Changed) 508 } 509 }) 510 } 511 } 512 513 func testJob() *Job { 514 return &Job{ 515 Region: "global", 516 ID: GenerateUUID(), 517 Name: "my-job", 518 Type: JobTypeService, 519 Priority: 50, 520 AllAtOnce: false, 521 Datacenters: []string{"dc1"}, 522 Constraints: []*Constraint{ 523 &Constraint{ 524 LTarget: "$attr.kernel.name", 525 RTarget: "linux", 526 Operand: "=", 527 }, 528 }, 529 Periodic: &PeriodicConfig{ 530 Enabled: false, 531 }, 532 TaskGroups: []*TaskGroup{ 533 &TaskGroup{ 534 Name: "web", 535 Count: 10, 536 EphemeralDisk: DefaultEphemeralDisk(), 537 RestartPolicy: &RestartPolicy{ 538 Mode: RestartPolicyModeFail, 539 Attempts: 3, 540 Interval: 10 * time.Minute, 541 Delay: 1 * time.Minute, 542 }, 543 Tasks: []*Task{ 544 &Task{ 545 Name: "web", 546 Driver: "exec", 547 Config: map[string]interface{}{ 548 "command": "/bin/date", 549 }, 550 Env: map[string]string{ 551 "FOO": "bar", 552 }, 553 Artifacts: []*TaskArtifact{ 554 { 555 GetterSource: "http://foo.com", 556 }, 557 }, 558 Services: []*Service{ 559 { 560 Name: "${TASK}-frontend", 561 PortLabel: "http", 562 }, 563 }, 564 Resources: &Resources{ 565 CPU: 500, 566 MemoryMB: 256, 567 Networks: []*NetworkResource{ 568 &NetworkResource{ 569 MBits: 50, 570 DynamicPorts: []Port{{Label: "http"}}, 571 }, 572 }, 573 }, 574 LogConfig: &LogConfig{ 575 MaxFiles: 10, 576 MaxFileSizeMB: 1, 577 }, 578 }, 579 }, 580 Meta: map[string]string{ 581 "elb_check_type": "http", 582 "elb_check_interval": "30s", 583 "elb_check_min": "3", 584 }, 585 }, 586 }, 587 Meta: map[string]string{ 588 "owner": "armon", 589 }, 590 } 591 } 592 593 func TestJob_Copy(t *testing.T) { 594 j := testJob() 595 c := j.Copy() 596 if !reflect.DeepEqual(j, c) { 597 t.Fatalf("Copy() returned an unequal Job; got %#v; want %#v", c, j) 598 } 599 } 600 601 func TestJob_IsPeriodic(t *testing.T) { 602 j := &Job{ 603 Type: JobTypeService, 604 Periodic: &PeriodicConfig{ 605 Enabled: true, 606 }, 607 } 608 if !j.IsPeriodic() { 609 t.Fatalf("IsPeriodic() returned false on periodic job") 610 } 611 612 j = &Job{ 613 Type: JobTypeService, 614 } 615 if j.IsPeriodic() { 616 t.Fatalf("IsPeriodic() returned true on non-periodic job") 617 } 618 } 619 620 func TestJob_SystemJob_Validate(t *testing.T) { 621 j := testJob() 622 j.Type = JobTypeSystem 623 j.Canonicalize() 624 625 err := j.Validate() 626 if err == nil || !strings.Contains(err.Error(), "exceed") { 627 t.Fatalf("expect error due to count") 628 } 629 630 j.TaskGroups[0].Count = 0 631 if err := j.Validate(); err != nil { 632 t.Fatalf("unexpected err: %v", err) 633 } 634 635 j.TaskGroups[0].Count = 1 636 if err := j.Validate(); err != nil { 637 t.Fatalf("unexpected err: %v", err) 638 } 639 } 640 641 func TestJob_VaultPolicies(t *testing.T) { 642 j0 := &Job{} 643 e0 := make(map[string]map[string]*Vault, 0) 644 645 vj1 := &Vault{ 646 Policies: []string{ 647 "p1", 648 "p2", 649 }, 650 } 651 vj2 := &Vault{ 652 Policies: []string{ 653 "p3", 654 "p4", 655 }, 656 } 657 vj3 := &Vault{ 658 Policies: []string{ 659 "p5", 660 }, 661 } 662 j1 := &Job{ 663 TaskGroups: []*TaskGroup{ 664 &TaskGroup{ 665 Name: "foo", 666 Tasks: []*Task{ 667 &Task{ 668 Name: "t1", 669 }, 670 &Task{ 671 Name: "t2", 672 Vault: vj1, 673 }, 674 }, 675 }, 676 &TaskGroup{ 677 Name: "bar", 678 Tasks: []*Task{ 679 &Task{ 680 Name: "t3", 681 Vault: vj2, 682 }, 683 &Task{ 684 Name: "t4", 685 Vault: vj3, 686 }, 687 }, 688 }, 689 }, 690 } 691 692 e1 := map[string]map[string]*Vault{ 693 "foo": map[string]*Vault{ 694 "t2": vj1, 695 }, 696 "bar": map[string]*Vault{ 697 "t3": vj2, 698 "t4": vj3, 699 }, 700 } 701 702 cases := []struct { 703 Job *Job 704 Expected map[string]map[string]*Vault 705 }{ 706 { 707 Job: j0, 708 Expected: e0, 709 }, 710 { 711 Job: j1, 712 Expected: e1, 713 }, 714 } 715 716 for i, c := range cases { 717 got := c.Job.VaultPolicies() 718 if !reflect.DeepEqual(got, c.Expected) { 719 t.Fatalf("case %d: got %#v; want %#v", i+1, got, c.Expected) 720 } 721 } 722 } 723 724 func TestJob_RequiredSignals(t *testing.T) { 725 j0 := &Job{} 726 e0 := make(map[string]map[string][]string, 0) 727 728 vj1 := &Vault{ 729 Policies: []string{"p1"}, 730 ChangeMode: VaultChangeModeNoop, 731 } 732 vj2 := &Vault{ 733 Policies: []string{"p1"}, 734 ChangeMode: VaultChangeModeSignal, 735 ChangeSignal: "SIGUSR1", 736 } 737 tj1 := &Template{ 738 SourcePath: "foo", 739 DestPath: "bar", 740 ChangeMode: TemplateChangeModeNoop, 741 } 742 tj2 := &Template{ 743 SourcePath: "foo", 744 DestPath: "bar", 745 ChangeMode: TemplateChangeModeSignal, 746 ChangeSignal: "SIGUSR2", 747 } 748 j1 := &Job{ 749 TaskGroups: []*TaskGroup{ 750 &TaskGroup{ 751 Name: "foo", 752 Tasks: []*Task{ 753 &Task{ 754 Name: "t1", 755 }, 756 &Task{ 757 Name: "t2", 758 Vault: vj2, 759 Templates: []*Template{tj2}, 760 }, 761 }, 762 }, 763 &TaskGroup{ 764 Name: "bar", 765 Tasks: []*Task{ 766 &Task{ 767 Name: "t3", 768 Vault: vj1, 769 Templates: []*Template{tj1}, 770 }, 771 &Task{ 772 Name: "t4", 773 Vault: vj2, 774 }, 775 }, 776 }, 777 }, 778 } 779 780 e1 := map[string]map[string][]string{ 781 "foo": map[string][]string{ 782 "t2": []string{"SIGUSR1", "SIGUSR2"}, 783 }, 784 "bar": map[string][]string{ 785 "t4": []string{"SIGUSR1"}, 786 }, 787 } 788 789 cases := []struct { 790 Job *Job 791 Expected map[string]map[string][]string 792 }{ 793 { 794 Job: j0, 795 Expected: e0, 796 }, 797 { 798 Job: j1, 799 Expected: e1, 800 }, 801 } 802 803 for i, c := range cases { 804 got := c.Job.RequiredSignals() 805 if !reflect.DeepEqual(got, c.Expected) { 806 t.Fatalf("case %d: got %#v; want %#v", i+1, got, c.Expected) 807 } 808 } 809 } 810 811 func TestTaskGroup_Validate(t *testing.T) { 812 j := testJob() 813 tg := &TaskGroup{ 814 Count: -1, 815 RestartPolicy: &RestartPolicy{ 816 Interval: 5 * time.Minute, 817 Delay: 10 * time.Second, 818 Attempts: 10, 819 Mode: RestartPolicyModeDelay, 820 }, 821 } 822 err := tg.Validate(j) 823 mErr := err.(*multierror.Error) 824 if !strings.Contains(mErr.Errors[0].Error(), "group name") { 825 t.Fatalf("err: %s", err) 826 } 827 if !strings.Contains(mErr.Errors[1].Error(), "count can't be negative") { 828 t.Fatalf("err: %s", err) 829 } 830 if !strings.Contains(mErr.Errors[2].Error(), "Missing tasks") { 831 t.Fatalf("err: %s", err) 832 } 833 834 tg = &TaskGroup{ 835 Tasks: []*Task{ 836 &Task{ 837 Name: "task-a", 838 Resources: &Resources{ 839 Networks: []*NetworkResource{ 840 &NetworkResource{ 841 ReservedPorts: []Port{{Label: "foo", Value: 123}}, 842 }, 843 }, 844 }, 845 }, 846 &Task{ 847 Name: "task-b", 848 Resources: &Resources{ 849 Networks: []*NetworkResource{ 850 &NetworkResource{ 851 ReservedPorts: []Port{{Label: "foo", Value: 123}}, 852 }, 853 }, 854 }, 855 }, 856 }, 857 } 858 err = tg.Validate(&Job{}) 859 expected := `Static port 123 already reserved by task-a:foo` 860 if !strings.Contains(err.Error(), expected) { 861 t.Errorf("expected %s but found: %v", expected, err) 862 } 863 864 tg = &TaskGroup{ 865 Tasks: []*Task{ 866 &Task{ 867 Name: "task-a", 868 Resources: &Resources{ 869 Networks: []*NetworkResource{ 870 &NetworkResource{ 871 ReservedPorts: []Port{ 872 {Label: "foo", Value: 123}, 873 {Label: "bar", Value: 123}, 874 }, 875 }, 876 }, 877 }, 878 }, 879 }, 880 } 881 err = tg.Validate(&Job{}) 882 expected = `Static port 123 already reserved by task-a:foo` 883 if !strings.Contains(err.Error(), expected) { 884 t.Errorf("expected %s but found: %v", expected, err) 885 } 886 887 tg = &TaskGroup{ 888 Name: "web", 889 Count: 1, 890 Tasks: []*Task{ 891 &Task{Name: "web", Leader: true}, 892 &Task{Name: "web", Leader: true}, 893 &Task{}, 894 }, 895 RestartPolicy: &RestartPolicy{ 896 Interval: 5 * time.Minute, 897 Delay: 10 * time.Second, 898 Attempts: 10, 899 Mode: RestartPolicyModeDelay, 900 }, 901 } 902 903 err = tg.Validate(j) 904 mErr = err.(*multierror.Error) 905 if !strings.Contains(mErr.Errors[0].Error(), "should have an ephemeral disk object") { 906 t.Fatalf("err: %s", err) 907 } 908 if !strings.Contains(mErr.Errors[1].Error(), "2 redefines 'web' from task 1") { 909 t.Fatalf("err: %s", err) 910 } 911 if !strings.Contains(mErr.Errors[2].Error(), "Task 3 missing name") { 912 t.Fatalf("err: %s", err) 913 } 914 if !strings.Contains(mErr.Errors[3].Error(), "Only one task may be marked as leader") { 915 t.Fatalf("err: %s", err) 916 } 917 if !strings.Contains(mErr.Errors[4].Error(), "Task web validation failed") { 918 t.Fatalf("err: %s", err) 919 } 920 921 // COMPAT: Enable in 0.7.0 922 //j.Type = JobTypeBatch 923 //err = tg.Validate(j) 924 //if !strings.Contains(err.Error(), "does not allow update block") { 925 //t.Fatalf("err: %s", err) 926 //} 927 } 928 929 func TestTask_Validate(t *testing.T) { 930 task := &Task{} 931 ephemeralDisk := DefaultEphemeralDisk() 932 err := task.Validate(ephemeralDisk) 933 mErr := err.(*multierror.Error) 934 if !strings.Contains(mErr.Errors[0].Error(), "task name") { 935 t.Fatalf("err: %s", err) 936 } 937 if !strings.Contains(mErr.Errors[1].Error(), "task driver") { 938 t.Fatalf("err: %s", err) 939 } 940 if !strings.Contains(mErr.Errors[2].Error(), "task resources") { 941 t.Fatalf("err: %s", err) 942 } 943 944 task = &Task{Name: "web/foo"} 945 err = task.Validate(ephemeralDisk) 946 mErr = err.(*multierror.Error) 947 if !strings.Contains(mErr.Errors[0].Error(), "slashes") { 948 t.Fatalf("err: %s", err) 949 } 950 951 task = &Task{ 952 Name: "web", 953 Driver: "docker", 954 Resources: &Resources{ 955 CPU: 100, 956 MemoryMB: 100, 957 IOPS: 10, 958 }, 959 LogConfig: DefaultLogConfig(), 960 } 961 ephemeralDisk.SizeMB = 200 962 err = task.Validate(ephemeralDisk) 963 if err != nil { 964 t.Fatalf("err: %s", err) 965 } 966 967 task.Constraints = append(task.Constraints, 968 &Constraint{ 969 Operand: ConstraintDistinctHosts, 970 }, 971 &Constraint{ 972 Operand: ConstraintDistinctProperty, 973 LTarget: "${meta.rack}", 974 }) 975 976 err = task.Validate(ephemeralDisk) 977 mErr = err.(*multierror.Error) 978 if !strings.Contains(mErr.Errors[0].Error(), "task level: distinct_hosts") { 979 t.Fatalf("err: %s", err) 980 } 981 if !strings.Contains(mErr.Errors[1].Error(), "task level: distinct_property") { 982 t.Fatalf("err: %s", err) 983 } 984 } 985 986 func TestTask_Validate_Services(t *testing.T) { 987 s1 := &Service{ 988 Name: "service-name", 989 PortLabel: "bar", 990 Checks: []*ServiceCheck{ 991 { 992 Name: "check-name", 993 Type: ServiceCheckTCP, 994 Interval: 0 * time.Second, 995 }, 996 { 997 Name: "check-name", 998 Type: ServiceCheckTCP, 999 Timeout: 2 * time.Second, 1000 }, 1001 { 1002 Name: "check-name", 1003 Type: ServiceCheckTCP, 1004 Interval: 1 * time.Second, 1005 }, 1006 }, 1007 } 1008 1009 s2 := &Service{ 1010 Name: "service-name", 1011 PortLabel: "bar", 1012 } 1013 1014 s3 := &Service{ 1015 Name: "service-A", 1016 PortLabel: "a", 1017 } 1018 s4 := &Service{ 1019 Name: "service-A", 1020 PortLabel: "b", 1021 } 1022 1023 ephemeralDisk := DefaultEphemeralDisk() 1024 ephemeralDisk.SizeMB = 200 1025 task := &Task{ 1026 Name: "web", 1027 Driver: "docker", 1028 Resources: &Resources{ 1029 CPU: 100, 1030 MemoryMB: 100, 1031 IOPS: 10, 1032 }, 1033 Services: []*Service{s1, s2}, 1034 } 1035 1036 task1 := &Task{ 1037 Name: "web", 1038 Driver: "docker", 1039 Resources: DefaultResources(), 1040 Services: []*Service{s3, s4}, 1041 LogConfig: DefaultLogConfig(), 1042 } 1043 task1.Resources.Networks = []*NetworkResource{ 1044 &NetworkResource{ 1045 MBits: 10, 1046 DynamicPorts: []Port{ 1047 Port{ 1048 Label: "a", 1049 Value: 1000, 1050 }, 1051 Port{ 1052 Label: "b", 1053 Value: 2000, 1054 }, 1055 }, 1056 }, 1057 } 1058 1059 err := task.Validate(ephemeralDisk) 1060 if err == nil { 1061 t.Fatal("expected an error") 1062 } 1063 1064 if !strings.Contains(err.Error(), "service \"service-name\" is duplicate") { 1065 t.Fatalf("err: %v", err) 1066 } 1067 1068 if !strings.Contains(err.Error(), "check \"check-name\" is duplicate") { 1069 t.Fatalf("err: %v", err) 1070 } 1071 1072 if !strings.Contains(err.Error(), "missing required value interval") { 1073 t.Fatalf("err: %v", err) 1074 } 1075 1076 if !strings.Contains(err.Error(), "cannot be less than") { 1077 t.Fatalf("err: %v", err) 1078 } 1079 1080 if err = task1.Validate(ephemeralDisk); err != nil { 1081 t.Fatalf("err : %v", err) 1082 } 1083 } 1084 1085 func TestTask_Validate_Service_Check(t *testing.T) { 1086 1087 check1 := ServiceCheck{ 1088 Name: "check-name", 1089 Type: ServiceCheckTCP, 1090 Interval: 10 * time.Second, 1091 Timeout: 2 * time.Second, 1092 } 1093 1094 err := check1.validate() 1095 if err != nil { 1096 t.Fatalf("err: %v", err) 1097 } 1098 1099 check1.InitialStatus = "foo" 1100 err = check1.validate() 1101 if err == nil { 1102 t.Fatal("Expected an error") 1103 } 1104 1105 if !strings.Contains(err.Error(), "invalid initial check state (foo)") { 1106 t.Fatalf("err: %v", err) 1107 } 1108 1109 check1.InitialStatus = api.HealthCritical 1110 err = check1.validate() 1111 if err != nil { 1112 t.Fatalf("err: %v", err) 1113 } 1114 1115 check1.InitialStatus = api.HealthPassing 1116 err = check1.validate() 1117 if err != nil { 1118 t.Fatalf("err: %v", err) 1119 } 1120 1121 check1.InitialStatus = "" 1122 err = check1.validate() 1123 if err != nil { 1124 t.Fatalf("err: %v", err) 1125 } 1126 } 1127 1128 func TestTask_Validate_LogConfig(t *testing.T) { 1129 task := &Task{ 1130 LogConfig: DefaultLogConfig(), 1131 } 1132 ephemeralDisk := &EphemeralDisk{ 1133 SizeMB: 1, 1134 } 1135 1136 err := task.Validate(ephemeralDisk) 1137 mErr := err.(*multierror.Error) 1138 if !strings.Contains(mErr.Errors[3].Error(), "log storage") { 1139 t.Fatalf("err: %s", err) 1140 } 1141 } 1142 1143 func TestTask_Validate_Template(t *testing.T) { 1144 1145 bad := &Template{} 1146 task := &Task{ 1147 Templates: []*Template{bad}, 1148 } 1149 ephemeralDisk := &EphemeralDisk{ 1150 SizeMB: 1, 1151 } 1152 1153 err := task.Validate(ephemeralDisk) 1154 if !strings.Contains(err.Error(), "Template 1 validation failed") { 1155 t.Fatalf("err: %s", err) 1156 } 1157 1158 // Have two templates that share the same destination 1159 good := &Template{ 1160 SourcePath: "foo", 1161 DestPath: "local/foo", 1162 ChangeMode: "noop", 1163 } 1164 1165 task.Templates = []*Template{good, good} 1166 err = task.Validate(ephemeralDisk) 1167 if !strings.Contains(err.Error(), "same destination as") { 1168 t.Fatalf("err: %s", err) 1169 } 1170 } 1171 1172 func TestTemplate_Validate(t *testing.T) { 1173 cases := []struct { 1174 Tmpl *Template 1175 Fail bool 1176 ContainsErrs []string 1177 }{ 1178 { 1179 Tmpl: &Template{}, 1180 Fail: true, 1181 ContainsErrs: []string{ 1182 "specify a source path", 1183 "specify a destination", 1184 TemplateChangeModeInvalidError.Error(), 1185 }, 1186 }, 1187 { 1188 Tmpl: &Template{ 1189 Splay: -100, 1190 }, 1191 Fail: true, 1192 ContainsErrs: []string{ 1193 "positive splay", 1194 }, 1195 }, 1196 { 1197 Tmpl: &Template{ 1198 ChangeMode: "foo", 1199 }, 1200 Fail: true, 1201 ContainsErrs: []string{ 1202 TemplateChangeModeInvalidError.Error(), 1203 }, 1204 }, 1205 { 1206 Tmpl: &Template{ 1207 ChangeMode: "signal", 1208 }, 1209 Fail: true, 1210 ContainsErrs: []string{ 1211 "specify signal value", 1212 }, 1213 }, 1214 { 1215 Tmpl: &Template{ 1216 SourcePath: "foo", 1217 DestPath: "../../root", 1218 ChangeMode: "noop", 1219 }, 1220 Fail: true, 1221 ContainsErrs: []string{ 1222 "destination escapes", 1223 }, 1224 }, 1225 { 1226 Tmpl: &Template{ 1227 SourcePath: "foo", 1228 DestPath: "local/foo", 1229 ChangeMode: "noop", 1230 }, 1231 Fail: false, 1232 }, 1233 { 1234 Tmpl: &Template{ 1235 SourcePath: "foo", 1236 DestPath: "local/foo", 1237 ChangeMode: "noop", 1238 Perms: "0444", 1239 }, 1240 Fail: false, 1241 }, 1242 { 1243 Tmpl: &Template{ 1244 SourcePath: "foo", 1245 DestPath: "local/foo", 1246 ChangeMode: "noop", 1247 Perms: "zza", 1248 }, 1249 Fail: true, 1250 ContainsErrs: []string{ 1251 "as octal", 1252 }, 1253 }, 1254 } 1255 1256 for i, c := range cases { 1257 err := c.Tmpl.Validate() 1258 if err != nil { 1259 if !c.Fail { 1260 t.Fatalf("Case %d: shouldn't have failed: %v", i+1, err) 1261 } 1262 1263 e := err.Error() 1264 for _, exp := range c.ContainsErrs { 1265 if !strings.Contains(e, exp) { 1266 t.Fatalf("Cased %d: should have contained error %q: %q", i+1, exp, e) 1267 } 1268 } 1269 } else if c.Fail { 1270 t.Fatalf("Case %d: should have failed: %v", i+1, err) 1271 } 1272 } 1273 } 1274 1275 func TestConstraint_Validate(t *testing.T) { 1276 c := &Constraint{} 1277 err := c.Validate() 1278 mErr := err.(*multierror.Error) 1279 if !strings.Contains(mErr.Errors[0].Error(), "Missing constraint operand") { 1280 t.Fatalf("err: %s", err) 1281 } 1282 1283 c = &Constraint{ 1284 LTarget: "$attr.kernel.name", 1285 RTarget: "linux", 1286 Operand: "=", 1287 } 1288 err = c.Validate() 1289 if err != nil { 1290 t.Fatalf("err: %v", err) 1291 } 1292 1293 // Perform additional regexp validation 1294 c.Operand = ConstraintRegex 1295 c.RTarget = "(foo" 1296 err = c.Validate() 1297 mErr = err.(*multierror.Error) 1298 if !strings.Contains(mErr.Errors[0].Error(), "missing closing") { 1299 t.Fatalf("err: %s", err) 1300 } 1301 1302 // Perform version validation 1303 c.Operand = ConstraintVersion 1304 c.RTarget = "~> foo" 1305 err = c.Validate() 1306 mErr = err.(*multierror.Error) 1307 if !strings.Contains(mErr.Errors[0].Error(), "Malformed constraint") { 1308 t.Fatalf("err: %s", err) 1309 } 1310 } 1311 1312 func TestUpdateStrategy_Validate(t *testing.T) { 1313 u := &UpdateStrategy{ 1314 MaxParallel: -1, 1315 HealthCheck: "foo", 1316 MinHealthyTime: -10, 1317 HealthyDeadline: -10, 1318 AutoRevert: false, 1319 Canary: -1, 1320 } 1321 1322 err := u.Validate() 1323 mErr := err.(*multierror.Error) 1324 if !strings.Contains(mErr.Errors[0].Error(), "Invalid health check given") { 1325 t.Fatalf("err: %s", err) 1326 } 1327 if !strings.Contains(mErr.Errors[1].Error(), "Max parallel can not be less than zero") { 1328 t.Fatalf("err: %s", err) 1329 } 1330 if !strings.Contains(mErr.Errors[2].Error(), "Canary count can not be less than zero") { 1331 t.Fatalf("err: %s", err) 1332 } 1333 if !strings.Contains(mErr.Errors[3].Error(), "Minimum healthy time may not be less than zero") { 1334 t.Fatalf("err: %s", err) 1335 } 1336 if !strings.Contains(mErr.Errors[4].Error(), "Healthy deadline must be greater than zero") { 1337 t.Fatalf("err: %s", err) 1338 } 1339 } 1340 1341 func TestResource_NetIndex(t *testing.T) { 1342 r := &Resources{ 1343 Networks: []*NetworkResource{ 1344 &NetworkResource{Device: "eth0"}, 1345 &NetworkResource{Device: "lo0"}, 1346 &NetworkResource{Device: ""}, 1347 }, 1348 } 1349 if idx := r.NetIndex(&NetworkResource{Device: "eth0"}); idx != 0 { 1350 t.Fatalf("Bad: %d", idx) 1351 } 1352 if idx := r.NetIndex(&NetworkResource{Device: "lo0"}); idx != 1 { 1353 t.Fatalf("Bad: %d", idx) 1354 } 1355 if idx := r.NetIndex(&NetworkResource{Device: "eth1"}); idx != -1 { 1356 t.Fatalf("Bad: %d", idx) 1357 } 1358 } 1359 1360 func TestResource_Superset(t *testing.T) { 1361 r1 := &Resources{ 1362 CPU: 2000, 1363 MemoryMB: 2048, 1364 DiskMB: 10000, 1365 IOPS: 100, 1366 } 1367 r2 := &Resources{ 1368 CPU: 2000, 1369 MemoryMB: 1024, 1370 DiskMB: 5000, 1371 IOPS: 50, 1372 } 1373 1374 if s, _ := r1.Superset(r1); !s { 1375 t.Fatalf("bad") 1376 } 1377 if s, _ := r1.Superset(r2); !s { 1378 t.Fatalf("bad") 1379 } 1380 if s, _ := r2.Superset(r1); s { 1381 t.Fatalf("bad") 1382 } 1383 if s, _ := r2.Superset(r2); !s { 1384 t.Fatalf("bad") 1385 } 1386 } 1387 1388 func TestResource_Add(t *testing.T) { 1389 r1 := &Resources{ 1390 CPU: 2000, 1391 MemoryMB: 2048, 1392 DiskMB: 10000, 1393 IOPS: 100, 1394 Networks: []*NetworkResource{ 1395 &NetworkResource{ 1396 CIDR: "10.0.0.0/8", 1397 MBits: 100, 1398 ReservedPorts: []Port{{"ssh", 22}}, 1399 }, 1400 }, 1401 } 1402 r2 := &Resources{ 1403 CPU: 2000, 1404 MemoryMB: 1024, 1405 DiskMB: 5000, 1406 IOPS: 50, 1407 Networks: []*NetworkResource{ 1408 &NetworkResource{ 1409 IP: "10.0.0.1", 1410 MBits: 50, 1411 ReservedPorts: []Port{{"web", 80}}, 1412 }, 1413 }, 1414 } 1415 1416 err := r1.Add(r2) 1417 if err != nil { 1418 t.Fatalf("Err: %v", err) 1419 } 1420 1421 expect := &Resources{ 1422 CPU: 3000, 1423 MemoryMB: 3072, 1424 DiskMB: 15000, 1425 IOPS: 150, 1426 Networks: []*NetworkResource{ 1427 &NetworkResource{ 1428 CIDR: "10.0.0.0/8", 1429 MBits: 150, 1430 ReservedPorts: []Port{{"ssh", 22}, {"web", 80}}, 1431 }, 1432 }, 1433 } 1434 1435 if !reflect.DeepEqual(expect.Networks, r1.Networks) { 1436 t.Fatalf("bad: %#v %#v", expect, r1) 1437 } 1438 } 1439 1440 func TestResource_Add_Network(t *testing.T) { 1441 r1 := &Resources{} 1442 r2 := &Resources{ 1443 Networks: []*NetworkResource{ 1444 &NetworkResource{ 1445 MBits: 50, 1446 DynamicPorts: []Port{{"http", 0}, {"https", 0}}, 1447 }, 1448 }, 1449 } 1450 r3 := &Resources{ 1451 Networks: []*NetworkResource{ 1452 &NetworkResource{ 1453 MBits: 25, 1454 DynamicPorts: []Port{{"admin", 0}}, 1455 }, 1456 }, 1457 } 1458 1459 err := r1.Add(r2) 1460 if err != nil { 1461 t.Fatalf("Err: %v", err) 1462 } 1463 err = r1.Add(r3) 1464 if err != nil { 1465 t.Fatalf("Err: %v", err) 1466 } 1467 1468 expect := &Resources{ 1469 Networks: []*NetworkResource{ 1470 &NetworkResource{ 1471 MBits: 75, 1472 DynamicPorts: []Port{{"http", 0}, {"https", 0}, {"admin", 0}}, 1473 }, 1474 }, 1475 } 1476 1477 if !reflect.DeepEqual(expect.Networks, r1.Networks) { 1478 t.Fatalf("bad: %#v %#v", expect.Networks[0], r1.Networks[0]) 1479 } 1480 } 1481 1482 func TestEncodeDecode(t *testing.T) { 1483 type FooRequest struct { 1484 Foo string 1485 Bar int 1486 Baz bool 1487 } 1488 arg := &FooRequest{ 1489 Foo: "test", 1490 Bar: 42, 1491 Baz: true, 1492 } 1493 buf, err := Encode(1, arg) 1494 if err != nil { 1495 t.Fatalf("err: %v", err) 1496 } 1497 1498 var out FooRequest 1499 err = Decode(buf[1:], &out) 1500 if err != nil { 1501 t.Fatalf("err: %v", err) 1502 } 1503 1504 if !reflect.DeepEqual(arg, &out) { 1505 t.Fatalf("bad: %#v %#v", arg, out) 1506 } 1507 } 1508 1509 func BenchmarkEncodeDecode(b *testing.B) { 1510 job := testJob() 1511 1512 for i := 0; i < b.N; i++ { 1513 buf, err := Encode(1, job) 1514 if err != nil { 1515 b.Fatalf("err: %v", err) 1516 } 1517 1518 var out Job 1519 err = Decode(buf[1:], &out) 1520 if err != nil { 1521 b.Fatalf("err: %v", err) 1522 } 1523 } 1524 } 1525 1526 func TestInvalidServiceCheck(t *testing.T) { 1527 s := Service{ 1528 Name: "service-name", 1529 PortLabel: "bar", 1530 Checks: []*ServiceCheck{ 1531 { 1532 Name: "check-name", 1533 Type: "lol", 1534 }, 1535 }, 1536 } 1537 if err := s.Validate(); err == nil { 1538 t.Fatalf("Service should be invalid (invalid type)") 1539 } 1540 1541 s = Service{ 1542 Name: "service.name", 1543 PortLabel: "bar", 1544 } 1545 if err := s.ValidateName(s.Name); err == nil { 1546 t.Fatalf("Service should be invalid (contains a dot): %v", err) 1547 } 1548 1549 s = Service{ 1550 Name: "-my-service", 1551 PortLabel: "bar", 1552 } 1553 if err := s.Validate(); err == nil { 1554 t.Fatalf("Service should be invalid (begins with a hyphen): %v", err) 1555 } 1556 1557 s = Service{ 1558 Name: "my-service-${NOMAD_META_FOO}", 1559 PortLabel: "bar", 1560 } 1561 if err := s.Validate(); err != nil { 1562 t.Fatalf("Service should be valid: %v", err) 1563 } 1564 1565 s = Service{ 1566 Name: "abcdef0123456789-abcdef0123456789-abcdef0123456789-abcdef0123456", 1567 PortLabel: "bar", 1568 } 1569 if err := s.ValidateName(s.Name); err == nil { 1570 t.Fatalf("Service should be invalid (too long): %v", err) 1571 } 1572 1573 s = Service{ 1574 Name: "service-name", 1575 Checks: []*ServiceCheck{ 1576 { 1577 Name: "check-tcp", 1578 Type: ServiceCheckTCP, 1579 Interval: 5 * time.Second, 1580 Timeout: 2 * time.Second, 1581 }, 1582 { 1583 Name: "check-http", 1584 Type: ServiceCheckHTTP, 1585 Path: "/foo", 1586 Interval: 5 * time.Second, 1587 Timeout: 2 * time.Second, 1588 }, 1589 }, 1590 } 1591 if err := s.Validate(); err == nil { 1592 t.Fatalf("service should be invalid (tcp/http checks with no port): %v", err) 1593 } 1594 1595 s = Service{ 1596 Name: "service-name", 1597 Checks: []*ServiceCheck{ 1598 { 1599 Name: "check-script", 1600 Type: ServiceCheckScript, 1601 Command: "/bin/date", 1602 Interval: 5 * time.Second, 1603 Timeout: 2 * time.Second, 1604 }, 1605 }, 1606 } 1607 if err := s.Validate(); err != nil { 1608 t.Fatalf("un-expected error: %v", err) 1609 } 1610 } 1611 1612 func TestDistinctCheckID(t *testing.T) { 1613 c1 := ServiceCheck{ 1614 Name: "web-health", 1615 Type: "http", 1616 Path: "/health", 1617 Interval: 2 * time.Second, 1618 Timeout: 3 * time.Second, 1619 } 1620 c2 := ServiceCheck{ 1621 Name: "web-health", 1622 Type: "http", 1623 Path: "/health1", 1624 Interval: 2 * time.Second, 1625 Timeout: 3 * time.Second, 1626 } 1627 1628 c3 := ServiceCheck{ 1629 Name: "web-health", 1630 Type: "http", 1631 Path: "/health", 1632 Interval: 4 * time.Second, 1633 Timeout: 3 * time.Second, 1634 } 1635 serviceID := "123" 1636 c1Hash := c1.Hash(serviceID) 1637 c2Hash := c2.Hash(serviceID) 1638 c3Hash := c3.Hash(serviceID) 1639 1640 if c1Hash == c2Hash || c1Hash == c3Hash || c3Hash == c2Hash { 1641 t.Fatalf("Checks need to be uniq c1: %s, c2: %s, c3: %s", c1Hash, c2Hash, c3Hash) 1642 } 1643 1644 } 1645 1646 func TestService_Canonicalize(t *testing.T) { 1647 job := "example" 1648 taskGroup := "cache" 1649 task := "redis" 1650 1651 s := Service{ 1652 Name: "${TASK}-db", 1653 } 1654 1655 s.Canonicalize(job, taskGroup, task) 1656 if s.Name != "redis-db" { 1657 t.Fatalf("Expected name: %v, Actual: %v", "redis-db", s.Name) 1658 } 1659 1660 s.Name = "db" 1661 s.Canonicalize(job, taskGroup, task) 1662 if s.Name != "db" { 1663 t.Fatalf("Expected name: %v, Actual: %v", "redis-db", s.Name) 1664 } 1665 1666 s.Name = "${JOB}-${TASKGROUP}-${TASK}-db" 1667 s.Canonicalize(job, taskGroup, task) 1668 if s.Name != "example-cache-redis-db" { 1669 t.Fatalf("Expected name: %v, Actual: %v", "expample-cache-redis-db", s.Name) 1670 } 1671 1672 s.Name = "${BASE}-db" 1673 s.Canonicalize(job, taskGroup, task) 1674 if s.Name != "example-cache-redis-db" { 1675 t.Fatalf("Expected name: %v, Actual: %v", "expample-cache-redis-db", s.Name) 1676 } 1677 1678 } 1679 1680 func TestJob_ExpandServiceNames(t *testing.T) { 1681 j := &Job{ 1682 Name: "my-job", 1683 TaskGroups: []*TaskGroup{ 1684 &TaskGroup{ 1685 Name: "web", 1686 Tasks: []*Task{ 1687 { 1688 Name: "frontend", 1689 Services: []*Service{ 1690 { 1691 Name: "${BASE}-default", 1692 }, 1693 { 1694 Name: "jmx", 1695 }, 1696 }, 1697 }, 1698 }, 1699 }, 1700 &TaskGroup{ 1701 Name: "admin", 1702 Tasks: []*Task{ 1703 { 1704 Name: "admin-web", 1705 }, 1706 }, 1707 }, 1708 }, 1709 } 1710 1711 j.Canonicalize() 1712 1713 service1Name := j.TaskGroups[0].Tasks[0].Services[0].Name 1714 if service1Name != "my-job-web-frontend-default" { 1715 t.Fatalf("Expected Service Name: %s, Actual: %s", "my-job-web-frontend-default", service1Name) 1716 } 1717 1718 service2Name := j.TaskGroups[0].Tasks[0].Services[1].Name 1719 if service2Name != "jmx" { 1720 t.Fatalf("Expected Service Name: %s, Actual: %s", "jmx", service2Name) 1721 } 1722 1723 } 1724 1725 func TestPeriodicConfig_EnabledInvalid(t *testing.T) { 1726 // Create a config that is enabled but with no interval specified. 1727 p := &PeriodicConfig{Enabled: true} 1728 if err := p.Validate(); err == nil { 1729 t.Fatal("Enabled PeriodicConfig with no spec or type shouldn't be valid") 1730 } 1731 1732 // Create a config that is enabled, with a spec but no type specified. 1733 p = &PeriodicConfig{Enabled: true, Spec: "foo"} 1734 if err := p.Validate(); err == nil { 1735 t.Fatal("Enabled PeriodicConfig with no spec type shouldn't be valid") 1736 } 1737 1738 // Create a config that is enabled, with a spec type but no spec specified. 1739 p = &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron} 1740 if err := p.Validate(); err == nil { 1741 t.Fatal("Enabled PeriodicConfig with no spec shouldn't be valid") 1742 } 1743 1744 // Create a config that is enabled, with a bad time zone. 1745 p = &PeriodicConfig{Enabled: true, TimeZone: "FOO"} 1746 if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "time zone") { 1747 t.Fatalf("Enabled PeriodicConfig with bad time zone shouldn't be valid: %v", err) 1748 } 1749 } 1750 1751 func TestPeriodicConfig_InvalidCron(t *testing.T) { 1752 specs := []string{"foo", "* *", "@foo"} 1753 for _, spec := range specs { 1754 p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec} 1755 p.Canonicalize() 1756 if err := p.Validate(); err == nil { 1757 t.Fatal("Invalid cron spec") 1758 } 1759 } 1760 } 1761 1762 func TestPeriodicConfig_ValidCron(t *testing.T) { 1763 specs := []string{"0 0 29 2 *", "@hourly", "0 0-15 * * *"} 1764 for _, spec := range specs { 1765 p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec} 1766 p.Canonicalize() 1767 if err := p.Validate(); err != nil { 1768 t.Fatal("Passed valid cron") 1769 } 1770 } 1771 } 1772 1773 func TestPeriodicConfig_NextCron(t *testing.T) { 1774 from := time.Date(2009, time.November, 10, 23, 22, 30, 0, time.UTC) 1775 specs := []string{"0 0 29 2 * 1980", "*/5 * * * *"} 1776 expected := []time.Time{time.Time{}, time.Date(2009, time.November, 10, 23, 25, 0, 0, time.UTC)} 1777 for i, spec := range specs { 1778 p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec} 1779 p.Canonicalize() 1780 n := p.Next(from) 1781 if expected[i] != n { 1782 t.Fatalf("Next(%v) returned %v; want %v", from, n, expected[i]) 1783 } 1784 } 1785 } 1786 1787 func TestPeriodicConfig_ValidTimeZone(t *testing.T) { 1788 zones := []string{"Africa/Abidjan", "America/Chicago", "Europe/Minsk", "UTC"} 1789 for _, zone := range zones { 1790 p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: "0 0 29 2 * 1980", TimeZone: zone} 1791 p.Canonicalize() 1792 if err := p.Validate(); err != nil { 1793 t.Fatalf("Valid tz errored: %v", err) 1794 } 1795 } 1796 } 1797 1798 func TestPeriodicConfig_DST(t *testing.T) { 1799 // On Sun, Mar 12, 2:00 am 2017: +1 hour UTC 1800 p := &PeriodicConfig{ 1801 Enabled: true, 1802 SpecType: PeriodicSpecCron, 1803 Spec: "0 2 11-12 3 * 2017", 1804 TimeZone: "America/Los_Angeles", 1805 } 1806 p.Canonicalize() 1807 1808 t1 := time.Date(2017, time.March, 11, 1, 0, 0, 0, p.location) 1809 t2 := time.Date(2017, time.March, 12, 1, 0, 0, 0, p.location) 1810 1811 // E1 is an 8 hour adjustment, E2 is a 7 hour adjustment 1812 e1 := time.Date(2017, time.March, 11, 10, 0, 0, 0, time.UTC) 1813 e2 := time.Date(2017, time.March, 12, 9, 0, 0, 0, time.UTC) 1814 1815 n1 := p.Next(t1).UTC() 1816 n2 := p.Next(t2).UTC() 1817 1818 if !reflect.DeepEqual(e1, n1) { 1819 t.Fatalf("Got %v; want %v", n1, e1) 1820 } 1821 if !reflect.DeepEqual(e2, n2) { 1822 t.Fatalf("Got %v; want %v", n1, e1) 1823 } 1824 } 1825 1826 func TestRestartPolicy_Validate(t *testing.T) { 1827 // Policy with acceptable restart options passes 1828 p := &RestartPolicy{ 1829 Mode: RestartPolicyModeFail, 1830 Attempts: 0, 1831 Interval: 5 * time.Second, 1832 } 1833 if err := p.Validate(); err != nil { 1834 t.Fatalf("err: %v", err) 1835 } 1836 1837 // Policy with ambiguous restart options fails 1838 p = &RestartPolicy{ 1839 Mode: RestartPolicyModeDelay, 1840 Attempts: 0, 1841 Interval: 5 * time.Second, 1842 } 1843 if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "ambiguous") { 1844 t.Fatalf("expect ambiguity error, got: %v", err) 1845 } 1846 1847 // Bad policy mode fails 1848 p = &RestartPolicy{ 1849 Mode: "nope", 1850 Attempts: 1, 1851 Interval: 5 * time.Second, 1852 } 1853 if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "mode") { 1854 t.Fatalf("expect mode error, got: %v", err) 1855 } 1856 1857 // Fails when attempts*delay does not fit inside interval 1858 p = &RestartPolicy{ 1859 Mode: RestartPolicyModeDelay, 1860 Attempts: 3, 1861 Delay: 5 * time.Second, 1862 Interval: 5 * time.Second, 1863 } 1864 if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "can't restart") { 1865 t.Fatalf("expect restart interval error, got: %v", err) 1866 } 1867 1868 // Fails when interval is to small 1869 p = &RestartPolicy{ 1870 Mode: RestartPolicyModeDelay, 1871 Attempts: 3, 1872 Delay: 5 * time.Second, 1873 Interval: 2 * time.Second, 1874 } 1875 if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "Interval can not be less than") { 1876 t.Fatalf("expect interval too small error, got: %v", err) 1877 } 1878 } 1879 1880 func TestAllocation_Index(t *testing.T) { 1881 a1 := Allocation{ 1882 Name: "example.cache[1]", 1883 TaskGroup: "cache", 1884 JobID: "example", 1885 Job: &Job{ 1886 ID: "example", 1887 TaskGroups: []*TaskGroup{{Name: "cache"}}}, 1888 } 1889 e1 := uint(1) 1890 a2 := a1.Copy() 1891 a2.Name = "example.cache[713127]" 1892 e2 := uint(713127) 1893 1894 if a1.Index() != e1 || a2.Index() != e2 { 1895 t.Fatalf("Got %d and %d", a1.Index(), a2.Index()) 1896 } 1897 } 1898 1899 func TestTaskArtifact_Validate_Source(t *testing.T) { 1900 valid := &TaskArtifact{GetterSource: "google.com"} 1901 if err := valid.Validate(); err != nil { 1902 t.Fatalf("unexpected error: %v", err) 1903 } 1904 } 1905 1906 func TestTaskArtifact_Validate_Dest(t *testing.T) { 1907 valid := &TaskArtifact{GetterSource: "google.com"} 1908 if err := valid.Validate(); err != nil { 1909 t.Fatalf("unexpected error: %v", err) 1910 } 1911 1912 valid.RelativeDest = "local/" 1913 if err := valid.Validate(); err != nil { 1914 t.Fatalf("unexpected error: %v", err) 1915 } 1916 1917 valid.RelativeDest = "local/.." 1918 if err := valid.Validate(); err != nil { 1919 t.Fatalf("unexpected error: %v", err) 1920 } 1921 1922 valid.RelativeDest = "local/../../.." 1923 if err := valid.Validate(); err == nil { 1924 t.Fatalf("expected error: %v", err) 1925 } 1926 } 1927 1928 func TestAllocation_ShouldMigrate(t *testing.T) { 1929 alloc := Allocation{ 1930 TaskGroup: "foo", 1931 Job: &Job{ 1932 TaskGroups: []*TaskGroup{ 1933 { 1934 Name: "foo", 1935 EphemeralDisk: &EphemeralDisk{ 1936 Migrate: true, 1937 Sticky: true, 1938 }, 1939 }, 1940 }, 1941 }, 1942 } 1943 1944 if !alloc.ShouldMigrate() { 1945 t.Fatalf("bad: %v", alloc) 1946 } 1947 1948 alloc1 := Allocation{ 1949 TaskGroup: "foo", 1950 Job: &Job{ 1951 TaskGroups: []*TaskGroup{ 1952 { 1953 Name: "foo", 1954 EphemeralDisk: &EphemeralDisk{}, 1955 }, 1956 }, 1957 }, 1958 } 1959 1960 if alloc1.ShouldMigrate() { 1961 t.Fatalf("bad: %v", alloc) 1962 } 1963 1964 alloc2 := Allocation{ 1965 TaskGroup: "foo", 1966 Job: &Job{ 1967 TaskGroups: []*TaskGroup{ 1968 { 1969 Name: "foo", 1970 EphemeralDisk: &EphemeralDisk{ 1971 Sticky: false, 1972 Migrate: true, 1973 }, 1974 }, 1975 }, 1976 }, 1977 } 1978 1979 if alloc2.ShouldMigrate() { 1980 t.Fatalf("bad: %v", alloc) 1981 } 1982 1983 alloc3 := Allocation{ 1984 TaskGroup: "foo", 1985 Job: &Job{ 1986 TaskGroups: []*TaskGroup{ 1987 { 1988 Name: "foo", 1989 }, 1990 }, 1991 }, 1992 } 1993 1994 if alloc3.ShouldMigrate() { 1995 t.Fatalf("bad: %v", alloc) 1996 } 1997 } 1998 1999 func TestTaskArtifact_Validate_Checksum(t *testing.T) { 2000 cases := []struct { 2001 Input *TaskArtifact 2002 Err bool 2003 }{ 2004 { 2005 &TaskArtifact{ 2006 GetterSource: "foo.com", 2007 GetterOptions: map[string]string{ 2008 "checksum": "no-type", 2009 }, 2010 }, 2011 true, 2012 }, 2013 { 2014 &TaskArtifact{ 2015 GetterSource: "foo.com", 2016 GetterOptions: map[string]string{ 2017 "checksum": "md5:toosmall", 2018 }, 2019 }, 2020 true, 2021 }, 2022 { 2023 &TaskArtifact{ 2024 GetterSource: "foo.com", 2025 GetterOptions: map[string]string{ 2026 "checksum": "invalid:type", 2027 }, 2028 }, 2029 true, 2030 }, 2031 } 2032 2033 for i, tc := range cases { 2034 err := tc.Input.Validate() 2035 if (err != nil) != tc.Err { 2036 t.Fatalf("case %d: %v", i, err) 2037 continue 2038 } 2039 } 2040 } 2041 2042 func TestAllocation_Terminated(t *testing.T) { 2043 type desiredState struct { 2044 ClientStatus string 2045 DesiredStatus string 2046 Terminated bool 2047 } 2048 2049 harness := []desiredState{ 2050 { 2051 ClientStatus: AllocClientStatusPending, 2052 DesiredStatus: AllocDesiredStatusStop, 2053 Terminated: false, 2054 }, 2055 { 2056 ClientStatus: AllocClientStatusRunning, 2057 DesiredStatus: AllocDesiredStatusStop, 2058 Terminated: false, 2059 }, 2060 { 2061 ClientStatus: AllocClientStatusFailed, 2062 DesiredStatus: AllocDesiredStatusStop, 2063 Terminated: true, 2064 }, 2065 { 2066 ClientStatus: AllocClientStatusFailed, 2067 DesiredStatus: AllocDesiredStatusRun, 2068 Terminated: true, 2069 }, 2070 } 2071 2072 for _, state := range harness { 2073 alloc := Allocation{} 2074 alloc.DesiredStatus = state.DesiredStatus 2075 alloc.ClientStatus = state.ClientStatus 2076 if alloc.Terminated() != state.Terminated { 2077 t.Fatalf("expected: %v, actual: %v", state.Terminated, alloc.Terminated()) 2078 } 2079 } 2080 } 2081 2082 func TestVault_Validate(t *testing.T) { 2083 v := &Vault{ 2084 Env: true, 2085 ChangeMode: VaultChangeModeNoop, 2086 } 2087 2088 if err := v.Validate(); err == nil || !strings.Contains(err.Error(), "Policy list") { 2089 t.Fatalf("Expected policy list empty error") 2090 } 2091 2092 v.Policies = []string{"foo", "root"} 2093 v.ChangeMode = VaultChangeModeSignal 2094 2095 err := v.Validate() 2096 if err == nil { 2097 t.Fatalf("Expected validation errors") 2098 } 2099 2100 if !strings.Contains(err.Error(), "Signal must") { 2101 t.Fatalf("Expected signal empty error") 2102 } 2103 if !strings.Contains(err.Error(), "root") { 2104 t.Fatalf("Expected root error") 2105 } 2106 } 2107 2108 func TestParameterizedJobConfig_Validate(t *testing.T) { 2109 d := &ParameterizedJobConfig{ 2110 Payload: "foo", 2111 } 2112 2113 if err := d.Validate(); err == nil || !strings.Contains(err.Error(), "payload") { 2114 t.Fatalf("Expected unknown payload requirement: %v", err) 2115 } 2116 2117 d.Payload = DispatchPayloadOptional 2118 d.MetaOptional = []string{"foo", "bar"} 2119 d.MetaRequired = []string{"bar", "baz"} 2120 2121 if err := d.Validate(); err == nil || !strings.Contains(err.Error(), "disjoint") { 2122 t.Fatalf("Expected meta not being disjoint error: %v", err) 2123 } 2124 } 2125 2126 func TestParameterizedJobConfig_Validate_NonBatch(t *testing.T) { 2127 job := testJob() 2128 job.ParameterizedJob = &ParameterizedJobConfig{ 2129 Payload: DispatchPayloadOptional, 2130 } 2131 job.Type = JobTypeSystem 2132 2133 if err := job.Validate(); err == nil || !strings.Contains(err.Error(), "only be used with") { 2134 t.Fatalf("Expected bad scheduler tpye: %v", err) 2135 } 2136 } 2137 2138 func TestParameterizedJobConfig_Canonicalize(t *testing.T) { 2139 d := &ParameterizedJobConfig{} 2140 d.Canonicalize() 2141 if d.Payload != DispatchPayloadOptional { 2142 t.Fatalf("Canonicalize failed") 2143 } 2144 } 2145 2146 func TestDispatchPayloadConfig_Validate(t *testing.T) { 2147 d := &DispatchPayloadConfig{ 2148 File: "foo", 2149 } 2150 2151 // task/local/haha 2152 if err := d.Validate(); err != nil { 2153 t.Fatalf("bad: %v", err) 2154 } 2155 2156 // task/haha 2157 d.File = "../haha" 2158 if err := d.Validate(); err != nil { 2159 t.Fatalf("bad: %v", err) 2160 } 2161 2162 // ../haha 2163 d.File = "../../../haha" 2164 if err := d.Validate(); err == nil { 2165 t.Fatalf("bad: %v", err) 2166 } 2167 } 2168 2169 func TestIsRecoverable(t *testing.T) { 2170 if IsRecoverable(nil) { 2171 t.Errorf("nil should not be recoverable") 2172 } 2173 if IsRecoverable(NewRecoverableError(nil, true)) { 2174 t.Errorf("NewRecoverableError(nil, true) should not be recoverable") 2175 } 2176 if IsRecoverable(fmt.Errorf("i promise im recoverable")) { 2177 t.Errorf("Custom errors should not be recoverable") 2178 } 2179 if IsRecoverable(NewRecoverableError(fmt.Errorf(""), false)) { 2180 t.Errorf("Explicitly unrecoverable errors should not be recoverable") 2181 } 2182 if !IsRecoverable(NewRecoverableError(fmt.Errorf(""), true)) { 2183 t.Errorf("Explicitly recoverable errors *should* be recoverable") 2184 } 2185 }