github.com/maier/nomad@v0.4.1-0.20161110003312-a9e3d0b8549d/nomad/structs/structs_test.go (about) 1 package structs 2 3 import ( 4 "reflect" 5 "strings" 6 "testing" 7 "time" 8 9 "github.com/hashicorp/consul/api" 10 "github.com/hashicorp/go-multierror" 11 ) 12 13 func TestJob_Validate(t *testing.T) { 14 j := &Job{} 15 err := j.Validate() 16 mErr := err.(*multierror.Error) 17 if !strings.Contains(mErr.Errors[0].Error(), "job region") { 18 t.Fatalf("err: %s", err) 19 } 20 if !strings.Contains(mErr.Errors[1].Error(), "job ID") { 21 t.Fatalf("err: %s", err) 22 } 23 if !strings.Contains(mErr.Errors[2].Error(), "job name") { 24 t.Fatalf("err: %s", err) 25 } 26 if !strings.Contains(mErr.Errors[3].Error(), "job type") { 27 t.Fatalf("err: %s", err) 28 } 29 if !strings.Contains(mErr.Errors[4].Error(), "priority") { 30 t.Fatalf("err: %s", err) 31 } 32 if !strings.Contains(mErr.Errors[5].Error(), "datacenters") { 33 t.Fatalf("err: %s", err) 34 } 35 if !strings.Contains(mErr.Errors[6].Error(), "task groups") { 36 t.Fatalf("err: %s", err) 37 } 38 39 j = &Job{ 40 Type: JobTypeService, 41 Periodic: &PeriodicConfig{ 42 Enabled: true, 43 }, 44 } 45 err = j.Validate() 46 mErr = err.(*multierror.Error) 47 if !strings.Contains(mErr.Error(), "Periodic") { 48 t.Fatalf("err: %s", err) 49 } 50 51 j = &Job{ 52 Region: "global", 53 ID: GenerateUUID(), 54 Name: "my-job", 55 Type: JobTypeService, 56 Priority: 50, 57 Datacenters: []string{"dc1"}, 58 TaskGroups: []*TaskGroup{ 59 &TaskGroup{ 60 Name: "web", 61 RestartPolicy: &RestartPolicy{ 62 Interval: 5 * time.Minute, 63 Delay: 10 * time.Second, 64 Attempts: 10, 65 }, 66 }, 67 &TaskGroup{ 68 Name: "web", 69 RestartPolicy: &RestartPolicy{ 70 Interval: 5 * time.Minute, 71 Delay: 10 * time.Second, 72 Attempts: 10, 73 }, 74 }, 75 &TaskGroup{ 76 RestartPolicy: &RestartPolicy{ 77 Interval: 5 * time.Minute, 78 Delay: 10 * time.Second, 79 Attempts: 10, 80 }, 81 }, 82 }, 83 } 84 err = j.Validate() 85 mErr = err.(*multierror.Error) 86 if !strings.Contains(mErr.Errors[0].Error(), "2 redefines 'web' from group 1") { 87 t.Fatalf("err: %s", err) 88 } 89 if !strings.Contains(mErr.Errors[1].Error(), "group 3 missing name") { 90 t.Fatalf("err: %s", err) 91 } 92 if !strings.Contains(mErr.Errors[2].Error(), "Task group web validation failed") { 93 t.Fatalf("err: %s", err) 94 } 95 } 96 97 func testJob() *Job { 98 return &Job{ 99 Region: "global", 100 ID: GenerateUUID(), 101 Name: "my-job", 102 Type: JobTypeService, 103 Priority: 50, 104 AllAtOnce: false, 105 Datacenters: []string{"dc1"}, 106 Constraints: []*Constraint{ 107 &Constraint{ 108 LTarget: "$attr.kernel.name", 109 RTarget: "linux", 110 Operand: "=", 111 }, 112 }, 113 Periodic: &PeriodicConfig{ 114 Enabled: false, 115 }, 116 TaskGroups: []*TaskGroup{ 117 &TaskGroup{ 118 Name: "web", 119 Count: 10, 120 EphemeralDisk: DefaultEphemeralDisk(), 121 RestartPolicy: &RestartPolicy{ 122 Mode: RestartPolicyModeFail, 123 Attempts: 3, 124 Interval: 10 * time.Minute, 125 Delay: 1 * time.Minute, 126 }, 127 Tasks: []*Task{ 128 &Task{ 129 Name: "web", 130 Driver: "exec", 131 Config: map[string]interface{}{ 132 "command": "/bin/date", 133 }, 134 Env: map[string]string{ 135 "FOO": "bar", 136 }, 137 Artifacts: []*TaskArtifact{ 138 { 139 GetterSource: "http://foo.com", 140 }, 141 }, 142 Services: []*Service{ 143 { 144 Name: "${TASK}-frontend", 145 PortLabel: "http", 146 }, 147 }, 148 Resources: &Resources{ 149 CPU: 500, 150 MemoryMB: 256, 151 Networks: []*NetworkResource{ 152 &NetworkResource{ 153 MBits: 50, 154 DynamicPorts: []Port{{Label: "http"}}, 155 }, 156 }, 157 }, 158 LogConfig: &LogConfig{ 159 MaxFiles: 10, 160 MaxFileSizeMB: 1, 161 }, 162 }, 163 }, 164 Meta: map[string]string{ 165 "elb_check_type": "http", 166 "elb_check_interval": "30s", 167 "elb_check_min": "3", 168 }, 169 }, 170 }, 171 Meta: map[string]string{ 172 "owner": "armon", 173 }, 174 } 175 } 176 177 func TestJob_Copy(t *testing.T) { 178 j := testJob() 179 c := j.Copy() 180 if !reflect.DeepEqual(j, c) { 181 t.Fatalf("Copy() returned an unequal Job; got %#v; want %#v", c, j) 182 } 183 } 184 185 func TestJob_IsPeriodic(t *testing.T) { 186 j := &Job{ 187 Type: JobTypeService, 188 Periodic: &PeriodicConfig{ 189 Enabled: true, 190 }, 191 } 192 if !j.IsPeriodic() { 193 t.Fatalf("IsPeriodic() returned false on periodic job") 194 } 195 196 j = &Job{ 197 Type: JobTypeService, 198 } 199 if j.IsPeriodic() { 200 t.Fatalf("IsPeriodic() returned true on non-periodic job") 201 } 202 } 203 204 func TestJob_SystemJob_Validate(t *testing.T) { 205 j := testJob() 206 j.Type = JobTypeSystem 207 j.Canonicalize() 208 209 err := j.Validate() 210 if err == nil || !strings.Contains(err.Error(), "exceed") { 211 t.Fatalf("expect error due to count") 212 } 213 214 j.TaskGroups[0].Count = 0 215 if err := j.Validate(); err != nil { 216 t.Fatalf("unexpected err: %v", err) 217 } 218 219 j.TaskGroups[0].Count = 1 220 if err := j.Validate(); err != nil { 221 t.Fatalf("unexpected err: %v", err) 222 } 223 } 224 225 func TestJob_VaultPolicies(t *testing.T) { 226 j0 := &Job{} 227 e0 := make(map[string]map[string]*Vault, 0) 228 229 vj1 := &Vault{ 230 Policies: []string{ 231 "p1", 232 "p2", 233 }, 234 } 235 vj2 := &Vault{ 236 Policies: []string{ 237 "p3", 238 "p4", 239 }, 240 } 241 vj3 := &Vault{ 242 Policies: []string{ 243 "p5", 244 }, 245 } 246 j1 := &Job{ 247 TaskGroups: []*TaskGroup{ 248 &TaskGroup{ 249 Name: "foo", 250 Tasks: []*Task{ 251 &Task{ 252 Name: "t1", 253 }, 254 &Task{ 255 Name: "t2", 256 Vault: vj1, 257 }, 258 }, 259 }, 260 &TaskGroup{ 261 Name: "bar", 262 Tasks: []*Task{ 263 &Task{ 264 Name: "t3", 265 Vault: vj2, 266 }, 267 &Task{ 268 Name: "t4", 269 Vault: vj3, 270 }, 271 }, 272 }, 273 }, 274 } 275 276 e1 := map[string]map[string]*Vault{ 277 "foo": map[string]*Vault{ 278 "t2": vj1, 279 }, 280 "bar": map[string]*Vault{ 281 "t3": vj2, 282 "t4": vj3, 283 }, 284 } 285 286 cases := []struct { 287 Job *Job 288 Expected map[string]map[string]*Vault 289 }{ 290 { 291 Job: j0, 292 Expected: e0, 293 }, 294 { 295 Job: j1, 296 Expected: e1, 297 }, 298 } 299 300 for i, c := range cases { 301 got := c.Job.VaultPolicies() 302 if !reflect.DeepEqual(got, c.Expected) { 303 t.Fatalf("case %d: got %#v; want %#v", i+1, got, c.Expected) 304 } 305 } 306 } 307 308 func TestJob_RequiredSignals(t *testing.T) { 309 j0 := &Job{} 310 e0 := make(map[string]map[string][]string, 0) 311 312 vj1 := &Vault{ 313 Policies: []string{"p1"}, 314 ChangeMode: VaultChangeModeNoop, 315 } 316 vj2 := &Vault{ 317 Policies: []string{"p1"}, 318 ChangeMode: VaultChangeModeSignal, 319 ChangeSignal: "SIGUSR1", 320 } 321 tj1 := &Template{ 322 SourcePath: "foo", 323 DestPath: "bar", 324 ChangeMode: TemplateChangeModeNoop, 325 } 326 tj2 := &Template{ 327 SourcePath: "foo", 328 DestPath: "bar", 329 ChangeMode: TemplateChangeModeSignal, 330 ChangeSignal: "SIGUSR2", 331 } 332 j1 := &Job{ 333 TaskGroups: []*TaskGroup{ 334 &TaskGroup{ 335 Name: "foo", 336 Tasks: []*Task{ 337 &Task{ 338 Name: "t1", 339 }, 340 &Task{ 341 Name: "t2", 342 Vault: vj2, 343 Templates: []*Template{tj2}, 344 }, 345 }, 346 }, 347 &TaskGroup{ 348 Name: "bar", 349 Tasks: []*Task{ 350 &Task{ 351 Name: "t3", 352 Vault: vj1, 353 Templates: []*Template{tj1}, 354 }, 355 &Task{ 356 Name: "t4", 357 Vault: vj2, 358 }, 359 }, 360 }, 361 }, 362 } 363 364 e1 := map[string]map[string][]string{ 365 "foo": map[string][]string{ 366 "t2": []string{"SIGUSR1", "SIGUSR2"}, 367 }, 368 "bar": map[string][]string{ 369 "t4": []string{"SIGUSR1"}, 370 }, 371 } 372 373 cases := []struct { 374 Job *Job 375 Expected map[string]map[string][]string 376 }{ 377 { 378 Job: j0, 379 Expected: e0, 380 }, 381 { 382 Job: j1, 383 Expected: e1, 384 }, 385 } 386 387 for i, c := range cases { 388 got := c.Job.RequiredSignals() 389 if !reflect.DeepEqual(got, c.Expected) { 390 t.Fatalf("case %d: got %#v; want %#v", i+1, got, c.Expected) 391 } 392 } 393 } 394 395 func TestTaskGroup_Validate(t *testing.T) { 396 tg := &TaskGroup{ 397 Count: -1, 398 RestartPolicy: &RestartPolicy{ 399 Interval: 5 * time.Minute, 400 Delay: 10 * time.Second, 401 Attempts: 10, 402 Mode: RestartPolicyModeDelay, 403 }, 404 } 405 err := tg.Validate() 406 mErr := err.(*multierror.Error) 407 if !strings.Contains(mErr.Errors[0].Error(), "group name") { 408 t.Fatalf("err: %s", err) 409 } 410 if !strings.Contains(mErr.Errors[1].Error(), "count can't be negative") { 411 t.Fatalf("err: %s", err) 412 } 413 if !strings.Contains(mErr.Errors[2].Error(), "Missing tasks") { 414 t.Fatalf("err: %s", err) 415 } 416 417 tg = &TaskGroup{ 418 Name: "web", 419 Count: 1, 420 Tasks: []*Task{ 421 &Task{Name: "web"}, 422 &Task{Name: "web"}, 423 &Task{}, 424 }, 425 RestartPolicy: &RestartPolicy{ 426 Interval: 5 * time.Minute, 427 Delay: 10 * time.Second, 428 Attempts: 10, 429 Mode: RestartPolicyModeDelay, 430 }, 431 } 432 433 err = tg.Validate() 434 mErr = err.(*multierror.Error) 435 if !strings.Contains(mErr.Errors[0].Error(), "should have an ephemeral disk object") { 436 t.Fatalf("err: %s", err) 437 } 438 if !strings.Contains(mErr.Errors[1].Error(), "2 redefines 'web' from task 1") { 439 t.Fatalf("err: %s", err) 440 } 441 if !strings.Contains(mErr.Errors[2].Error(), "Task 3 missing name") { 442 t.Fatalf("err: %s", err) 443 } 444 if !strings.Contains(mErr.Errors[3].Error(), "Task web validation failed") { 445 t.Fatalf("err: %s", err) 446 } 447 } 448 449 func TestTask_Validate(t *testing.T) { 450 task := &Task{} 451 ephemeralDisk := DefaultEphemeralDisk() 452 err := task.Validate(ephemeralDisk) 453 mErr := err.(*multierror.Error) 454 if !strings.Contains(mErr.Errors[0].Error(), "task name") { 455 t.Fatalf("err: %s", err) 456 } 457 if !strings.Contains(mErr.Errors[1].Error(), "task driver") { 458 t.Fatalf("err: %s", err) 459 } 460 if !strings.Contains(mErr.Errors[2].Error(), "task resources") { 461 t.Fatalf("err: %s", err) 462 } 463 464 task = &Task{Name: "web/foo"} 465 err = task.Validate(ephemeralDisk) 466 mErr = err.(*multierror.Error) 467 if !strings.Contains(mErr.Errors[0].Error(), "slashes") { 468 t.Fatalf("err: %s", err) 469 } 470 471 task = &Task{ 472 Name: "web", 473 Driver: "docker", 474 Resources: &Resources{ 475 CPU: 100, 476 MemoryMB: 100, 477 IOPS: 10, 478 }, 479 LogConfig: DefaultLogConfig(), 480 } 481 ephemeralDisk.SizeMB = 200 482 err = task.Validate(ephemeralDisk) 483 if err != nil { 484 t.Fatalf("err: %s", err) 485 } 486 } 487 488 func TestTask_Validate_Services(t *testing.T) { 489 s1 := &Service{ 490 Name: "service-name", 491 PortLabel: "bar", 492 Checks: []*ServiceCheck{ 493 { 494 Name: "check-name", 495 Type: ServiceCheckTCP, 496 Interval: 0 * time.Second, 497 }, 498 { 499 Name: "check-name", 500 Type: ServiceCheckTCP, 501 Timeout: 2 * time.Second, 502 }, 503 { 504 Name: "check-name", 505 Type: ServiceCheckTCP, 506 Interval: 1 * time.Second, 507 }, 508 }, 509 } 510 511 s2 := &Service{ 512 Name: "service-name", 513 } 514 515 ephemeralDisk := DefaultEphemeralDisk() 516 task := &Task{ 517 Name: "web", 518 Driver: "docker", 519 Resources: &Resources{ 520 CPU: 100, 521 MemoryMB: 100, 522 IOPS: 10, 523 }, 524 Services: []*Service{s1, s2}, 525 } 526 ephemeralDisk.SizeMB = 200 527 528 err := task.Validate(ephemeralDisk) 529 if err == nil { 530 t.Fatal("expected an error") 531 } 532 if !strings.Contains(err.Error(), "referenced by services service-name does not exist") { 533 t.Fatalf("err: %s", err) 534 } 535 536 if !strings.Contains(err.Error(), "service \"service-name\" is duplicate") { 537 t.Fatalf("err: %v", err) 538 } 539 540 if !strings.Contains(err.Error(), "check \"check-name\" is duplicate") { 541 t.Fatalf("err: %v", err) 542 } 543 544 if !strings.Contains(err.Error(), "missing required value interval") { 545 t.Fatalf("err: %v", err) 546 } 547 548 if !strings.Contains(err.Error(), "cannot be less than") { 549 t.Fatalf("err: %v", err) 550 } 551 } 552 553 func TestTask_Validate_Service_Check(t *testing.T) { 554 555 check1 := ServiceCheck{ 556 Name: "check-name", 557 Type: ServiceCheckTCP, 558 Interval: 10 * time.Second, 559 Timeout: 2 * time.Second, 560 } 561 562 err := check1.validate() 563 if err != nil { 564 t.Fatal("err: %v", err) 565 } 566 567 check1.InitialStatus = "foo" 568 err = check1.validate() 569 if err == nil { 570 t.Fatal("Expected an error") 571 } 572 573 if !strings.Contains(err.Error(), "invalid initial check state (foo)") { 574 t.Fatalf("err: %v", err) 575 } 576 577 check1.InitialStatus = api.HealthCritical 578 err = check1.validate() 579 if err != nil { 580 t.Fatalf("err: %v", err) 581 } 582 583 check1.InitialStatus = api.HealthPassing 584 err = check1.validate() 585 if err != nil { 586 t.Fatalf("err: %v", err) 587 } 588 589 check1.InitialStatus = "" 590 err = check1.validate() 591 if err != nil { 592 t.Fatalf("err: %v", err) 593 } 594 } 595 596 func TestTask_Validate_LogConfig(t *testing.T) { 597 task := &Task{ 598 LogConfig: DefaultLogConfig(), 599 } 600 ephemeralDisk := &EphemeralDisk{ 601 SizeMB: 1, 602 } 603 604 err := task.Validate(ephemeralDisk) 605 mErr := err.(*multierror.Error) 606 if !strings.Contains(mErr.Errors[3].Error(), "log storage") { 607 t.Fatalf("err: %s", err) 608 } 609 } 610 611 func TestTask_Validate_Template(t *testing.T) { 612 613 bad := &Template{} 614 task := &Task{ 615 Templates: []*Template{bad}, 616 } 617 ephemeralDisk := &EphemeralDisk{ 618 SizeMB: 1, 619 } 620 621 err := task.Validate(ephemeralDisk) 622 if !strings.Contains(err.Error(), "Template 1 validation failed") { 623 t.Fatalf("err: %s", err) 624 } 625 626 // Have two templates that share the same destination 627 good := &Template{ 628 SourcePath: "foo", 629 DestPath: "local/foo", 630 ChangeMode: "noop", 631 } 632 633 task.Templates = []*Template{good, good} 634 err = task.Validate(ephemeralDisk) 635 if !strings.Contains(err.Error(), "same destination as") { 636 t.Fatalf("err: %s", err) 637 } 638 } 639 640 func TestTemplate_Validate(t *testing.T) { 641 cases := []struct { 642 Tmpl *Template 643 Fail bool 644 ContainsErrs []string 645 }{ 646 { 647 Tmpl: &Template{}, 648 Fail: true, 649 ContainsErrs: []string{ 650 "specify a source path", 651 "specify a destination", 652 TemplateChangeModeInvalidError.Error(), 653 }, 654 }, 655 { 656 Tmpl: &Template{ 657 Splay: -100, 658 }, 659 Fail: true, 660 ContainsErrs: []string{ 661 "positive splay", 662 }, 663 }, 664 { 665 Tmpl: &Template{ 666 ChangeMode: "foo", 667 }, 668 Fail: true, 669 ContainsErrs: []string{ 670 TemplateChangeModeInvalidError.Error(), 671 }, 672 }, 673 { 674 Tmpl: &Template{ 675 ChangeMode: "signal", 676 }, 677 Fail: true, 678 ContainsErrs: []string{ 679 "specify signal value", 680 }, 681 }, 682 { 683 Tmpl: &Template{ 684 SourcePath: "foo", 685 DestPath: "../../root", 686 ChangeMode: "noop", 687 }, 688 Fail: true, 689 ContainsErrs: []string{ 690 "destination escapes", 691 }, 692 }, 693 { 694 Tmpl: &Template{ 695 SourcePath: "foo", 696 DestPath: "local/foo", 697 ChangeMode: "noop", 698 }, 699 Fail: false, 700 }, 701 } 702 703 for i, c := range cases { 704 err := c.Tmpl.Validate() 705 if err != nil { 706 if !c.Fail { 707 t.Fatalf("Case %d: shouldn't have failed: %v", i+1, err) 708 } 709 710 e := err.Error() 711 for _, exp := range c.ContainsErrs { 712 if !strings.Contains(e, exp) { 713 t.Fatalf("Cased %d: should have contained error %q: %q", i+1, exp, e) 714 } 715 } 716 } else if c.Fail { 717 t.Fatalf("Case %d: should have failed: %v", i+1, err) 718 } 719 } 720 } 721 722 func TestConstraint_Validate(t *testing.T) { 723 c := &Constraint{} 724 err := c.Validate() 725 mErr := err.(*multierror.Error) 726 if !strings.Contains(mErr.Errors[0].Error(), "Missing constraint operand") { 727 t.Fatalf("err: %s", err) 728 } 729 730 c = &Constraint{ 731 LTarget: "$attr.kernel.name", 732 RTarget: "linux", 733 Operand: "=", 734 } 735 err = c.Validate() 736 if err != nil { 737 t.Fatalf("err: %v", err) 738 } 739 740 // Perform additional regexp validation 741 c.Operand = ConstraintRegex 742 c.RTarget = "(foo" 743 err = c.Validate() 744 mErr = err.(*multierror.Error) 745 if !strings.Contains(mErr.Errors[0].Error(), "missing closing") { 746 t.Fatalf("err: %s", err) 747 } 748 749 // Perform version validation 750 c.Operand = ConstraintVersion 751 c.RTarget = "~> foo" 752 err = c.Validate() 753 mErr = err.(*multierror.Error) 754 if !strings.Contains(mErr.Errors[0].Error(), "Malformed constraint") { 755 t.Fatalf("err: %s", err) 756 } 757 } 758 759 func TestResource_NetIndex(t *testing.T) { 760 r := &Resources{ 761 Networks: []*NetworkResource{ 762 &NetworkResource{Device: "eth0"}, 763 &NetworkResource{Device: "lo0"}, 764 &NetworkResource{Device: ""}, 765 }, 766 } 767 if idx := r.NetIndex(&NetworkResource{Device: "eth0"}); idx != 0 { 768 t.Fatalf("Bad: %d", idx) 769 } 770 if idx := r.NetIndex(&NetworkResource{Device: "lo0"}); idx != 1 { 771 t.Fatalf("Bad: %d", idx) 772 } 773 if idx := r.NetIndex(&NetworkResource{Device: "eth1"}); idx != -1 { 774 t.Fatalf("Bad: %d", idx) 775 } 776 } 777 778 func TestResource_Superset(t *testing.T) { 779 r1 := &Resources{ 780 CPU: 2000, 781 MemoryMB: 2048, 782 DiskMB: 10000, 783 IOPS: 100, 784 } 785 r2 := &Resources{ 786 CPU: 2000, 787 MemoryMB: 1024, 788 DiskMB: 5000, 789 IOPS: 50, 790 } 791 792 if s, _ := r1.Superset(r1); !s { 793 t.Fatalf("bad") 794 } 795 if s, _ := r1.Superset(r2); !s { 796 t.Fatalf("bad") 797 } 798 if s, _ := r2.Superset(r1); s { 799 t.Fatalf("bad") 800 } 801 if s, _ := r2.Superset(r2); !s { 802 t.Fatalf("bad") 803 } 804 } 805 806 func TestResource_Add(t *testing.T) { 807 r1 := &Resources{ 808 CPU: 2000, 809 MemoryMB: 2048, 810 DiskMB: 10000, 811 IOPS: 100, 812 Networks: []*NetworkResource{ 813 &NetworkResource{ 814 CIDR: "10.0.0.0/8", 815 MBits: 100, 816 ReservedPorts: []Port{{"ssh", 22}}, 817 }, 818 }, 819 } 820 r2 := &Resources{ 821 CPU: 2000, 822 MemoryMB: 1024, 823 DiskMB: 5000, 824 IOPS: 50, 825 Networks: []*NetworkResource{ 826 &NetworkResource{ 827 IP: "10.0.0.1", 828 MBits: 50, 829 ReservedPorts: []Port{{"web", 80}}, 830 }, 831 }, 832 } 833 834 err := r1.Add(r2) 835 if err != nil { 836 t.Fatalf("Err: %v", err) 837 } 838 839 expect := &Resources{ 840 CPU: 3000, 841 MemoryMB: 3072, 842 DiskMB: 15000, 843 IOPS: 150, 844 Networks: []*NetworkResource{ 845 &NetworkResource{ 846 CIDR: "10.0.0.0/8", 847 MBits: 150, 848 ReservedPorts: []Port{{"ssh", 22}, {"web", 80}}, 849 }, 850 }, 851 } 852 853 if !reflect.DeepEqual(expect.Networks, r1.Networks) { 854 t.Fatalf("bad: %#v %#v", expect, r1) 855 } 856 } 857 858 func TestResource_Add_Network(t *testing.T) { 859 r1 := &Resources{} 860 r2 := &Resources{ 861 Networks: []*NetworkResource{ 862 &NetworkResource{ 863 MBits: 50, 864 DynamicPorts: []Port{{"http", 0}, {"https", 0}}, 865 }, 866 }, 867 } 868 r3 := &Resources{ 869 Networks: []*NetworkResource{ 870 &NetworkResource{ 871 MBits: 25, 872 DynamicPorts: []Port{{"admin", 0}}, 873 }, 874 }, 875 } 876 877 err := r1.Add(r2) 878 if err != nil { 879 t.Fatalf("Err: %v", err) 880 } 881 err = r1.Add(r3) 882 if err != nil { 883 t.Fatalf("Err: %v", err) 884 } 885 886 expect := &Resources{ 887 Networks: []*NetworkResource{ 888 &NetworkResource{ 889 MBits: 75, 890 DynamicPorts: []Port{{"http", 0}, {"https", 0}, {"admin", 0}}, 891 }, 892 }, 893 } 894 895 if !reflect.DeepEqual(expect.Networks, r1.Networks) { 896 t.Fatalf("bad: %#v %#v", expect.Networks[0], r1.Networks[0]) 897 } 898 } 899 900 func TestEncodeDecode(t *testing.T) { 901 type FooRequest struct { 902 Foo string 903 Bar int 904 Baz bool 905 } 906 arg := &FooRequest{ 907 Foo: "test", 908 Bar: 42, 909 Baz: true, 910 } 911 buf, err := Encode(1, arg) 912 if err != nil { 913 t.Fatalf("err: %v", err) 914 } 915 916 var out FooRequest 917 err = Decode(buf[1:], &out) 918 if err != nil { 919 t.Fatalf("err: %v", err) 920 } 921 922 if !reflect.DeepEqual(arg, &out) { 923 t.Fatalf("bad: %#v %#v", arg, out) 924 } 925 } 926 927 func BenchmarkEncodeDecode(b *testing.B) { 928 job := testJob() 929 930 for i := 0; i < b.N; i++ { 931 buf, err := Encode(1, job) 932 if err != nil { 933 b.Fatalf("err: %v", err) 934 } 935 936 var out Job 937 err = Decode(buf[1:], &out) 938 if err != nil { 939 b.Fatalf("err: %v", err) 940 } 941 } 942 } 943 944 func TestInvalidServiceCheck(t *testing.T) { 945 s := Service{ 946 Name: "service-name", 947 PortLabel: "bar", 948 Checks: []*ServiceCheck{ 949 { 950 Name: "check-name", 951 Type: "lol", 952 }, 953 }, 954 } 955 if err := s.Validate(); err == nil { 956 t.Fatalf("Service should be invalid (invalid type)") 957 } 958 959 s = Service{ 960 Name: "service.name", 961 PortLabel: "bar", 962 } 963 if err := s.ValidateName(s.Name); err == nil { 964 t.Fatalf("Service should be invalid (contains a dot): %v", err) 965 } 966 967 s = Service{ 968 Name: "-my-service", 969 PortLabel: "bar", 970 } 971 if err := s.Validate(); err == nil { 972 t.Fatalf("Service should be invalid (begins with a hyphen): %v", err) 973 } 974 975 s = Service{ 976 Name: "my-service-${NOMAD_META_FOO}", 977 PortLabel: "bar", 978 } 979 if err := s.Validate(); err != nil { 980 t.Fatalf("Service should be valid: %v", err) 981 } 982 983 s = Service{ 984 Name: "abcdef0123456789-abcdef0123456789-abcdef0123456789-abcdef0123456", 985 PortLabel: "bar", 986 } 987 if err := s.ValidateName(s.Name); err == nil { 988 t.Fatalf("Service should be invalid (too long): %v", err) 989 } 990 991 s = Service{ 992 Name: "service-name", 993 Checks: []*ServiceCheck{ 994 { 995 Name: "check-tcp", 996 Type: ServiceCheckTCP, 997 Interval: 5 * time.Second, 998 Timeout: 2 * time.Second, 999 }, 1000 { 1001 Name: "check-http", 1002 Type: ServiceCheckHTTP, 1003 Path: "/foo", 1004 Interval: 5 * time.Second, 1005 Timeout: 2 * time.Second, 1006 }, 1007 }, 1008 } 1009 if err := s.Validate(); err == nil { 1010 t.Fatalf("service should be invalid (tcp/http checks with no port): %v", err) 1011 } 1012 1013 s = Service{ 1014 Name: "service-name", 1015 Checks: []*ServiceCheck{ 1016 { 1017 Name: "check-script", 1018 Type: ServiceCheckScript, 1019 Command: "/bin/date", 1020 Interval: 5 * time.Second, 1021 Timeout: 2 * time.Second, 1022 }, 1023 }, 1024 } 1025 if err := s.Validate(); err != nil { 1026 t.Fatalf("un-expected error: %v", err) 1027 } 1028 } 1029 1030 func TestDistinctCheckID(t *testing.T) { 1031 c1 := ServiceCheck{ 1032 Name: "web-health", 1033 Type: "http", 1034 Path: "/health", 1035 Interval: 2 * time.Second, 1036 Timeout: 3 * time.Second, 1037 } 1038 c2 := ServiceCheck{ 1039 Name: "web-health", 1040 Type: "http", 1041 Path: "/health1", 1042 Interval: 2 * time.Second, 1043 Timeout: 3 * time.Second, 1044 } 1045 1046 c3 := ServiceCheck{ 1047 Name: "web-health", 1048 Type: "http", 1049 Path: "/health", 1050 Interval: 4 * time.Second, 1051 Timeout: 3 * time.Second, 1052 } 1053 serviceID := "123" 1054 c1Hash := c1.Hash(serviceID) 1055 c2Hash := c2.Hash(serviceID) 1056 c3Hash := c3.Hash(serviceID) 1057 1058 if c1Hash == c2Hash || c1Hash == c3Hash || c3Hash == c2Hash { 1059 t.Fatalf("Checks need to be uniq c1: %s, c2: %s, c3: %s", c1Hash, c2Hash, c3Hash) 1060 } 1061 1062 } 1063 1064 func TestService_Canonicalize(t *testing.T) { 1065 job := "example" 1066 taskGroup := "cache" 1067 task := "redis" 1068 1069 s := Service{ 1070 Name: "${TASK}-db", 1071 } 1072 1073 s.Canonicalize(job, taskGroup, task) 1074 if s.Name != "redis-db" { 1075 t.Fatalf("Expected name: %v, Actual: %v", "redis-db", s.Name) 1076 } 1077 1078 s.Name = "db" 1079 s.Canonicalize(job, taskGroup, task) 1080 if s.Name != "db" { 1081 t.Fatalf("Expected name: %v, Actual: %v", "redis-db", s.Name) 1082 } 1083 1084 s.Name = "${JOB}-${TASKGROUP}-${TASK}-db" 1085 s.Canonicalize(job, taskGroup, task) 1086 if s.Name != "example-cache-redis-db" { 1087 t.Fatalf("Expected name: %v, Actual: %v", "expample-cache-redis-db", s.Name) 1088 } 1089 1090 s.Name = "${BASE}-db" 1091 s.Canonicalize(job, taskGroup, task) 1092 if s.Name != "example-cache-redis-db" { 1093 t.Fatalf("Expected name: %v, Actual: %v", "expample-cache-redis-db", s.Name) 1094 } 1095 1096 } 1097 1098 func TestJob_ExpandServiceNames(t *testing.T) { 1099 j := &Job{ 1100 Name: "my-job", 1101 TaskGroups: []*TaskGroup{ 1102 &TaskGroup{ 1103 Name: "web", 1104 Tasks: []*Task{ 1105 { 1106 Name: "frontend", 1107 Services: []*Service{ 1108 { 1109 Name: "${BASE}-default", 1110 }, 1111 { 1112 Name: "jmx", 1113 }, 1114 }, 1115 }, 1116 }, 1117 }, 1118 &TaskGroup{ 1119 Name: "admin", 1120 Tasks: []*Task{ 1121 { 1122 Name: "admin-web", 1123 }, 1124 }, 1125 }, 1126 }, 1127 } 1128 1129 j.Canonicalize() 1130 1131 service1Name := j.TaskGroups[0].Tasks[0].Services[0].Name 1132 if service1Name != "my-job-web-frontend-default" { 1133 t.Fatalf("Expected Service Name: %s, Actual: %s", "my-job-web-frontend-default", service1Name) 1134 } 1135 1136 service2Name := j.TaskGroups[0].Tasks[0].Services[1].Name 1137 if service2Name != "jmx" { 1138 t.Fatalf("Expected Service Name: %s, Actual: %s", "jmx", service2Name) 1139 } 1140 1141 } 1142 1143 func TestPeriodicConfig_EnabledInvalid(t *testing.T) { 1144 // Create a config that is enabled but with no interval specified. 1145 p := &PeriodicConfig{Enabled: true} 1146 if err := p.Validate(); err == nil { 1147 t.Fatal("Enabled PeriodicConfig with no spec or type shouldn't be valid") 1148 } 1149 1150 // Create a config that is enabled, with a spec but no type specified. 1151 p = &PeriodicConfig{Enabled: true, Spec: "foo"} 1152 if err := p.Validate(); err == nil { 1153 t.Fatal("Enabled PeriodicConfig with no spec type shouldn't be valid") 1154 } 1155 1156 // Create a config that is enabled, with a spec type but no spec specified. 1157 p = &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron} 1158 if err := p.Validate(); err == nil { 1159 t.Fatal("Enabled PeriodicConfig with no spec shouldn't be valid") 1160 } 1161 } 1162 1163 func TestPeriodicConfig_InvalidCron(t *testing.T) { 1164 specs := []string{"foo", "* *", "@foo"} 1165 for _, spec := range specs { 1166 p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec} 1167 if err := p.Validate(); err == nil { 1168 t.Fatal("Invalid cron spec") 1169 } 1170 } 1171 } 1172 1173 func TestPeriodicConfig_ValidCron(t *testing.T) { 1174 specs := []string{"0 0 29 2 *", "@hourly", "0 0-15 * * *"} 1175 for _, spec := range specs { 1176 p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec} 1177 if err := p.Validate(); err != nil { 1178 t.Fatal("Passed valid cron") 1179 } 1180 } 1181 } 1182 1183 func TestPeriodicConfig_NextCron(t *testing.T) { 1184 from := time.Date(2009, time.November, 10, 23, 22, 30, 0, time.UTC) 1185 specs := []string{"0 0 29 2 * 1980", "*/5 * * * *"} 1186 expected := []time.Time{time.Time{}, time.Date(2009, time.November, 10, 23, 25, 0, 0, time.UTC)} 1187 for i, spec := range specs { 1188 p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec} 1189 n := p.Next(from) 1190 if expected[i] != n { 1191 t.Fatalf("Next(%v) returned %v; want %v", from, n, expected[i]) 1192 } 1193 } 1194 } 1195 1196 func TestRestartPolicy_Validate(t *testing.T) { 1197 // Policy with acceptable restart options passes 1198 p := &RestartPolicy{ 1199 Mode: RestartPolicyModeFail, 1200 Attempts: 0, 1201 } 1202 if err := p.Validate(); err != nil { 1203 t.Fatalf("err: %v", err) 1204 } 1205 1206 // Policy with ambiguous restart options fails 1207 p = &RestartPolicy{ 1208 Mode: RestartPolicyModeDelay, 1209 Attempts: 0, 1210 } 1211 if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "ambiguous") { 1212 t.Fatalf("expect ambiguity error, got: %v", err) 1213 } 1214 1215 // Bad policy mode fails 1216 p = &RestartPolicy{ 1217 Mode: "nope", 1218 Attempts: 1, 1219 } 1220 if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "mode") { 1221 t.Fatalf("expect mode error, got: %v", err) 1222 } 1223 1224 // Fails when attempts*delay does not fit inside interval 1225 p = &RestartPolicy{ 1226 Mode: RestartPolicyModeDelay, 1227 Attempts: 3, 1228 Delay: 5 * time.Second, 1229 Interval: time.Second, 1230 } 1231 if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "can't restart") { 1232 t.Fatalf("expect restart interval error, got: %v", err) 1233 } 1234 } 1235 1236 func TestAllocation_Index(t *testing.T) { 1237 a1 := Allocation{Name: "example.cache[0]"} 1238 e1 := 0 1239 a2 := Allocation{Name: "ex[123]am123ple.c311ac[123]he12[1][77]"} 1240 e2 := 77 1241 1242 if a1.Index() != e1 || a2.Index() != e2 { 1243 t.Fatal() 1244 } 1245 } 1246 1247 func TestTaskArtifact_Validate_Source(t *testing.T) { 1248 valid := &TaskArtifact{GetterSource: "google.com"} 1249 if err := valid.Validate(); err != nil { 1250 t.Fatalf("unexpected error: %v", err) 1251 } 1252 } 1253 1254 func TestTaskArtifact_Validate_Dest(t *testing.T) { 1255 valid := &TaskArtifact{GetterSource: "google.com"} 1256 if err := valid.Validate(); err != nil { 1257 t.Fatalf("unexpected error: %v", err) 1258 } 1259 1260 valid.RelativeDest = "local/" 1261 if err := valid.Validate(); err != nil { 1262 t.Fatalf("unexpected error: %v", err) 1263 } 1264 1265 valid.RelativeDest = "local/.." 1266 if err := valid.Validate(); err != nil { 1267 t.Fatalf("unexpected error: %v", err) 1268 } 1269 1270 valid.RelativeDest = "local/../.." 1271 if err := valid.Validate(); err == nil { 1272 t.Fatalf("expected error: %v", err) 1273 } 1274 } 1275 1276 func TestAllocation_ShouldMigrate(t *testing.T) { 1277 alloc := Allocation{ 1278 TaskGroup: "foo", 1279 Job: &Job{ 1280 TaskGroups: []*TaskGroup{ 1281 { 1282 Name: "foo", 1283 EphemeralDisk: &EphemeralDisk{ 1284 Migrate: true, 1285 Sticky: true, 1286 }, 1287 }, 1288 }, 1289 }, 1290 } 1291 1292 if !alloc.ShouldMigrate() { 1293 t.Fatalf("bad: %v", alloc) 1294 } 1295 1296 alloc1 := Allocation{ 1297 TaskGroup: "foo", 1298 Job: &Job{ 1299 TaskGroups: []*TaskGroup{ 1300 { 1301 Name: "foo", 1302 EphemeralDisk: &EphemeralDisk{}, 1303 }, 1304 }, 1305 }, 1306 } 1307 1308 if alloc1.ShouldMigrate() { 1309 t.Fatalf("bad: %v", alloc) 1310 } 1311 1312 alloc2 := Allocation{ 1313 TaskGroup: "foo", 1314 Job: &Job{ 1315 TaskGroups: []*TaskGroup{ 1316 { 1317 Name: "foo", 1318 EphemeralDisk: &EphemeralDisk{ 1319 Sticky: false, 1320 Migrate: true, 1321 }, 1322 }, 1323 }, 1324 }, 1325 } 1326 1327 if alloc2.ShouldMigrate() { 1328 t.Fatalf("bad: %v", alloc) 1329 } 1330 1331 alloc3 := Allocation{ 1332 TaskGroup: "foo", 1333 Job: &Job{ 1334 TaskGroups: []*TaskGroup{ 1335 { 1336 Name: "foo", 1337 }, 1338 }, 1339 }, 1340 } 1341 1342 if alloc3.ShouldMigrate() { 1343 t.Fatalf("bad: %v", alloc) 1344 } 1345 } 1346 1347 func TestTaskArtifact_Validate_Checksum(t *testing.T) { 1348 cases := []struct { 1349 Input *TaskArtifact 1350 Err bool 1351 }{ 1352 { 1353 &TaskArtifact{ 1354 GetterSource: "foo.com", 1355 GetterOptions: map[string]string{ 1356 "checksum": "no-type", 1357 }, 1358 }, 1359 true, 1360 }, 1361 { 1362 &TaskArtifact{ 1363 GetterSource: "foo.com", 1364 GetterOptions: map[string]string{ 1365 "checksum": "md5:toosmall", 1366 }, 1367 }, 1368 true, 1369 }, 1370 { 1371 &TaskArtifact{ 1372 GetterSource: "foo.com", 1373 GetterOptions: map[string]string{ 1374 "checksum": "invalid:type", 1375 }, 1376 }, 1377 true, 1378 }, 1379 } 1380 1381 for i, tc := range cases { 1382 err := tc.Input.Validate() 1383 if (err != nil) != tc.Err { 1384 t.Fatalf("case %d: %v", i, err) 1385 continue 1386 } 1387 } 1388 } 1389 1390 func TestAllocation_Terminated(t *testing.T) { 1391 type desiredState struct { 1392 ClientStatus string 1393 DesiredStatus string 1394 Terminated bool 1395 } 1396 1397 harness := []desiredState{ 1398 { 1399 ClientStatus: AllocClientStatusPending, 1400 DesiredStatus: AllocDesiredStatusStop, 1401 Terminated: false, 1402 }, 1403 { 1404 ClientStatus: AllocClientStatusRunning, 1405 DesiredStatus: AllocDesiredStatusStop, 1406 Terminated: false, 1407 }, 1408 { 1409 ClientStatus: AllocClientStatusFailed, 1410 DesiredStatus: AllocDesiredStatusStop, 1411 Terminated: true, 1412 }, 1413 { 1414 ClientStatus: AllocClientStatusFailed, 1415 DesiredStatus: AllocDesiredStatusRun, 1416 Terminated: true, 1417 }, 1418 } 1419 1420 for _, state := range harness { 1421 alloc := Allocation{} 1422 alloc.DesiredStatus = state.DesiredStatus 1423 alloc.ClientStatus = state.ClientStatus 1424 if alloc.Terminated() != state.Terminated { 1425 t.Fatalf("expected: %v, actual: %v", state.Terminated, alloc.Terminated()) 1426 } 1427 } 1428 } 1429 1430 func TestVault_Validate(t *testing.T) { 1431 v := &Vault{ 1432 Env: true, 1433 ChangeMode: VaultChangeModeNoop, 1434 } 1435 1436 if err := v.Validate(); err == nil || !strings.Contains(err.Error(), "Policy list") { 1437 t.Fatalf("Expected policy list empty error") 1438 } 1439 1440 v.Policies = []string{"foo"} 1441 v.ChangeMode = VaultChangeModeSignal 1442 1443 if err := v.Validate(); err == nil || !strings.Contains(err.Error(), "Signal must") { 1444 t.Fatalf("Expected signal empty error") 1445 } 1446 }