github.com/maier/nomad@v0.4.1-0.20161110003312-a9e3d0b8549d/jobspec/parse_test.go (about) 1 package jobspec 2 3 import ( 4 "path/filepath" 5 "reflect" 6 "strings" 7 "testing" 8 "time" 9 10 "github.com/hashicorp/nomad/nomad/structs" 11 12 "github.com/hashicorp/consul/api" 13 ) 14 15 func TestParse(t *testing.T) { 16 cases := []struct { 17 File string 18 Result *structs.Job 19 Err bool 20 }{ 21 { 22 "basic.hcl", 23 &structs.Job{ 24 ID: "binstore-storagelocker", 25 Name: "binstore-storagelocker", 26 Type: "service", 27 Priority: 50, 28 AllAtOnce: true, 29 Datacenters: []string{"us2", "eu1"}, 30 Region: "global", 31 VaultToken: "foo", 32 33 Meta: map[string]string{ 34 "foo": "bar", 35 }, 36 37 Constraints: []*structs.Constraint{ 38 &structs.Constraint{ 39 LTarget: "kernel.os", 40 RTarget: "windows", 41 Operand: "=", 42 }, 43 }, 44 45 Update: structs.UpdateStrategy{ 46 Stagger: 60 * time.Second, 47 MaxParallel: 2, 48 }, 49 50 TaskGroups: []*structs.TaskGroup{ 51 &structs.TaskGroup{ 52 Name: "outside", 53 Count: 1, 54 EphemeralDisk: structs.DefaultEphemeralDisk(), 55 Tasks: []*structs.Task{ 56 &structs.Task{ 57 Name: "outside", 58 Driver: "java", 59 Config: map[string]interface{}{ 60 "jar_path": "s3://my-cool-store/foo.jar", 61 }, 62 Meta: map[string]string{ 63 "my-cool-key": "foobar", 64 }, 65 LogConfig: structs.DefaultLogConfig(), 66 }, 67 }, 68 }, 69 70 &structs.TaskGroup{ 71 Name: "binsl", 72 Count: 5, 73 Constraints: []*structs.Constraint{ 74 &structs.Constraint{ 75 LTarget: "kernel.os", 76 RTarget: "linux", 77 Operand: "=", 78 }, 79 }, 80 Meta: map[string]string{ 81 "elb_mode": "tcp", 82 "elb_interval": "10", 83 "elb_checks": "3", 84 }, 85 RestartPolicy: &structs.RestartPolicy{ 86 Interval: 10 * time.Minute, 87 Attempts: 5, 88 Delay: 15 * time.Second, 89 Mode: "delay", 90 }, 91 EphemeralDisk: &structs.EphemeralDisk{ 92 Sticky: true, 93 SizeMB: 150, 94 }, 95 Tasks: []*structs.Task{ 96 &structs.Task{ 97 Name: "binstore", 98 Driver: "docker", 99 User: "bob", 100 Config: map[string]interface{}{ 101 "image": "hashicorp/binstore", 102 "labels": []map[string]interface{}{ 103 map[string]interface{}{ 104 "FOO": "bar", 105 }, 106 }, 107 }, 108 Services: []*structs.Service{ 109 { 110 Name: "binstore-storagelocker-binsl-binstore", 111 Tags: []string{"foo", "bar"}, 112 PortLabel: "http", 113 Checks: []*structs.ServiceCheck{ 114 { 115 Name: "check-name", 116 Type: "tcp", 117 PortLabel: "admin", 118 Interval: 10 * time.Second, 119 Timeout: 2 * time.Second, 120 }, 121 }, 122 }, 123 }, 124 Env: map[string]string{ 125 "HELLO": "world", 126 "LOREM": "ipsum", 127 }, 128 Resources: &structs.Resources{ 129 CPU: 500, 130 MemoryMB: 128, 131 IOPS: 0, 132 Networks: []*structs.NetworkResource{ 133 &structs.NetworkResource{ 134 MBits: 100, 135 ReservedPorts: []structs.Port{{"one", 1}, {"two", 2}, {"three", 3}}, 136 DynamicPorts: []structs.Port{{"http", 0}, {"https", 0}, {"admin", 0}}, 137 }, 138 }, 139 }, 140 KillTimeout: 22 * time.Second, 141 LogConfig: &structs.LogConfig{ 142 MaxFiles: 10, 143 MaxFileSizeMB: 100, 144 }, 145 Artifacts: []*structs.TaskArtifact{ 146 { 147 GetterSource: "http://foo.com/artifact", 148 RelativeDest: "local/", 149 GetterOptions: map[string]string{ 150 "checksum": "md5:b8a4f3f72ecab0510a6a31e997461c5f", 151 }, 152 }, 153 { 154 GetterSource: "http://bar.com/artifact", 155 RelativeDest: "local/", 156 GetterOptions: map[string]string{ 157 "checksum": "md5:ff1cc0d3432dad54d607c1505fb7245c", 158 }, 159 }, 160 }, 161 Vault: &structs.Vault{ 162 Policies: []string{"foo", "bar"}, 163 Env: true, 164 ChangeMode: structs.VaultChangeModeRestart, 165 }, 166 Templates: []*structs.Template{ 167 { 168 SourcePath: "foo", 169 DestPath: "foo", 170 ChangeMode: "foo", 171 ChangeSignal: "foo", 172 Splay: 10 * time.Second, 173 }, 174 { 175 SourcePath: "bar", 176 DestPath: "bar", 177 ChangeMode: structs.TemplateChangeModeRestart, 178 ChangeSignal: "", 179 Splay: 5 * time.Second, 180 }, 181 }, 182 }, 183 &structs.Task{ 184 Name: "storagelocker", 185 Driver: "docker", 186 User: "", 187 Config: map[string]interface{}{ 188 "image": "hashicorp/storagelocker", 189 }, 190 Resources: &structs.Resources{ 191 CPU: 500, 192 MemoryMB: 128, 193 IOPS: 30, 194 }, 195 Constraints: []*structs.Constraint{ 196 &structs.Constraint{ 197 LTarget: "kernel.arch", 198 RTarget: "amd64", 199 Operand: "=", 200 }, 201 }, 202 LogConfig: structs.DefaultLogConfig(), 203 Vault: &structs.Vault{ 204 Policies: []string{"foo", "bar"}, 205 Env: false, 206 ChangeMode: structs.VaultChangeModeSignal, 207 ChangeSignal: "SIGUSR1", 208 }, 209 }, 210 }, 211 }, 212 }, 213 }, 214 false, 215 }, 216 217 { 218 "multi-network.hcl", 219 nil, 220 true, 221 }, 222 223 { 224 "multi-resource.hcl", 225 nil, 226 true, 227 }, 228 229 { 230 "multi-vault.hcl", 231 nil, 232 true, 233 }, 234 235 { 236 "default-job.hcl", 237 &structs.Job{ 238 ID: "foo", 239 Name: "foo", 240 Priority: 50, 241 Region: "global", 242 Type: "service", 243 }, 244 false, 245 }, 246 247 { 248 "version-constraint.hcl", 249 &structs.Job{ 250 ID: "foo", 251 Name: "foo", 252 Priority: 50, 253 Region: "global", 254 Type: "service", 255 Constraints: []*structs.Constraint{ 256 &structs.Constraint{ 257 LTarget: "$attr.kernel.version", 258 RTarget: "~> 3.2", 259 Operand: structs.ConstraintVersion, 260 }, 261 }, 262 }, 263 false, 264 }, 265 266 { 267 "regexp-constraint.hcl", 268 &structs.Job{ 269 ID: "foo", 270 Name: "foo", 271 Priority: 50, 272 Region: "global", 273 Type: "service", 274 Constraints: []*structs.Constraint{ 275 &structs.Constraint{ 276 LTarget: "$attr.kernel.version", 277 RTarget: "[0-9.]+", 278 Operand: structs.ConstraintRegex, 279 }, 280 }, 281 }, 282 false, 283 }, 284 285 { 286 "set-contains-constraint.hcl", 287 &structs.Job{ 288 ID: "foo", 289 Name: "foo", 290 Priority: 50, 291 Region: "global", 292 Type: "service", 293 Constraints: []*structs.Constraint{ 294 &structs.Constraint{ 295 LTarget: "$meta.data", 296 RTarget: "foo,bar,baz", 297 Operand: structs.ConstraintSetContains, 298 }, 299 }, 300 }, 301 false, 302 }, 303 304 { 305 "distinctHosts-constraint.hcl", 306 &structs.Job{ 307 ID: "foo", 308 Name: "foo", 309 Priority: 50, 310 Region: "global", 311 Type: "service", 312 Constraints: []*structs.Constraint{ 313 &structs.Constraint{ 314 Operand: structs.ConstraintDistinctHosts, 315 }, 316 }, 317 }, 318 false, 319 }, 320 321 { 322 "periodic-cron.hcl", 323 &structs.Job{ 324 ID: "foo", 325 Name: "foo", 326 Priority: 50, 327 Region: "global", 328 Type: "service", 329 Periodic: &structs.PeriodicConfig{ 330 Enabled: true, 331 SpecType: structs.PeriodicSpecCron, 332 Spec: "*/5 * * *", 333 ProhibitOverlap: true, 334 }, 335 }, 336 false, 337 }, 338 339 { 340 "specify-job.hcl", 341 &structs.Job{ 342 ID: "job1", 343 Name: "My Job", 344 Priority: 50, 345 Region: "global", 346 Type: "service", 347 }, 348 false, 349 }, 350 351 { 352 "task-nested-config.hcl", 353 &structs.Job{ 354 Region: "global", 355 ID: "foo", 356 Name: "foo", 357 Type: "service", 358 Priority: 50, 359 360 TaskGroups: []*structs.TaskGroup{ 361 &structs.TaskGroup{ 362 Name: "bar", 363 Count: 1, 364 EphemeralDisk: structs.DefaultEphemeralDisk(), 365 Tasks: []*structs.Task{ 366 &structs.Task{ 367 Name: "bar", 368 Driver: "docker", 369 Config: map[string]interface{}{ 370 "image": "hashicorp/image", 371 "port_map": []map[string]interface{}{ 372 map[string]interface{}{ 373 "db": 1234, 374 }, 375 }, 376 }, 377 LogConfig: &structs.LogConfig{ 378 MaxFiles: 10, 379 MaxFileSizeMB: 10, 380 }, 381 }, 382 }, 383 }, 384 }, 385 }, 386 false, 387 }, 388 389 { 390 "bad-artifact.hcl", 391 nil, 392 true, 393 }, 394 395 { 396 "artifacts.hcl", 397 &structs.Job{ 398 ID: "binstore-storagelocker", 399 Name: "binstore-storagelocker", 400 Type: "service", 401 Priority: 50, 402 Region: "global", 403 404 TaskGroups: []*structs.TaskGroup{ 405 &structs.TaskGroup{ 406 Name: "binsl", 407 Count: 1, 408 EphemeralDisk: structs.DefaultEphemeralDisk(), 409 Tasks: []*structs.Task{ 410 &structs.Task{ 411 Name: "binstore", 412 Driver: "docker", 413 Resources: &structs.Resources{ 414 CPU: 100, 415 MemoryMB: 10, 416 IOPS: 0, 417 }, 418 LogConfig: &structs.LogConfig{ 419 MaxFiles: 10, 420 MaxFileSizeMB: 10, 421 }, 422 Artifacts: []*structs.TaskArtifact{ 423 { 424 GetterSource: "http://foo.com/bar", 425 GetterOptions: map[string]string{"foo": "bar"}, 426 RelativeDest: "", 427 }, 428 { 429 GetterSource: "http://foo.com/baz", 430 GetterOptions: nil, 431 RelativeDest: "local/", 432 }, 433 { 434 GetterSource: "http://foo.com/bam", 435 GetterOptions: nil, 436 RelativeDest: "var/foo", 437 }, 438 }, 439 }, 440 }, 441 }, 442 }, 443 }, 444 false, 445 }, 446 { 447 "service-check-initial-status.hcl", 448 &structs.Job{ 449 ID: "check_initial_status", 450 Name: "check_initial_status", 451 Type: "service", 452 Priority: 50, 453 Region: "global", 454 TaskGroups: []*structs.TaskGroup{ 455 &structs.TaskGroup{ 456 Name: "group", 457 Count: 1, 458 EphemeralDisk: structs.DefaultEphemeralDisk(), 459 Tasks: []*structs.Task{ 460 &structs.Task{ 461 Name: "task", 462 Services: []*structs.Service{ 463 { 464 Name: "check_initial_status-group-task", 465 Tags: []string{"foo", "bar"}, 466 PortLabel: "http", 467 Checks: []*structs.ServiceCheck{ 468 { 469 Name: "check-name", 470 Type: "http", 471 Interval: 10 * time.Second, 472 Timeout: 2 * time.Second, 473 InitialStatus: api.HealthPassing, 474 }, 475 }, 476 }, 477 }, 478 LogConfig: structs.DefaultLogConfig(), 479 }, 480 }, 481 }, 482 }, 483 }, 484 false, 485 }, 486 { 487 "vault_inheritance.hcl", 488 &structs.Job{ 489 ID: "example", 490 Name: "example", 491 Type: "service", 492 Priority: 50, 493 Region: "global", 494 TaskGroups: []*structs.TaskGroup{ 495 &structs.TaskGroup{ 496 Name: "cache", 497 Count: 1, 498 EphemeralDisk: structs.DefaultEphemeralDisk(), 499 Tasks: []*structs.Task{ 500 &structs.Task{ 501 Name: "redis", 502 LogConfig: structs.DefaultLogConfig(), 503 Vault: &structs.Vault{ 504 Policies: []string{"group"}, 505 Env: true, 506 ChangeMode: structs.VaultChangeModeRestart, 507 }, 508 }, 509 &structs.Task{ 510 Name: "redis2", 511 LogConfig: structs.DefaultLogConfig(), 512 Vault: &structs.Vault{ 513 Policies: []string{"task"}, 514 Env: false, 515 ChangeMode: structs.VaultChangeModeRestart, 516 }, 517 }, 518 }, 519 }, 520 &structs.TaskGroup{ 521 Name: "cache2", 522 Count: 1, 523 EphemeralDisk: structs.DefaultEphemeralDisk(), 524 Tasks: []*structs.Task{ 525 &structs.Task{ 526 Name: "redis", 527 LogConfig: structs.DefaultLogConfig(), 528 Vault: &structs.Vault{ 529 Policies: []string{"job"}, 530 Env: true, 531 ChangeMode: structs.VaultChangeModeRestart, 532 }, 533 }, 534 }, 535 }, 536 }, 537 }, 538 false, 539 }, 540 } 541 542 for _, tc := range cases { 543 t.Logf("Testing parse: %s", tc.File) 544 545 path, err := filepath.Abs(filepath.Join("./test-fixtures", tc.File)) 546 if err != nil { 547 t.Fatalf("file: %s\n\n%s", tc.File, err) 548 continue 549 } 550 551 actual, err := ParseFile(path) 552 if (err != nil) != tc.Err { 553 t.Fatalf("file: %s\n\n%s", tc.File, err) 554 continue 555 } 556 557 if !reflect.DeepEqual(actual, tc.Result) { 558 diff, err := actual.Diff(tc.Result, true) 559 if err == nil { 560 t.Logf("file %s diff:\n%#v\n", tc.File, diff) 561 } 562 t.Fatalf("file: %s\n\n%#v\n\n%#v", tc.File, actual, tc.Result) 563 } 564 } 565 } 566 567 func TestBadConfigEmpty(t *testing.T) { 568 path, err := filepath.Abs(filepath.Join("./test-fixtures", "bad-config-empty.hcl")) 569 if err != nil { 570 t.Fatalf("Can't get absolute path for file: %s", err) 571 } 572 573 _, err = ParseFile(path) 574 575 if !strings.Contains(err.Error(), "field \"image\" is required, but no value was found") { 576 t.Fatalf("\nExpected error\n %s\ngot\n %v", 577 "field \"image\" is required, but no value was found", 578 err, 579 ) 580 } 581 } 582 583 func TestBadConfigMissing(t *testing.T) { 584 path, err := filepath.Abs(filepath.Join("./test-fixtures", "bad-config-missing.hcl")) 585 if err != nil { 586 t.Fatalf("Can't get absolute path for file: %s", err) 587 } 588 589 _, err = ParseFile(path) 590 591 if !strings.Contains(err.Error(), "field \"image\" is required") { 592 t.Fatalf("\nExpected error\n %s\ngot\n %v", 593 "field \"image\" is required", 594 err, 595 ) 596 } 597 } 598 599 func TestBadConfig(t *testing.T) { 600 path, err := filepath.Abs(filepath.Join("./test-fixtures", "bad-config.hcl")) 601 if err != nil { 602 t.Fatalf("Can't get absolute path for file: %s", err) 603 } 604 605 _, err = ParseFile(path) 606 607 if !strings.Contains(err.Error(), "seem to be of type boolean") { 608 t.Fatalf("\nExpected error\n %s\ngot\n %v", 609 "seem to be of type boolean", 610 err, 611 ) 612 } 613 614 if !strings.Contains(err.Error(), "\"foo\" is an invalid field") { 615 t.Fatalf("\nExpected error\n %s\ngot\n %v", 616 "\"foo\" is an invalid field", 617 err, 618 ) 619 } 620 } 621 622 func TestBadPorts(t *testing.T) { 623 path, err := filepath.Abs(filepath.Join("./test-fixtures", "bad-ports.hcl")) 624 if err != nil { 625 t.Fatalf("Can't get absolute path for file: %s", err) 626 } 627 628 _, err = ParseFile(path) 629 630 if !strings.Contains(err.Error(), errPortLabel.Error()) { 631 t.Fatalf("\nExpected error\n %s\ngot\n %v", errPortLabel, err) 632 } 633 } 634 635 func TestOverlappingPorts(t *testing.T) { 636 path, err := filepath.Abs(filepath.Join("./test-fixtures", "overlapping-ports.hcl")) 637 if err != nil { 638 t.Fatalf("Can't get absolute path for file: %s", err) 639 } 640 641 _, err = ParseFile(path) 642 643 if err == nil { 644 t.Fatalf("Expected an error") 645 } 646 647 if !strings.Contains(err.Error(), "found a port label collision") { 648 t.Fatalf("Expected collision error; got %v", err) 649 } 650 } 651 652 func TestIncompleteServiceDefn(t *testing.T) { 653 path, err := filepath.Abs(filepath.Join("./test-fixtures", "incorrect-service-def.hcl")) 654 if err != nil { 655 t.Fatalf("Can't get absolute path for file: %s", err) 656 } 657 658 _, err = ParseFile(path) 659 660 if err == nil { 661 t.Fatalf("Expected an error") 662 } 663 664 if !strings.Contains(err.Error(), "Only one service block may omit the Name field") { 665 t.Fatalf("Expected collision error; got %v", err) 666 } 667 } 668 669 func TestIncorrectKey(t *testing.T) { 670 path, err := filepath.Abs(filepath.Join("./test-fixtures", "basic_wrong_key.hcl")) 671 if err != nil { 672 t.Fatalf("Can't get absolute path for file: %s", err) 673 } 674 675 _, err = ParseFile(path) 676 677 if err == nil { 678 t.Fatalf("Expected an error") 679 } 680 681 if !strings.Contains(err.Error(), "* group: 'binsl', task: 'binstore', service: 'binstore-storagelocker-binsl-binstore', check -> invalid key: nterval") { 682 t.Fatalf("Expected collision error; got %v", err) 683 } 684 }