github.com/ranjib/nomad@v0.1.1-0.20160225204057-97751b02f70b/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/go-multierror" 10 ) 11 12 func TestJob_Validate(t *testing.T) { 13 j := &Job{} 14 err := j.Validate() 15 mErr := err.(*multierror.Error) 16 if !strings.Contains(mErr.Errors[0].Error(), "job region") { 17 t.Fatalf("err: %s", err) 18 } 19 if !strings.Contains(mErr.Errors[1].Error(), "job ID") { 20 t.Fatalf("err: %s", err) 21 } 22 if !strings.Contains(mErr.Errors[2].Error(), "job name") { 23 t.Fatalf("err: %s", err) 24 } 25 if !strings.Contains(mErr.Errors[3].Error(), "job type") { 26 t.Fatalf("err: %s", err) 27 } 28 if !strings.Contains(mErr.Errors[4].Error(), "priority") { 29 t.Fatalf("err: %s", err) 30 } 31 if !strings.Contains(mErr.Errors[5].Error(), "datacenters") { 32 t.Fatalf("err: %s", err) 33 } 34 if !strings.Contains(mErr.Errors[6].Error(), "task groups") { 35 t.Fatalf("err: %s", err) 36 } 37 38 j = &Job{ 39 Type: JobTypeService, 40 Periodic: &PeriodicConfig{ 41 Enabled: true, 42 }, 43 } 44 err = j.Validate() 45 mErr = err.(*multierror.Error) 46 if !strings.Contains(mErr.Error(), "Periodic") { 47 t.Fatalf("err: %s", err) 48 } 49 50 j = &Job{ 51 Region: "global", 52 ID: GenerateUUID(), 53 Name: "my-job", 54 Type: JobTypeService, 55 Priority: 50, 56 Datacenters: []string{"dc1"}, 57 TaskGroups: []*TaskGroup{ 58 &TaskGroup{ 59 Name: "web", 60 RestartPolicy: &RestartPolicy{ 61 Interval: 5 * time.Minute, 62 Delay: 10 * time.Second, 63 Attempts: 10, 64 }, 65 }, 66 &TaskGroup{ 67 Name: "web", 68 RestartPolicy: &RestartPolicy{ 69 Interval: 5 * time.Minute, 70 Delay: 10 * time.Second, 71 Attempts: 10, 72 }, 73 }, 74 &TaskGroup{ 75 RestartPolicy: &RestartPolicy{ 76 Interval: 5 * time.Minute, 77 Delay: 10 * time.Second, 78 Attempts: 10, 79 }, 80 }, 81 }, 82 } 83 err = j.Validate() 84 mErr = err.(*multierror.Error) 85 if !strings.Contains(mErr.Errors[0].Error(), "2 redefines 'web' from group 1") { 86 t.Fatalf("err: %s", err) 87 } 88 if !strings.Contains(mErr.Errors[1].Error(), "group 3 missing name") { 89 t.Fatalf("err: %s", err) 90 } 91 if !strings.Contains(mErr.Errors[2].Error(), "Task group 1 validation failed") { 92 t.Fatalf("err: %s", err) 93 } 94 } 95 96 func testJob() *Job { 97 return &Job{ 98 Region: "global", 99 ID: GenerateUUID(), 100 Name: "my-job", 101 Type: JobTypeService, 102 Priority: 50, 103 AllAtOnce: false, 104 Datacenters: []string{"dc1"}, 105 Constraints: []*Constraint{ 106 &Constraint{ 107 LTarget: "$attr.kernel.name", 108 RTarget: "linux", 109 Operand: "=", 110 }, 111 }, 112 Periodic: &PeriodicConfig{ 113 Enabled: false, 114 }, 115 TaskGroups: []*TaskGroup{ 116 &TaskGroup{ 117 Name: "web", 118 Count: 10, 119 RestartPolicy: &RestartPolicy{ 120 Attempts: 3, 121 Interval: 10 * time.Minute, 122 Delay: 1 * time.Minute, 123 }, 124 Tasks: []*Task{ 125 &Task{ 126 Name: "web", 127 Driver: "exec", 128 Config: map[string]interface{}{ 129 "command": "/bin/date", 130 }, 131 Env: map[string]string{ 132 "FOO": "bar", 133 }, 134 Services: []*Service{ 135 { 136 Name: "${TASK}-frontend", 137 PortLabel: "http", 138 }, 139 }, 140 Resources: &Resources{ 141 CPU: 500, 142 MemoryMB: 256, 143 Networks: []*NetworkResource{ 144 &NetworkResource{ 145 MBits: 50, 146 DynamicPorts: []Port{{Label: "http"}}, 147 }, 148 }, 149 }, 150 }, 151 }, 152 Meta: map[string]string{ 153 "elb_check_type": "http", 154 "elb_check_interval": "30s", 155 "elb_check_min": "3", 156 }, 157 }, 158 }, 159 Meta: map[string]string{ 160 "owner": "armon", 161 }, 162 } 163 } 164 165 func TestJob_Copy(t *testing.T) { 166 j := testJob() 167 c := j.Copy() 168 if !reflect.DeepEqual(j, c) { 169 t.Fatalf("Copy() returned an unequal Job; got %#v; want %#v", c, j) 170 } 171 } 172 173 func TestJob_IsPeriodic(t *testing.T) { 174 j := &Job{ 175 Type: JobTypeService, 176 Periodic: &PeriodicConfig{ 177 Enabled: true, 178 }, 179 } 180 if !j.IsPeriodic() { 181 t.Fatalf("IsPeriodic() returned false on periodic job") 182 } 183 184 j = &Job{ 185 Type: JobTypeService, 186 } 187 if j.IsPeriodic() { 188 t.Fatalf("IsPeriodic() returned true on non-periodic job") 189 } 190 } 191 192 func TestTaskGroup_Validate(t *testing.T) { 193 tg := &TaskGroup{ 194 RestartPolicy: &RestartPolicy{ 195 Interval: 5 * time.Minute, 196 Delay: 10 * time.Second, 197 Attempts: 10, 198 Mode: RestartPolicyModeDelay, 199 }, 200 } 201 err := tg.Validate() 202 mErr := err.(*multierror.Error) 203 if !strings.Contains(mErr.Errors[0].Error(), "group name") { 204 t.Fatalf("err: %s", err) 205 } 206 if !strings.Contains(mErr.Errors[1].Error(), "count must be positive") { 207 t.Fatalf("err: %s", err) 208 } 209 if !strings.Contains(mErr.Errors[2].Error(), "Missing tasks") { 210 t.Fatalf("err: %s", err) 211 } 212 213 tg = &TaskGroup{ 214 Name: "web", 215 Count: 1, 216 Tasks: []*Task{ 217 &Task{Name: "web"}, 218 &Task{Name: "web"}, 219 &Task{}, 220 }, 221 RestartPolicy: &RestartPolicy{ 222 Interval: 5 * time.Minute, 223 Delay: 10 * time.Second, 224 Attempts: 10, 225 Mode: RestartPolicyModeDelay, 226 }, 227 } 228 err = tg.Validate() 229 mErr = err.(*multierror.Error) 230 if !strings.Contains(mErr.Errors[0].Error(), "2 redefines 'web' from task 1") { 231 t.Fatalf("err: %s", err) 232 } 233 if !strings.Contains(mErr.Errors[1].Error(), "Task 3 missing name") { 234 t.Fatalf("err: %s", err) 235 } 236 if !strings.Contains(mErr.Errors[2].Error(), "Task 1 validation failed") { 237 t.Fatalf("err: %s", err) 238 } 239 } 240 241 func TestTask_Validate(t *testing.T) { 242 task := &Task{} 243 err := task.Validate() 244 mErr := err.(*multierror.Error) 245 if !strings.Contains(mErr.Errors[0].Error(), "task name") { 246 t.Fatalf("err: %s", err) 247 } 248 if !strings.Contains(mErr.Errors[1].Error(), "task driver") { 249 t.Fatalf("err: %s", err) 250 } 251 if !strings.Contains(mErr.Errors[2].Error(), "task resources") { 252 t.Fatalf("err: %s", err) 253 } 254 255 task = &Task{ 256 Name: "web", 257 Driver: "docker", 258 Resources: &Resources{ 259 CPU: 100, 260 DiskMB: 200, 261 MemoryMB: 100, 262 IOPS: 10, 263 }, 264 LogConfig: DefaultLogConfig(), 265 } 266 err = task.Validate() 267 if err != nil { 268 t.Fatalf("err: %s", err) 269 } 270 } 271 272 func TestTask_Validate_LogConfig(t *testing.T) { 273 task := &Task{ 274 LogConfig: DefaultLogConfig(), 275 Resources: &Resources{ 276 DiskMB: 1, 277 }, 278 } 279 280 err := task.Validate() 281 mErr := err.(*multierror.Error) 282 if !strings.Contains(mErr.Errors[3].Error(), "log storage") { 283 t.Fatalf("err: %s", err) 284 } 285 } 286 287 func TestConstraint_Validate(t *testing.T) { 288 c := &Constraint{} 289 err := c.Validate() 290 mErr := err.(*multierror.Error) 291 if !strings.Contains(mErr.Errors[0].Error(), "Missing constraint operand") { 292 t.Fatalf("err: %s", err) 293 } 294 295 c = &Constraint{ 296 LTarget: "$attr.kernel.name", 297 RTarget: "linux", 298 Operand: "=", 299 } 300 err = c.Validate() 301 if err != nil { 302 t.Fatalf("err: %v", err) 303 } 304 305 // Perform additional regexp validation 306 c.Operand = ConstraintRegex 307 c.RTarget = "(foo" 308 err = c.Validate() 309 mErr = err.(*multierror.Error) 310 if !strings.Contains(mErr.Errors[0].Error(), "missing closing") { 311 t.Fatalf("err: %s", err) 312 } 313 314 // Perform version validation 315 c.Operand = ConstraintVersion 316 c.RTarget = "~> foo" 317 err = c.Validate() 318 mErr = err.(*multierror.Error) 319 if !strings.Contains(mErr.Errors[0].Error(), "Malformed constraint") { 320 t.Fatalf("err: %s", err) 321 } 322 } 323 324 func TestResource_NetIndex(t *testing.T) { 325 r := &Resources{ 326 Networks: []*NetworkResource{ 327 &NetworkResource{Device: "eth0"}, 328 &NetworkResource{Device: "lo0"}, 329 &NetworkResource{Device: ""}, 330 }, 331 } 332 if idx := r.NetIndex(&NetworkResource{Device: "eth0"}); idx != 0 { 333 t.Fatalf("Bad: %d", idx) 334 } 335 if idx := r.NetIndex(&NetworkResource{Device: "lo0"}); idx != 1 { 336 t.Fatalf("Bad: %d", idx) 337 } 338 if idx := r.NetIndex(&NetworkResource{Device: "eth1"}); idx != -1 { 339 t.Fatalf("Bad: %d", idx) 340 } 341 } 342 343 func TestResource_Superset(t *testing.T) { 344 r1 := &Resources{ 345 CPU: 2000, 346 MemoryMB: 2048, 347 DiskMB: 10000, 348 IOPS: 100, 349 } 350 r2 := &Resources{ 351 CPU: 2000, 352 MemoryMB: 1024, 353 DiskMB: 5000, 354 IOPS: 50, 355 } 356 357 if s, _ := r1.Superset(r1); !s { 358 t.Fatalf("bad") 359 } 360 if s, _ := r1.Superset(r2); !s { 361 t.Fatalf("bad") 362 } 363 if s, _ := r2.Superset(r1); s { 364 t.Fatalf("bad") 365 } 366 if s, _ := r2.Superset(r2); !s { 367 t.Fatalf("bad") 368 } 369 } 370 371 func TestResource_Add(t *testing.T) { 372 r1 := &Resources{ 373 CPU: 2000, 374 MemoryMB: 2048, 375 DiskMB: 10000, 376 IOPS: 100, 377 Networks: []*NetworkResource{ 378 &NetworkResource{ 379 CIDR: "10.0.0.0/8", 380 MBits: 100, 381 ReservedPorts: []Port{{"ssh", 22}}, 382 }, 383 }, 384 } 385 r2 := &Resources{ 386 CPU: 2000, 387 MemoryMB: 1024, 388 DiskMB: 5000, 389 IOPS: 50, 390 Networks: []*NetworkResource{ 391 &NetworkResource{ 392 IP: "10.0.0.1", 393 MBits: 50, 394 ReservedPorts: []Port{{"web", 80}}, 395 }, 396 }, 397 } 398 399 err := r1.Add(r2) 400 if err != nil { 401 t.Fatalf("Err: %v", err) 402 } 403 404 expect := &Resources{ 405 CPU: 3000, 406 MemoryMB: 3072, 407 DiskMB: 15000, 408 IOPS: 150, 409 Networks: []*NetworkResource{ 410 &NetworkResource{ 411 CIDR: "10.0.0.0/8", 412 MBits: 150, 413 ReservedPorts: []Port{{"ssh", 22}, {"web", 80}}, 414 }, 415 }, 416 } 417 418 if !reflect.DeepEqual(expect.Networks, r1.Networks) { 419 t.Fatalf("bad: %#v %#v", expect, r1) 420 } 421 } 422 423 func TestResource_Add_Network(t *testing.T) { 424 r1 := &Resources{} 425 r2 := &Resources{ 426 Networks: []*NetworkResource{ 427 &NetworkResource{ 428 MBits: 50, 429 DynamicPorts: []Port{{"http", 0}, {"https", 0}}, 430 }, 431 }, 432 } 433 r3 := &Resources{ 434 Networks: []*NetworkResource{ 435 &NetworkResource{ 436 MBits: 25, 437 DynamicPorts: []Port{{"admin", 0}}, 438 }, 439 }, 440 } 441 442 err := r1.Add(r2) 443 if err != nil { 444 t.Fatalf("Err: %v", err) 445 } 446 err = r1.Add(r3) 447 if err != nil { 448 t.Fatalf("Err: %v", err) 449 } 450 451 expect := &Resources{ 452 Networks: []*NetworkResource{ 453 &NetworkResource{ 454 MBits: 75, 455 DynamicPorts: []Port{{"http", 0}, {"https", 0}, {"admin", 0}}, 456 }, 457 }, 458 } 459 460 if !reflect.DeepEqual(expect.Networks, r1.Networks) { 461 t.Fatalf("bad: %#v %#v", expect.Networks[0], r1.Networks[0]) 462 } 463 } 464 465 func TestEncodeDecode(t *testing.T) { 466 type FooRequest struct { 467 Foo string 468 Bar int 469 Baz bool 470 } 471 arg := &FooRequest{ 472 Foo: "test", 473 Bar: 42, 474 Baz: true, 475 } 476 buf, err := Encode(1, arg) 477 if err != nil { 478 t.Fatalf("err: %v", err) 479 } 480 481 var out FooRequest 482 err = Decode(buf[1:], &out) 483 if err != nil { 484 t.Fatalf("err: %v", err) 485 } 486 487 if !reflect.DeepEqual(arg, &out) { 488 t.Fatalf("bad: %#v %#v", arg, out) 489 } 490 } 491 492 func BenchmarkEncodeDecode(b *testing.B) { 493 job := testJob() 494 495 for i := 0; i < b.N; i++ { 496 buf, err := Encode(1, job) 497 if err != nil { 498 b.Fatalf("err: %v", err) 499 } 500 501 var out Job 502 err = Decode(buf[1:], &out) 503 if err != nil { 504 b.Fatalf("err: %v", err) 505 } 506 } 507 } 508 509 func TestInvalidServiceCheck(t *testing.T) { 510 s := Service{ 511 Name: "service-name", 512 PortLabel: "bar", 513 Checks: []*ServiceCheck{ 514 { 515 516 Name: "check-name", 517 Type: "lol", 518 }, 519 }, 520 } 521 if err := s.Validate(); err == nil { 522 t.Fatalf("Service should be invalid") 523 } 524 525 s = Service{ 526 Name: "service.name", 527 PortLabel: "bar", 528 } 529 if err := s.Validate(); err == nil { 530 t.Fatalf("Service should be invalid: %v", err) 531 } 532 } 533 534 func TestDistinctCheckID(t *testing.T) { 535 c1 := ServiceCheck{ 536 Name: "web-health", 537 Type: "http", 538 Path: "/health", 539 Interval: 2 * time.Second, 540 Timeout: 3 * time.Second, 541 } 542 c2 := ServiceCheck{ 543 Name: "web-health", 544 Type: "http", 545 Path: "/health1", 546 Interval: 2 * time.Second, 547 Timeout: 3 * time.Second, 548 } 549 550 c3 := ServiceCheck{ 551 Name: "web-health", 552 Type: "http", 553 Path: "/health", 554 Interval: 4 * time.Second, 555 Timeout: 3 * time.Second, 556 } 557 serviceID := "123" 558 c1Hash := c1.Hash(serviceID) 559 c2Hash := c2.Hash(serviceID) 560 c3Hash := c3.Hash(serviceID) 561 562 if c1Hash == c2Hash || c1Hash == c3Hash || c3Hash == c2Hash { 563 t.Fatalf("Checks need to be uniq c1: %s, c2: %s, c3: %s", c1Hash, c2Hash, c3Hash) 564 } 565 566 } 567 568 func TestService_InitFields(t *testing.T) { 569 job := "example" 570 taskGroup := "cache" 571 task := "redis" 572 573 s := Service{ 574 Name: "${TASK}-db", 575 } 576 577 s.InitFields(job, taskGroup, task) 578 if s.Name != "redis-db" { 579 t.Fatalf("Expected name: %v, Actual: %v", "redis-db", s.Name) 580 } 581 582 s.Name = "db" 583 s.InitFields(job, taskGroup, task) 584 if s.Name != "db" { 585 t.Fatalf("Expected name: %v, Actual: %v", "redis-db", s.Name) 586 } 587 588 s.Name = "${JOB}-${TASKGROUP}-${TASK}-db" 589 s.InitFields(job, taskGroup, task) 590 if s.Name != "example-cache-redis-db" { 591 t.Fatalf("Expected name: %v, Actual: %v", "expample-cache-redis-db", s.Name) 592 } 593 594 s.Name = "${BASE}-db" 595 s.InitFields(job, taskGroup, task) 596 if s.Name != "example-cache-redis-db" { 597 t.Fatalf("Expected name: %v, Actual: %v", "expample-cache-redis-db", s.Name) 598 } 599 600 } 601 602 func TestJob_ExpandServiceNames(t *testing.T) { 603 j := &Job{ 604 Name: "my-job", 605 TaskGroups: []*TaskGroup{ 606 &TaskGroup{ 607 Name: "web", 608 Tasks: []*Task{ 609 { 610 Name: "frontend", 611 Services: []*Service{ 612 { 613 Name: "${BASE}-default", 614 }, 615 { 616 Name: "jmx", 617 }, 618 }, 619 }, 620 }, 621 }, 622 &TaskGroup{ 623 Name: "admin", 624 Tasks: []*Task{ 625 { 626 Name: "admin-web", 627 }, 628 }, 629 }, 630 }, 631 } 632 633 j.InitFields() 634 635 service1Name := j.TaskGroups[0].Tasks[0].Services[0].Name 636 if service1Name != "my-job-web-frontend-default" { 637 t.Fatalf("Expected Service Name: %s, Actual: %s", "my-job-web-frontend-default", service1Name) 638 } 639 640 service2Name := j.TaskGroups[0].Tasks[0].Services[1].Name 641 if service2Name != "jmx" { 642 t.Fatalf("Expected Service Name: %s, Actual: %s", "jmx", service2Name) 643 } 644 645 } 646 647 func TestPeriodicConfig_EnabledInvalid(t *testing.T) { 648 // Create a config that is enabled but with no interval specified. 649 p := &PeriodicConfig{Enabled: true} 650 if err := p.Validate(); err == nil { 651 t.Fatal("Enabled PeriodicConfig with no spec or type shouldn't be valid") 652 } 653 654 // Create a config that is enabled, with a spec but no type specified. 655 p = &PeriodicConfig{Enabled: true, Spec: "foo"} 656 if err := p.Validate(); err == nil { 657 t.Fatal("Enabled PeriodicConfig with no spec type shouldn't be valid") 658 } 659 660 // Create a config that is enabled, with a spec type but no spec specified. 661 p = &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron} 662 if err := p.Validate(); err == nil { 663 t.Fatal("Enabled PeriodicConfig with no spec shouldn't be valid") 664 } 665 } 666 667 func TestPeriodicConfig_InvalidCron(t *testing.T) { 668 specs := []string{"foo", "* *", "@foo"} 669 for _, spec := range specs { 670 p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec} 671 if err := p.Validate(); err == nil { 672 t.Fatal("Invalid cron spec") 673 } 674 } 675 } 676 677 func TestPeriodicConfig_ValidCron(t *testing.T) { 678 specs := []string{"0 0 29 2 *", "@hourly", "0 0-15 * * *"} 679 for _, spec := range specs { 680 p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec} 681 if err := p.Validate(); err != nil { 682 t.Fatal("Passed valid cron") 683 } 684 } 685 } 686 687 func TestPeriodicConfig_NextCron(t *testing.T) { 688 from := time.Date(2009, time.November, 10, 23, 22, 30, 0, time.UTC) 689 specs := []string{"0 0 29 2 * 1980", "*/5 * * * *"} 690 expected := []time.Time{time.Time{}, time.Date(2009, time.November, 10, 23, 25, 0, 0, time.UTC)} 691 for i, spec := range specs { 692 p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec} 693 n := p.Next(from) 694 if expected[i] != n { 695 t.Fatalf("Next(%v) returned %v; want %v", from, n, expected[i]) 696 } 697 } 698 } 699 700 func TestRestartPolicy_Validate(t *testing.T) { 701 // Policy with acceptable restart options passes 702 p := &RestartPolicy{ 703 Mode: RestartPolicyModeFail, 704 Attempts: 0, 705 } 706 if err := p.Validate(); err != nil { 707 t.Fatalf("err: %v", err) 708 } 709 710 // Policy with ambiguous restart options fails 711 p = &RestartPolicy{ 712 Mode: RestartPolicyModeDelay, 713 Attempts: 0, 714 } 715 if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "ambiguous") { 716 t.Fatalf("expect ambiguity error, got: %v", err) 717 } 718 719 // Bad policy mode fails 720 p = &RestartPolicy{ 721 Mode: "nope", 722 Attempts: 1, 723 } 724 if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "mode") { 725 t.Fatalf("expect mode error, got: %v", err) 726 } 727 728 // Fails when attempts*delay does not fit inside interval 729 p = &RestartPolicy{ 730 Mode: RestartPolicyModeDelay, 731 Attempts: 3, 732 Delay: 5 * time.Second, 733 Interval: time.Second, 734 } 735 if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "can't restart") { 736 t.Fatalf("expect restart interval error, got: %v", err) 737 } 738 }