github.com/jrxfive/nomad@v0.6.1-0.20170802162750-1fef470e89bf/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 // Env templates can't use signals 1172 task.Templates = []*Template{ 1173 { 1174 Envvars: true, 1175 ChangeMode: "signal", 1176 }, 1177 } 1178 1179 err = task.Validate(ephemeralDisk) 1180 if err == nil { 1181 t.Fatalf("expected error from Template.Validate") 1182 } 1183 if expected := "cannot use signals"; !strings.Contains(err.Error(), expected) { 1184 t.Errorf("expected to find %q but found %v", expected, err) 1185 } 1186 } 1187 1188 func TestTemplate_Validate(t *testing.T) { 1189 cases := []struct { 1190 Tmpl *Template 1191 Fail bool 1192 ContainsErrs []string 1193 }{ 1194 { 1195 Tmpl: &Template{}, 1196 Fail: true, 1197 ContainsErrs: []string{ 1198 "specify a source path", 1199 "specify a destination", 1200 TemplateChangeModeInvalidError.Error(), 1201 }, 1202 }, 1203 { 1204 Tmpl: &Template{ 1205 Splay: -100, 1206 }, 1207 Fail: true, 1208 ContainsErrs: []string{ 1209 "positive splay", 1210 }, 1211 }, 1212 { 1213 Tmpl: &Template{ 1214 ChangeMode: "foo", 1215 }, 1216 Fail: true, 1217 ContainsErrs: []string{ 1218 TemplateChangeModeInvalidError.Error(), 1219 }, 1220 }, 1221 { 1222 Tmpl: &Template{ 1223 ChangeMode: "signal", 1224 }, 1225 Fail: true, 1226 ContainsErrs: []string{ 1227 "specify signal value", 1228 }, 1229 }, 1230 { 1231 Tmpl: &Template{ 1232 SourcePath: "foo", 1233 DestPath: "../../root", 1234 ChangeMode: "noop", 1235 }, 1236 Fail: true, 1237 ContainsErrs: []string{ 1238 "destination escapes", 1239 }, 1240 }, 1241 { 1242 Tmpl: &Template{ 1243 SourcePath: "foo", 1244 DestPath: "local/foo", 1245 ChangeMode: "noop", 1246 }, 1247 Fail: false, 1248 }, 1249 { 1250 Tmpl: &Template{ 1251 SourcePath: "foo", 1252 DestPath: "local/foo", 1253 ChangeMode: "noop", 1254 Perms: "0444", 1255 }, 1256 Fail: false, 1257 }, 1258 { 1259 Tmpl: &Template{ 1260 SourcePath: "foo", 1261 DestPath: "local/foo", 1262 ChangeMode: "noop", 1263 Perms: "zza", 1264 }, 1265 Fail: true, 1266 ContainsErrs: []string{ 1267 "as octal", 1268 }, 1269 }, 1270 } 1271 1272 for i, c := range cases { 1273 err := c.Tmpl.Validate() 1274 if err != nil { 1275 if !c.Fail { 1276 t.Fatalf("Case %d: shouldn't have failed: %v", i+1, err) 1277 } 1278 1279 e := err.Error() 1280 for _, exp := range c.ContainsErrs { 1281 if !strings.Contains(e, exp) { 1282 t.Fatalf("Cased %d: should have contained error %q: %q", i+1, exp, e) 1283 } 1284 } 1285 } else if c.Fail { 1286 t.Fatalf("Case %d: should have failed: %v", i+1, err) 1287 } 1288 } 1289 } 1290 1291 func TestConstraint_Validate(t *testing.T) { 1292 c := &Constraint{} 1293 err := c.Validate() 1294 mErr := err.(*multierror.Error) 1295 if !strings.Contains(mErr.Errors[0].Error(), "Missing constraint operand") { 1296 t.Fatalf("err: %s", err) 1297 } 1298 1299 c = &Constraint{ 1300 LTarget: "$attr.kernel.name", 1301 RTarget: "linux", 1302 Operand: "=", 1303 } 1304 err = c.Validate() 1305 if err != nil { 1306 t.Fatalf("err: %v", err) 1307 } 1308 1309 // Perform additional regexp validation 1310 c.Operand = ConstraintRegex 1311 c.RTarget = "(foo" 1312 err = c.Validate() 1313 mErr = err.(*multierror.Error) 1314 if !strings.Contains(mErr.Errors[0].Error(), "missing closing") { 1315 t.Fatalf("err: %s", err) 1316 } 1317 1318 // Perform version validation 1319 c.Operand = ConstraintVersion 1320 c.RTarget = "~> foo" 1321 err = c.Validate() 1322 mErr = err.(*multierror.Error) 1323 if !strings.Contains(mErr.Errors[0].Error(), "Malformed constraint") { 1324 t.Fatalf("err: %s", err) 1325 } 1326 1327 // Perform distinct_property validation 1328 c.Operand = ConstraintDistinctProperty 1329 c.RTarget = "0" 1330 err = c.Validate() 1331 mErr = err.(*multierror.Error) 1332 if !strings.Contains(mErr.Errors[0].Error(), "count of 1 or greater") { 1333 t.Fatalf("err: %s", err) 1334 } 1335 1336 c.RTarget = "-1" 1337 err = c.Validate() 1338 mErr = err.(*multierror.Error) 1339 if !strings.Contains(mErr.Errors[0].Error(), "to uint64") { 1340 t.Fatalf("err: %s", err) 1341 } 1342 1343 // Perform distinct_hosts validation 1344 c.Operand = ConstraintDistinctHosts 1345 c.RTarget = "foo" 1346 err = c.Validate() 1347 mErr = err.(*multierror.Error) 1348 if !strings.Contains(mErr.Errors[0].Error(), "doesn't allow RTarget") { 1349 t.Fatalf("err: %s", err) 1350 } 1351 if !strings.Contains(mErr.Errors[1].Error(), "doesn't allow LTarget") { 1352 t.Fatalf("err: %s", err) 1353 } 1354 1355 // Perform set_contains validation 1356 c.Operand = ConstraintSetContains 1357 c.RTarget = "" 1358 err = c.Validate() 1359 mErr = err.(*multierror.Error) 1360 if !strings.Contains(mErr.Errors[0].Error(), "requires an RTarget") { 1361 t.Fatalf("err: %s", err) 1362 } 1363 1364 // Perform LTarget validation 1365 c.Operand = ConstraintRegex 1366 c.RTarget = "foo" 1367 c.LTarget = "" 1368 err = c.Validate() 1369 mErr = err.(*multierror.Error) 1370 if !strings.Contains(mErr.Errors[0].Error(), "No LTarget") { 1371 t.Fatalf("err: %s", err) 1372 } 1373 1374 // Perform constraint type validation 1375 c.Operand = "foo" 1376 err = c.Validate() 1377 mErr = err.(*multierror.Error) 1378 if !strings.Contains(mErr.Errors[0].Error(), "Unknown constraint type") { 1379 t.Fatalf("err: %s", err) 1380 } 1381 } 1382 1383 func TestUpdateStrategy_Validate(t *testing.T) { 1384 u := &UpdateStrategy{ 1385 MaxParallel: -1, 1386 HealthCheck: "foo", 1387 MinHealthyTime: -10, 1388 HealthyDeadline: -15, 1389 AutoRevert: false, 1390 Canary: -1, 1391 } 1392 1393 err := u.Validate() 1394 mErr := err.(*multierror.Error) 1395 if !strings.Contains(mErr.Errors[0].Error(), "Invalid health check given") { 1396 t.Fatalf("err: %s", err) 1397 } 1398 if !strings.Contains(mErr.Errors[1].Error(), "Max parallel can not be less than zero") { 1399 t.Fatalf("err: %s", err) 1400 } 1401 if !strings.Contains(mErr.Errors[2].Error(), "Canary count can not be less than zero") { 1402 t.Fatalf("err: %s", err) 1403 } 1404 if !strings.Contains(mErr.Errors[3].Error(), "Minimum healthy time may not be less than zero") { 1405 t.Fatalf("err: %s", err) 1406 } 1407 if !strings.Contains(mErr.Errors[4].Error(), "Healthy deadline must be greater than zero") { 1408 t.Fatalf("err: %s", err) 1409 } 1410 if !strings.Contains(mErr.Errors[5].Error(), "Minimum healthy time must be less than healthy deadline") { 1411 t.Fatalf("err: %s", err) 1412 } 1413 } 1414 1415 func TestResource_NetIndex(t *testing.T) { 1416 r := &Resources{ 1417 Networks: []*NetworkResource{ 1418 &NetworkResource{Device: "eth0"}, 1419 &NetworkResource{Device: "lo0"}, 1420 &NetworkResource{Device: ""}, 1421 }, 1422 } 1423 if idx := r.NetIndex(&NetworkResource{Device: "eth0"}); idx != 0 { 1424 t.Fatalf("Bad: %d", idx) 1425 } 1426 if idx := r.NetIndex(&NetworkResource{Device: "lo0"}); idx != 1 { 1427 t.Fatalf("Bad: %d", idx) 1428 } 1429 if idx := r.NetIndex(&NetworkResource{Device: "eth1"}); idx != -1 { 1430 t.Fatalf("Bad: %d", idx) 1431 } 1432 } 1433 1434 func TestResource_Superset(t *testing.T) { 1435 r1 := &Resources{ 1436 CPU: 2000, 1437 MemoryMB: 2048, 1438 DiskMB: 10000, 1439 IOPS: 100, 1440 } 1441 r2 := &Resources{ 1442 CPU: 2000, 1443 MemoryMB: 1024, 1444 DiskMB: 5000, 1445 IOPS: 50, 1446 } 1447 1448 if s, _ := r1.Superset(r1); !s { 1449 t.Fatalf("bad") 1450 } 1451 if s, _ := r1.Superset(r2); !s { 1452 t.Fatalf("bad") 1453 } 1454 if s, _ := r2.Superset(r1); s { 1455 t.Fatalf("bad") 1456 } 1457 if s, _ := r2.Superset(r2); !s { 1458 t.Fatalf("bad") 1459 } 1460 } 1461 1462 func TestResource_Add(t *testing.T) { 1463 r1 := &Resources{ 1464 CPU: 2000, 1465 MemoryMB: 2048, 1466 DiskMB: 10000, 1467 IOPS: 100, 1468 Networks: []*NetworkResource{ 1469 &NetworkResource{ 1470 CIDR: "10.0.0.0/8", 1471 MBits: 100, 1472 ReservedPorts: []Port{{"ssh", 22}}, 1473 }, 1474 }, 1475 } 1476 r2 := &Resources{ 1477 CPU: 2000, 1478 MemoryMB: 1024, 1479 DiskMB: 5000, 1480 IOPS: 50, 1481 Networks: []*NetworkResource{ 1482 &NetworkResource{ 1483 IP: "10.0.0.1", 1484 MBits: 50, 1485 ReservedPorts: []Port{{"web", 80}}, 1486 }, 1487 }, 1488 } 1489 1490 err := r1.Add(r2) 1491 if err != nil { 1492 t.Fatalf("Err: %v", err) 1493 } 1494 1495 expect := &Resources{ 1496 CPU: 3000, 1497 MemoryMB: 3072, 1498 DiskMB: 15000, 1499 IOPS: 150, 1500 Networks: []*NetworkResource{ 1501 &NetworkResource{ 1502 CIDR: "10.0.0.0/8", 1503 MBits: 150, 1504 ReservedPorts: []Port{{"ssh", 22}, {"web", 80}}, 1505 }, 1506 }, 1507 } 1508 1509 if !reflect.DeepEqual(expect.Networks, r1.Networks) { 1510 t.Fatalf("bad: %#v %#v", expect, r1) 1511 } 1512 } 1513 1514 func TestResource_Add_Network(t *testing.T) { 1515 r1 := &Resources{} 1516 r2 := &Resources{ 1517 Networks: []*NetworkResource{ 1518 &NetworkResource{ 1519 MBits: 50, 1520 DynamicPorts: []Port{{"http", 0}, {"https", 0}}, 1521 }, 1522 }, 1523 } 1524 r3 := &Resources{ 1525 Networks: []*NetworkResource{ 1526 &NetworkResource{ 1527 MBits: 25, 1528 DynamicPorts: []Port{{"admin", 0}}, 1529 }, 1530 }, 1531 } 1532 1533 err := r1.Add(r2) 1534 if err != nil { 1535 t.Fatalf("Err: %v", err) 1536 } 1537 err = r1.Add(r3) 1538 if err != nil { 1539 t.Fatalf("Err: %v", err) 1540 } 1541 1542 expect := &Resources{ 1543 Networks: []*NetworkResource{ 1544 &NetworkResource{ 1545 MBits: 75, 1546 DynamicPorts: []Port{{"http", 0}, {"https", 0}, {"admin", 0}}, 1547 }, 1548 }, 1549 } 1550 1551 if !reflect.DeepEqual(expect.Networks, r1.Networks) { 1552 t.Fatalf("bad: %#v %#v", expect.Networks[0], r1.Networks[0]) 1553 } 1554 } 1555 1556 func TestEncodeDecode(t *testing.T) { 1557 type FooRequest struct { 1558 Foo string 1559 Bar int 1560 Baz bool 1561 } 1562 arg := &FooRequest{ 1563 Foo: "test", 1564 Bar: 42, 1565 Baz: true, 1566 } 1567 buf, err := Encode(1, arg) 1568 if err != nil { 1569 t.Fatalf("err: %v", err) 1570 } 1571 1572 var out FooRequest 1573 err = Decode(buf[1:], &out) 1574 if err != nil { 1575 t.Fatalf("err: %v", err) 1576 } 1577 1578 if !reflect.DeepEqual(arg, &out) { 1579 t.Fatalf("bad: %#v %#v", arg, out) 1580 } 1581 } 1582 1583 func BenchmarkEncodeDecode(b *testing.B) { 1584 job := testJob() 1585 1586 for i := 0; i < b.N; i++ { 1587 buf, err := Encode(1, job) 1588 if err != nil { 1589 b.Fatalf("err: %v", err) 1590 } 1591 1592 var out Job 1593 err = Decode(buf[1:], &out) 1594 if err != nil { 1595 b.Fatalf("err: %v", err) 1596 } 1597 } 1598 } 1599 1600 func TestInvalidServiceCheck(t *testing.T) { 1601 s := Service{ 1602 Name: "service-name", 1603 PortLabel: "bar", 1604 Checks: []*ServiceCheck{ 1605 { 1606 Name: "check-name", 1607 Type: "lol", 1608 }, 1609 }, 1610 } 1611 if err := s.Validate(); err == nil { 1612 t.Fatalf("Service should be invalid (invalid type)") 1613 } 1614 1615 s = Service{ 1616 Name: "service.name", 1617 PortLabel: "bar", 1618 } 1619 if err := s.ValidateName(s.Name); err == nil { 1620 t.Fatalf("Service should be invalid (contains a dot): %v", err) 1621 } 1622 1623 s = Service{ 1624 Name: "-my-service", 1625 PortLabel: "bar", 1626 } 1627 if err := s.Validate(); err == nil { 1628 t.Fatalf("Service should be invalid (begins with a hyphen): %v", err) 1629 } 1630 1631 s = Service{ 1632 Name: "my-service-${NOMAD_META_FOO}", 1633 PortLabel: "bar", 1634 } 1635 if err := s.Validate(); err != nil { 1636 t.Fatalf("Service should be valid: %v", err) 1637 } 1638 1639 s = Service{ 1640 Name: "abcdef0123456789-abcdef0123456789-abcdef0123456789-abcdef0123456", 1641 PortLabel: "bar", 1642 } 1643 if err := s.ValidateName(s.Name); err == nil { 1644 t.Fatalf("Service should be invalid (too long): %v", err) 1645 } 1646 1647 s = Service{ 1648 Name: "service-name", 1649 Checks: []*ServiceCheck{ 1650 { 1651 Name: "check-tcp", 1652 Type: ServiceCheckTCP, 1653 Interval: 5 * time.Second, 1654 Timeout: 2 * time.Second, 1655 }, 1656 { 1657 Name: "check-http", 1658 Type: ServiceCheckHTTP, 1659 Path: "/foo", 1660 Interval: 5 * time.Second, 1661 Timeout: 2 * time.Second, 1662 }, 1663 }, 1664 } 1665 if err := s.Validate(); err == nil { 1666 t.Fatalf("service should be invalid (tcp/http checks with no port): %v", err) 1667 } 1668 1669 s = Service{ 1670 Name: "service-name", 1671 Checks: []*ServiceCheck{ 1672 { 1673 Name: "check-script", 1674 Type: ServiceCheckScript, 1675 Command: "/bin/date", 1676 Interval: 5 * time.Second, 1677 Timeout: 2 * time.Second, 1678 }, 1679 }, 1680 } 1681 if err := s.Validate(); err != nil { 1682 t.Fatalf("un-expected error: %v", err) 1683 } 1684 } 1685 1686 func TestDistinctCheckID(t *testing.T) { 1687 c1 := ServiceCheck{ 1688 Name: "web-health", 1689 Type: "http", 1690 Path: "/health", 1691 Interval: 2 * time.Second, 1692 Timeout: 3 * time.Second, 1693 } 1694 c2 := ServiceCheck{ 1695 Name: "web-health", 1696 Type: "http", 1697 Path: "/health1", 1698 Interval: 2 * time.Second, 1699 Timeout: 3 * time.Second, 1700 } 1701 1702 c3 := ServiceCheck{ 1703 Name: "web-health", 1704 Type: "http", 1705 Path: "/health", 1706 Interval: 4 * time.Second, 1707 Timeout: 3 * time.Second, 1708 } 1709 serviceID := "123" 1710 c1Hash := c1.Hash(serviceID) 1711 c2Hash := c2.Hash(serviceID) 1712 c3Hash := c3.Hash(serviceID) 1713 1714 if c1Hash == c2Hash || c1Hash == c3Hash || c3Hash == c2Hash { 1715 t.Fatalf("Checks need to be uniq c1: %s, c2: %s, c3: %s", c1Hash, c2Hash, c3Hash) 1716 } 1717 1718 } 1719 1720 func TestService_Canonicalize(t *testing.T) { 1721 job := "example" 1722 taskGroup := "cache" 1723 task := "redis" 1724 1725 s := Service{ 1726 Name: "${TASK}-db", 1727 } 1728 1729 s.Canonicalize(job, taskGroup, task) 1730 if s.Name != "redis-db" { 1731 t.Fatalf("Expected name: %v, Actual: %v", "redis-db", s.Name) 1732 } 1733 1734 s.Name = "db" 1735 s.Canonicalize(job, taskGroup, task) 1736 if s.Name != "db" { 1737 t.Fatalf("Expected name: %v, Actual: %v", "redis-db", s.Name) 1738 } 1739 1740 s.Name = "${JOB}-${TASKGROUP}-${TASK}-db" 1741 s.Canonicalize(job, taskGroup, task) 1742 if s.Name != "example-cache-redis-db" { 1743 t.Fatalf("Expected name: %v, Actual: %v", "expample-cache-redis-db", s.Name) 1744 } 1745 1746 s.Name = "${BASE}-db" 1747 s.Canonicalize(job, taskGroup, task) 1748 if s.Name != "example-cache-redis-db" { 1749 t.Fatalf("Expected name: %v, Actual: %v", "expample-cache-redis-db", s.Name) 1750 } 1751 1752 } 1753 1754 func TestJob_ExpandServiceNames(t *testing.T) { 1755 j := &Job{ 1756 Name: "my-job", 1757 TaskGroups: []*TaskGroup{ 1758 &TaskGroup{ 1759 Name: "web", 1760 Tasks: []*Task{ 1761 { 1762 Name: "frontend", 1763 Services: []*Service{ 1764 { 1765 Name: "${BASE}-default", 1766 }, 1767 { 1768 Name: "jmx", 1769 }, 1770 }, 1771 }, 1772 }, 1773 }, 1774 &TaskGroup{ 1775 Name: "admin", 1776 Tasks: []*Task{ 1777 { 1778 Name: "admin-web", 1779 }, 1780 }, 1781 }, 1782 }, 1783 } 1784 1785 j.Canonicalize() 1786 1787 service1Name := j.TaskGroups[0].Tasks[0].Services[0].Name 1788 if service1Name != "my-job-web-frontend-default" { 1789 t.Fatalf("Expected Service Name: %s, Actual: %s", "my-job-web-frontend-default", service1Name) 1790 } 1791 1792 service2Name := j.TaskGroups[0].Tasks[0].Services[1].Name 1793 if service2Name != "jmx" { 1794 t.Fatalf("Expected Service Name: %s, Actual: %s", "jmx", service2Name) 1795 } 1796 1797 } 1798 1799 func TestPeriodicConfig_EnabledInvalid(t *testing.T) { 1800 // Create a config that is enabled but with no interval specified. 1801 p := &PeriodicConfig{Enabled: true} 1802 if err := p.Validate(); err == nil { 1803 t.Fatal("Enabled PeriodicConfig with no spec or type shouldn't be valid") 1804 } 1805 1806 // Create a config that is enabled, with a spec but no type specified. 1807 p = &PeriodicConfig{Enabled: true, Spec: "foo"} 1808 if err := p.Validate(); err == nil { 1809 t.Fatal("Enabled PeriodicConfig with no spec type shouldn't be valid") 1810 } 1811 1812 // Create a config that is enabled, with a spec type but no spec specified. 1813 p = &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron} 1814 if err := p.Validate(); err == nil { 1815 t.Fatal("Enabled PeriodicConfig with no spec shouldn't be valid") 1816 } 1817 1818 // Create a config that is enabled, with a bad time zone. 1819 p = &PeriodicConfig{Enabled: true, TimeZone: "FOO"} 1820 if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "time zone") { 1821 t.Fatalf("Enabled PeriodicConfig with bad time zone shouldn't be valid: %v", err) 1822 } 1823 } 1824 1825 func TestPeriodicConfig_InvalidCron(t *testing.T) { 1826 specs := []string{"foo", "* *", "@foo"} 1827 for _, spec := range specs { 1828 p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec} 1829 p.Canonicalize() 1830 if err := p.Validate(); err == nil { 1831 t.Fatal("Invalid cron spec") 1832 } 1833 } 1834 } 1835 1836 func TestPeriodicConfig_ValidCron(t *testing.T) { 1837 specs := []string{"0 0 29 2 *", "@hourly", "0 0-15 * * *"} 1838 for _, spec := range specs { 1839 p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec} 1840 p.Canonicalize() 1841 if err := p.Validate(); err != nil { 1842 t.Fatal("Passed valid cron") 1843 } 1844 } 1845 } 1846 1847 func TestPeriodicConfig_NextCron(t *testing.T) { 1848 from := time.Date(2009, time.November, 10, 23, 22, 30, 0, time.UTC) 1849 specs := []string{"0 0 29 2 * 1980", "*/5 * * * *"} 1850 expected := []time.Time{time.Time{}, time.Date(2009, time.November, 10, 23, 25, 0, 0, time.UTC)} 1851 for i, spec := range specs { 1852 p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec} 1853 p.Canonicalize() 1854 n := p.Next(from) 1855 if expected[i] != n { 1856 t.Fatalf("Next(%v) returned %v; want %v", from, n, expected[i]) 1857 } 1858 } 1859 } 1860 1861 func TestPeriodicConfig_ValidTimeZone(t *testing.T) { 1862 zones := []string{"Africa/Abidjan", "America/Chicago", "Europe/Minsk", "UTC"} 1863 for _, zone := range zones { 1864 p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: "0 0 29 2 * 1980", TimeZone: zone} 1865 p.Canonicalize() 1866 if err := p.Validate(); err != nil { 1867 t.Fatalf("Valid tz errored: %v", err) 1868 } 1869 } 1870 } 1871 1872 func TestPeriodicConfig_DST(t *testing.T) { 1873 // On Sun, Mar 12, 2:00 am 2017: +1 hour UTC 1874 p := &PeriodicConfig{ 1875 Enabled: true, 1876 SpecType: PeriodicSpecCron, 1877 Spec: "0 2 11-12 3 * 2017", 1878 TimeZone: "America/Los_Angeles", 1879 } 1880 p.Canonicalize() 1881 1882 t1 := time.Date(2017, time.March, 11, 1, 0, 0, 0, p.location) 1883 t2 := time.Date(2017, time.March, 12, 1, 0, 0, 0, p.location) 1884 1885 // E1 is an 8 hour adjustment, E2 is a 7 hour adjustment 1886 e1 := time.Date(2017, time.March, 11, 10, 0, 0, 0, time.UTC) 1887 e2 := time.Date(2017, time.March, 12, 9, 0, 0, 0, time.UTC) 1888 1889 n1 := p.Next(t1).UTC() 1890 n2 := p.Next(t2).UTC() 1891 1892 if !reflect.DeepEqual(e1, n1) { 1893 t.Fatalf("Got %v; want %v", n1, e1) 1894 } 1895 if !reflect.DeepEqual(e2, n2) { 1896 t.Fatalf("Got %v; want %v", n1, e1) 1897 } 1898 } 1899 1900 func TestRestartPolicy_Validate(t *testing.T) { 1901 // Policy with acceptable restart options passes 1902 p := &RestartPolicy{ 1903 Mode: RestartPolicyModeFail, 1904 Attempts: 0, 1905 Interval: 5 * time.Second, 1906 } 1907 if err := p.Validate(); err != nil { 1908 t.Fatalf("err: %v", err) 1909 } 1910 1911 // Policy with ambiguous restart options fails 1912 p = &RestartPolicy{ 1913 Mode: RestartPolicyModeDelay, 1914 Attempts: 0, 1915 Interval: 5 * time.Second, 1916 } 1917 if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "ambiguous") { 1918 t.Fatalf("expect ambiguity error, got: %v", err) 1919 } 1920 1921 // Bad policy mode fails 1922 p = &RestartPolicy{ 1923 Mode: "nope", 1924 Attempts: 1, 1925 Interval: 5 * time.Second, 1926 } 1927 if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "mode") { 1928 t.Fatalf("expect mode error, got: %v", err) 1929 } 1930 1931 // Fails when attempts*delay does not fit inside interval 1932 p = &RestartPolicy{ 1933 Mode: RestartPolicyModeDelay, 1934 Attempts: 3, 1935 Delay: 5 * time.Second, 1936 Interval: 5 * time.Second, 1937 } 1938 if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "can't restart") { 1939 t.Fatalf("expect restart interval error, got: %v", err) 1940 } 1941 1942 // Fails when interval is to small 1943 p = &RestartPolicy{ 1944 Mode: RestartPolicyModeDelay, 1945 Attempts: 3, 1946 Delay: 5 * time.Second, 1947 Interval: 2 * time.Second, 1948 } 1949 if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "Interval can not be less than") { 1950 t.Fatalf("expect interval too small error, got: %v", err) 1951 } 1952 } 1953 1954 func TestAllocation_Index(t *testing.T) { 1955 a1 := Allocation{ 1956 Name: "example.cache[1]", 1957 TaskGroup: "cache", 1958 JobID: "example", 1959 Job: &Job{ 1960 ID: "example", 1961 TaskGroups: []*TaskGroup{{Name: "cache"}}}, 1962 } 1963 e1 := uint(1) 1964 a2 := a1.Copy() 1965 a2.Name = "example.cache[713127]" 1966 e2 := uint(713127) 1967 1968 if a1.Index() != e1 || a2.Index() != e2 { 1969 t.Fatalf("Got %d and %d", a1.Index(), a2.Index()) 1970 } 1971 } 1972 1973 func TestTaskArtifact_Validate_Source(t *testing.T) { 1974 valid := &TaskArtifact{GetterSource: "google.com"} 1975 if err := valid.Validate(); err != nil { 1976 t.Fatalf("unexpected error: %v", err) 1977 } 1978 } 1979 1980 func TestTaskArtifact_Validate_Dest(t *testing.T) { 1981 valid := &TaskArtifact{GetterSource: "google.com"} 1982 if err := valid.Validate(); err != nil { 1983 t.Fatalf("unexpected error: %v", err) 1984 } 1985 1986 valid.RelativeDest = "local/" 1987 if err := valid.Validate(); err != nil { 1988 t.Fatalf("unexpected error: %v", err) 1989 } 1990 1991 valid.RelativeDest = "local/.." 1992 if err := valid.Validate(); err != nil { 1993 t.Fatalf("unexpected error: %v", err) 1994 } 1995 1996 valid.RelativeDest = "local/../../.." 1997 if err := valid.Validate(); err == nil { 1998 t.Fatalf("expected error: %v", err) 1999 } 2000 } 2001 2002 func TestAllocation_ShouldMigrate(t *testing.T) { 2003 alloc := Allocation{ 2004 TaskGroup: "foo", 2005 Job: &Job{ 2006 TaskGroups: []*TaskGroup{ 2007 { 2008 Name: "foo", 2009 EphemeralDisk: &EphemeralDisk{ 2010 Migrate: true, 2011 Sticky: true, 2012 }, 2013 }, 2014 }, 2015 }, 2016 } 2017 2018 if !alloc.ShouldMigrate() { 2019 t.Fatalf("bad: %v", alloc) 2020 } 2021 2022 alloc1 := Allocation{ 2023 TaskGroup: "foo", 2024 Job: &Job{ 2025 TaskGroups: []*TaskGroup{ 2026 { 2027 Name: "foo", 2028 EphemeralDisk: &EphemeralDisk{}, 2029 }, 2030 }, 2031 }, 2032 } 2033 2034 if alloc1.ShouldMigrate() { 2035 t.Fatalf("bad: %v", alloc) 2036 } 2037 2038 alloc2 := Allocation{ 2039 TaskGroup: "foo", 2040 Job: &Job{ 2041 TaskGroups: []*TaskGroup{ 2042 { 2043 Name: "foo", 2044 EphemeralDisk: &EphemeralDisk{ 2045 Sticky: false, 2046 Migrate: true, 2047 }, 2048 }, 2049 }, 2050 }, 2051 } 2052 2053 if alloc2.ShouldMigrate() { 2054 t.Fatalf("bad: %v", alloc) 2055 } 2056 2057 alloc3 := Allocation{ 2058 TaskGroup: "foo", 2059 Job: &Job{ 2060 TaskGroups: []*TaskGroup{ 2061 { 2062 Name: "foo", 2063 }, 2064 }, 2065 }, 2066 } 2067 2068 if alloc3.ShouldMigrate() { 2069 t.Fatalf("bad: %v", alloc) 2070 } 2071 } 2072 2073 func TestTaskArtifact_Validate_Checksum(t *testing.T) { 2074 cases := []struct { 2075 Input *TaskArtifact 2076 Err bool 2077 }{ 2078 { 2079 &TaskArtifact{ 2080 GetterSource: "foo.com", 2081 GetterOptions: map[string]string{ 2082 "checksum": "no-type", 2083 }, 2084 }, 2085 true, 2086 }, 2087 { 2088 &TaskArtifact{ 2089 GetterSource: "foo.com", 2090 GetterOptions: map[string]string{ 2091 "checksum": "md5:toosmall", 2092 }, 2093 }, 2094 true, 2095 }, 2096 { 2097 &TaskArtifact{ 2098 GetterSource: "foo.com", 2099 GetterOptions: map[string]string{ 2100 "checksum": "invalid:type", 2101 }, 2102 }, 2103 true, 2104 }, 2105 } 2106 2107 for i, tc := range cases { 2108 err := tc.Input.Validate() 2109 if (err != nil) != tc.Err { 2110 t.Fatalf("case %d: %v", i, err) 2111 continue 2112 } 2113 } 2114 } 2115 2116 func TestAllocation_Terminated(t *testing.T) { 2117 type desiredState struct { 2118 ClientStatus string 2119 DesiredStatus string 2120 Terminated bool 2121 } 2122 2123 harness := []desiredState{ 2124 { 2125 ClientStatus: AllocClientStatusPending, 2126 DesiredStatus: AllocDesiredStatusStop, 2127 Terminated: false, 2128 }, 2129 { 2130 ClientStatus: AllocClientStatusRunning, 2131 DesiredStatus: AllocDesiredStatusStop, 2132 Terminated: false, 2133 }, 2134 { 2135 ClientStatus: AllocClientStatusFailed, 2136 DesiredStatus: AllocDesiredStatusStop, 2137 Terminated: true, 2138 }, 2139 { 2140 ClientStatus: AllocClientStatusFailed, 2141 DesiredStatus: AllocDesiredStatusRun, 2142 Terminated: true, 2143 }, 2144 } 2145 2146 for _, state := range harness { 2147 alloc := Allocation{} 2148 alloc.DesiredStatus = state.DesiredStatus 2149 alloc.ClientStatus = state.ClientStatus 2150 if alloc.Terminated() != state.Terminated { 2151 t.Fatalf("expected: %v, actual: %v", state.Terminated, alloc.Terminated()) 2152 } 2153 } 2154 } 2155 2156 func TestVault_Validate(t *testing.T) { 2157 v := &Vault{ 2158 Env: true, 2159 ChangeMode: VaultChangeModeNoop, 2160 } 2161 2162 if err := v.Validate(); err == nil || !strings.Contains(err.Error(), "Policy list") { 2163 t.Fatalf("Expected policy list empty error") 2164 } 2165 2166 v.Policies = []string{"foo", "root"} 2167 v.ChangeMode = VaultChangeModeSignal 2168 2169 err := v.Validate() 2170 if err == nil { 2171 t.Fatalf("Expected validation errors") 2172 } 2173 2174 if !strings.Contains(err.Error(), "Signal must") { 2175 t.Fatalf("Expected signal empty error") 2176 } 2177 if !strings.Contains(err.Error(), "root") { 2178 t.Fatalf("Expected root error") 2179 } 2180 } 2181 2182 func TestParameterizedJobConfig_Validate(t *testing.T) { 2183 d := &ParameterizedJobConfig{ 2184 Payload: "foo", 2185 } 2186 2187 if err := d.Validate(); err == nil || !strings.Contains(err.Error(), "payload") { 2188 t.Fatalf("Expected unknown payload requirement: %v", err) 2189 } 2190 2191 d.Payload = DispatchPayloadOptional 2192 d.MetaOptional = []string{"foo", "bar"} 2193 d.MetaRequired = []string{"bar", "baz"} 2194 2195 if err := d.Validate(); err == nil || !strings.Contains(err.Error(), "disjoint") { 2196 t.Fatalf("Expected meta not being disjoint error: %v", err) 2197 } 2198 } 2199 2200 func TestParameterizedJobConfig_Validate_NonBatch(t *testing.T) { 2201 job := testJob() 2202 job.ParameterizedJob = &ParameterizedJobConfig{ 2203 Payload: DispatchPayloadOptional, 2204 } 2205 job.Type = JobTypeSystem 2206 2207 if err := job.Validate(); err == nil || !strings.Contains(err.Error(), "only be used with") { 2208 t.Fatalf("Expected bad scheduler tpye: %v", err) 2209 } 2210 } 2211 2212 func TestParameterizedJobConfig_Canonicalize(t *testing.T) { 2213 d := &ParameterizedJobConfig{} 2214 d.Canonicalize() 2215 if d.Payload != DispatchPayloadOptional { 2216 t.Fatalf("Canonicalize failed") 2217 } 2218 } 2219 2220 func TestDispatchPayloadConfig_Validate(t *testing.T) { 2221 d := &DispatchPayloadConfig{ 2222 File: "foo", 2223 } 2224 2225 // task/local/haha 2226 if err := d.Validate(); err != nil { 2227 t.Fatalf("bad: %v", err) 2228 } 2229 2230 // task/haha 2231 d.File = "../haha" 2232 if err := d.Validate(); err != nil { 2233 t.Fatalf("bad: %v", err) 2234 } 2235 2236 // ../haha 2237 d.File = "../../../haha" 2238 if err := d.Validate(); err == nil { 2239 t.Fatalf("bad: %v", err) 2240 } 2241 } 2242 2243 func TestIsRecoverable(t *testing.T) { 2244 if IsRecoverable(nil) { 2245 t.Errorf("nil should not be recoverable") 2246 } 2247 if IsRecoverable(NewRecoverableError(nil, true)) { 2248 t.Errorf("NewRecoverableError(nil, true) should not be recoverable") 2249 } 2250 if IsRecoverable(fmt.Errorf("i promise im recoverable")) { 2251 t.Errorf("Custom errors should not be recoverable") 2252 } 2253 if IsRecoverable(NewRecoverableError(fmt.Errorf(""), false)) { 2254 t.Errorf("Explicitly unrecoverable errors should not be recoverable") 2255 } 2256 if !IsRecoverable(NewRecoverableError(fmt.Errorf(""), true)) { 2257 t.Errorf("Explicitly recoverable errors *should* be recoverable") 2258 } 2259 }