github.com/ryanslade/nomad@v0.2.4-0.20160128061903-fc95782f2089/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_Copy(t *testing.T) { 97 j := &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 c := j.Copy() 165 if !reflect.DeepEqual(j, c) { 166 t.Fatalf("Copy() returned an unequal Job; got %v; want %v", c, j) 167 } 168 } 169 170 func TestJob_IsPeriodic(t *testing.T) { 171 j := &Job{ 172 Type: JobTypeService, 173 Periodic: &PeriodicConfig{ 174 Enabled: true, 175 }, 176 } 177 if !j.IsPeriodic() { 178 t.Fatalf("IsPeriodic() returned false on periodic job") 179 } 180 181 j = &Job{ 182 Type: JobTypeService, 183 } 184 if j.IsPeriodic() { 185 t.Fatalf("IsPeriodic() returned true on non-periodic job") 186 } 187 } 188 189 func TestTaskGroup_Validate(t *testing.T) { 190 tg := &TaskGroup{ 191 RestartPolicy: &RestartPolicy{ 192 Interval: 5 * time.Minute, 193 Delay: 10 * time.Second, 194 Attempts: 10, 195 RestartOnSuccess: true, 196 Mode: RestartPolicyModeDelay, 197 }, 198 } 199 err := tg.Validate() 200 mErr := err.(*multierror.Error) 201 if !strings.Contains(mErr.Errors[0].Error(), "group name") { 202 t.Fatalf("err: %s", err) 203 } 204 if !strings.Contains(mErr.Errors[1].Error(), "count must be positive") { 205 t.Fatalf("err: %s", err) 206 } 207 if !strings.Contains(mErr.Errors[2].Error(), "Missing tasks") { 208 t.Fatalf("err: %s", err) 209 } 210 211 tg = &TaskGroup{ 212 Name: "web", 213 Count: 1, 214 Tasks: []*Task{ 215 &Task{Name: "web"}, 216 &Task{Name: "web"}, 217 &Task{}, 218 }, 219 RestartPolicy: &RestartPolicy{ 220 Interval: 5 * time.Minute, 221 Delay: 10 * time.Second, 222 Attempts: 10, 223 RestartOnSuccess: true, 224 Mode: RestartPolicyModeDelay, 225 }, 226 } 227 err = tg.Validate() 228 mErr = err.(*multierror.Error) 229 if !strings.Contains(mErr.Errors[0].Error(), "2 redefines 'web' from task 1") { 230 t.Fatalf("err: %s", err) 231 } 232 if !strings.Contains(mErr.Errors[1].Error(), "Task 3 missing name") { 233 t.Fatalf("err: %s", err) 234 } 235 if !strings.Contains(mErr.Errors[2].Error(), "Task 1 validation failed") { 236 t.Fatalf("err: %s", err) 237 } 238 } 239 240 func TestTask_Validate(t *testing.T) { 241 task := &Task{} 242 err := task.Validate() 243 mErr := err.(*multierror.Error) 244 if !strings.Contains(mErr.Errors[0].Error(), "task name") { 245 t.Fatalf("err: %s", err) 246 } 247 if !strings.Contains(mErr.Errors[1].Error(), "task driver") { 248 t.Fatalf("err: %s", err) 249 } 250 if !strings.Contains(mErr.Errors[2].Error(), "task resources") { 251 t.Fatalf("err: %s", err) 252 } 253 254 task = &Task{ 255 Name: "web", 256 Driver: "docker", 257 Resources: &Resources{}, 258 } 259 err = task.Validate() 260 if err != nil { 261 t.Fatalf("err: %s", err) 262 } 263 } 264 265 func TestConstraint_Validate(t *testing.T) { 266 c := &Constraint{} 267 err := c.Validate() 268 mErr := err.(*multierror.Error) 269 if !strings.Contains(mErr.Errors[0].Error(), "Missing constraint operand") { 270 t.Fatalf("err: %s", err) 271 } 272 273 c = &Constraint{ 274 LTarget: "$attr.kernel.name", 275 RTarget: "linux", 276 Operand: "=", 277 } 278 err = c.Validate() 279 if err != nil { 280 t.Fatalf("err: %v", err) 281 } 282 283 // Perform additional regexp validation 284 c.Operand = ConstraintRegex 285 c.RTarget = "(foo" 286 err = c.Validate() 287 mErr = err.(*multierror.Error) 288 if !strings.Contains(mErr.Errors[0].Error(), "missing closing") { 289 t.Fatalf("err: %s", err) 290 } 291 292 // Perform version validation 293 c.Operand = ConstraintVersion 294 c.RTarget = "~> foo" 295 err = c.Validate() 296 mErr = err.(*multierror.Error) 297 if !strings.Contains(mErr.Errors[0].Error(), "Malformed constraint") { 298 t.Fatalf("err: %s", err) 299 } 300 } 301 302 func TestResource_NetIndex(t *testing.T) { 303 r := &Resources{ 304 Networks: []*NetworkResource{ 305 &NetworkResource{Device: "eth0"}, 306 &NetworkResource{Device: "lo0"}, 307 &NetworkResource{Device: ""}, 308 }, 309 } 310 if idx := r.NetIndex(&NetworkResource{Device: "eth0"}); idx != 0 { 311 t.Fatalf("Bad: %d", idx) 312 } 313 if idx := r.NetIndex(&NetworkResource{Device: "lo0"}); idx != 1 { 314 t.Fatalf("Bad: %d", idx) 315 } 316 if idx := r.NetIndex(&NetworkResource{Device: "eth1"}); idx != -1 { 317 t.Fatalf("Bad: %d", idx) 318 } 319 } 320 321 func TestResource_Superset(t *testing.T) { 322 r1 := &Resources{ 323 CPU: 2000, 324 MemoryMB: 2048, 325 DiskMB: 10000, 326 IOPS: 100, 327 } 328 r2 := &Resources{ 329 CPU: 2000, 330 MemoryMB: 1024, 331 DiskMB: 5000, 332 IOPS: 50, 333 } 334 335 if s, _ := r1.Superset(r1); !s { 336 t.Fatalf("bad") 337 } 338 if s, _ := r1.Superset(r2); !s { 339 t.Fatalf("bad") 340 } 341 if s, _ := r2.Superset(r1); s { 342 t.Fatalf("bad") 343 } 344 if s, _ := r2.Superset(r2); !s { 345 t.Fatalf("bad") 346 } 347 } 348 349 func TestResource_Add(t *testing.T) { 350 r1 := &Resources{ 351 CPU: 2000, 352 MemoryMB: 2048, 353 DiskMB: 10000, 354 IOPS: 100, 355 Networks: []*NetworkResource{ 356 &NetworkResource{ 357 CIDR: "10.0.0.0/8", 358 MBits: 100, 359 ReservedPorts: []Port{{"ssh", 22}}, 360 }, 361 }, 362 } 363 r2 := &Resources{ 364 CPU: 2000, 365 MemoryMB: 1024, 366 DiskMB: 5000, 367 IOPS: 50, 368 Networks: []*NetworkResource{ 369 &NetworkResource{ 370 IP: "10.0.0.1", 371 MBits: 50, 372 ReservedPorts: []Port{{"web", 80}}, 373 }, 374 }, 375 } 376 377 err := r1.Add(r2) 378 if err != nil { 379 t.Fatalf("Err: %v", err) 380 } 381 382 expect := &Resources{ 383 CPU: 3000, 384 MemoryMB: 3072, 385 DiskMB: 15000, 386 IOPS: 150, 387 Networks: []*NetworkResource{ 388 &NetworkResource{ 389 CIDR: "10.0.0.0/8", 390 MBits: 150, 391 ReservedPorts: []Port{{"ssh", 22}, {"web", 80}}, 392 }, 393 }, 394 } 395 396 if !reflect.DeepEqual(expect.Networks, r1.Networks) { 397 t.Fatalf("bad: %#v %#v", expect, r1) 398 } 399 } 400 401 func TestResource_Add_Network(t *testing.T) { 402 r1 := &Resources{} 403 r2 := &Resources{ 404 Networks: []*NetworkResource{ 405 &NetworkResource{ 406 MBits: 50, 407 DynamicPorts: []Port{{"http", 0}, {"https", 0}}, 408 }, 409 }, 410 } 411 r3 := &Resources{ 412 Networks: []*NetworkResource{ 413 &NetworkResource{ 414 MBits: 25, 415 DynamicPorts: []Port{{"admin", 0}}, 416 }, 417 }, 418 } 419 420 err := r1.Add(r2) 421 if err != nil { 422 t.Fatalf("Err: %v", err) 423 } 424 err = r1.Add(r3) 425 if err != nil { 426 t.Fatalf("Err: %v", err) 427 } 428 429 expect := &Resources{ 430 Networks: []*NetworkResource{ 431 &NetworkResource{ 432 MBits: 75, 433 DynamicPorts: []Port{{"http", 0}, {"https", 0}, {"admin", 0}}, 434 }, 435 }, 436 } 437 438 if !reflect.DeepEqual(expect.Networks, r1.Networks) { 439 t.Fatalf("bad: %#v %#v", expect.Networks[0], r1.Networks[0]) 440 } 441 } 442 443 func TestEncodeDecode(t *testing.T) { 444 type FooRequest struct { 445 Foo string 446 Bar int 447 Baz bool 448 } 449 arg := &FooRequest{ 450 Foo: "test", 451 Bar: 42, 452 Baz: true, 453 } 454 buf, err := Encode(1, arg) 455 if err != nil { 456 t.Fatalf("err: %v", err) 457 } 458 459 var out FooRequest 460 err = Decode(buf[1:], &out) 461 if err != nil { 462 t.Fatalf("err: %v", err) 463 } 464 465 if !reflect.DeepEqual(arg, &out) { 466 t.Fatalf("bad: %#v %#v", arg, out) 467 } 468 } 469 470 func TestInvalidServiceCheck(t *testing.T) { 471 s := Service{ 472 Name: "service-name", 473 PortLabel: "bar", 474 Checks: []*ServiceCheck{ 475 { 476 477 Name: "check-name", 478 Type: "lol", 479 }, 480 }, 481 } 482 if err := s.Validate(); err == nil { 483 t.Fatalf("Service should be invalid") 484 } 485 } 486 487 func TestDistinctCheckID(t *testing.T) { 488 c1 := ServiceCheck{ 489 Name: "web-health", 490 Type: "http", 491 Path: "/health", 492 Interval: 2 * time.Second, 493 Timeout: 3 * time.Second, 494 } 495 c2 := ServiceCheck{ 496 Name: "web-health", 497 Type: "http", 498 Path: "/health1", 499 Interval: 2 * time.Second, 500 Timeout: 3 * time.Second, 501 } 502 503 c3 := ServiceCheck{ 504 Name: "web-health", 505 Type: "http", 506 Path: "/health", 507 Interval: 4 * time.Second, 508 Timeout: 3 * time.Second, 509 } 510 serviceID := "123" 511 c1Hash := c1.Hash(serviceID) 512 c2Hash := c2.Hash(serviceID) 513 c3Hash := c3.Hash(serviceID) 514 515 if c1Hash == c2Hash || c1Hash == c3Hash || c3Hash == c2Hash { 516 t.Fatalf("Checks need to be uniq c1: %s, c2: %s, c3: %s", c1Hash, c2Hash, c3Hash) 517 } 518 519 } 520 521 func TestService_InitFields(t *testing.T) { 522 job := "example" 523 taskGroup := "cache" 524 task := "redis" 525 526 s := Service{ 527 Name: "${TASK}-db", 528 } 529 530 s.InitFields(job, taskGroup, task) 531 if s.Name != "redis-db" { 532 t.Fatalf("Expected name: %v, Actual: %v", "redis-db", s.Name) 533 } 534 535 s.Name = "db" 536 s.InitFields(job, taskGroup, task) 537 if s.Name != "db" { 538 t.Fatalf("Expected name: %v, Actual: %v", "redis-db", s.Name) 539 } 540 541 s.Name = "${JOB}-${TASKGROUP}-${TASK}-db" 542 s.InitFields(job, taskGroup, task) 543 if s.Name != "example-cache-redis-db" { 544 t.Fatalf("Expected name: %v, Actual: %v", "expample-cache-redis-db", s.Name) 545 } 546 547 s.Name = "${BASE}-db" 548 s.InitFields(job, taskGroup, task) 549 if s.Name != "example-cache-redis-db" { 550 t.Fatalf("Expected name: %v, Actual: %v", "expample-cache-redis-db", s.Name) 551 } 552 553 } 554 555 func TestJob_ExpandServiceNames(t *testing.T) { 556 j := &Job{ 557 Name: "my-job", 558 TaskGroups: []*TaskGroup{ 559 &TaskGroup{ 560 Name: "web", 561 Tasks: []*Task{ 562 { 563 Name: "frontend", 564 Services: []*Service{ 565 { 566 Name: "${BASE}-default", 567 }, 568 { 569 Name: "jmx", 570 }, 571 }, 572 }, 573 }, 574 }, 575 &TaskGroup{ 576 Name: "admin", 577 Tasks: []*Task{ 578 { 579 Name: "admin-web", 580 }, 581 }, 582 }, 583 }, 584 } 585 586 j.InitFields() 587 588 service1Name := j.TaskGroups[0].Tasks[0].Services[0].Name 589 if service1Name != "my-job-web-frontend-default" { 590 t.Fatalf("Expected Service Name: %s, Actual: %s", "my-job-web-frontend-default", service1Name) 591 } 592 593 service2Name := j.TaskGroups[0].Tasks[0].Services[1].Name 594 if service2Name != "jmx" { 595 t.Fatalf("Expected Service Name: %s, Actual: %s", "jmx", service2Name) 596 } 597 598 } 599 600 func TestPeriodicConfig_EnabledInvalid(t *testing.T) { 601 // Create a config that is enabled but with no interval specified. 602 p := &PeriodicConfig{Enabled: true} 603 if err := p.Validate(); err == nil { 604 t.Fatal("Enabled PeriodicConfig with no spec or type shouldn't be valid") 605 } 606 607 // Create a config that is enabled, with a spec but no type specified. 608 p = &PeriodicConfig{Enabled: true, Spec: "foo"} 609 if err := p.Validate(); err == nil { 610 t.Fatal("Enabled PeriodicConfig with no spec type shouldn't be valid") 611 } 612 613 // Create a config that is enabled, with a spec type but no spec specified. 614 p = &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron} 615 if err := p.Validate(); err == nil { 616 t.Fatal("Enabled PeriodicConfig with no spec shouldn't be valid") 617 } 618 } 619 620 func TestPeriodicConfig_InvalidCron(t *testing.T) { 621 specs := []string{"foo", "* *", "@foo"} 622 for _, spec := range specs { 623 p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec} 624 if err := p.Validate(); err == nil { 625 t.Fatal("Invalid cron spec") 626 } 627 } 628 } 629 630 func TestPeriodicConfig_ValidCron(t *testing.T) { 631 specs := []string{"0 0 29 2 *", "@hourly", "0 0-15 * * *"} 632 for _, spec := range specs { 633 p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec} 634 if err := p.Validate(); err != nil { 635 t.Fatal("Passed valid cron") 636 } 637 } 638 } 639 640 func TestPeriodicConfig_NextCron(t *testing.T) { 641 from := time.Date(2009, time.November, 10, 23, 22, 30, 0, time.UTC) 642 specs := []string{"0 0 29 2 * 1980", "*/5 * * * *"} 643 expected := []time.Time{time.Time{}, time.Date(2009, time.November, 10, 23, 25, 0, 0, time.UTC)} 644 for i, spec := range specs { 645 p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec} 646 n := p.Next(from) 647 if expected[i] != n { 648 t.Fatalf("Next(%v) returned %v; want %v", from, n, expected[i]) 649 } 650 } 651 }