github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/jobspec2/parse_test.go (about) 1 package jobspec2 2 3 import ( 4 "io/ioutil" 5 "os" 6 "strings" 7 "testing" 8 9 "github.com/hashicorp/nomad/api" 10 "github.com/hashicorp/nomad/jobspec" 11 "github.com/stretchr/testify/require" 12 ) 13 14 func TestEquivalentToHCL1(t *testing.T) { 15 hclSpecDir := "../jobspec/test-fixtures/" 16 fis, err := ioutil.ReadDir(hclSpecDir) 17 require.NoError(t, err) 18 19 for _, fi := range fis { 20 name := fi.Name() 21 22 t.Run(name, func(t *testing.T) { 23 f, err := os.Open(hclSpecDir + name) 24 require.NoError(t, err) 25 defer f.Close() 26 27 job1, err := jobspec.Parse(f) 28 if err != nil { 29 t.Skip("file is not parsable in v1") 30 } 31 32 f.Seek(0, 0) 33 34 job2, err := Parse(name, f) 35 require.NoError(t, err) 36 37 require.Equal(t, job1, job2) 38 }) 39 } 40 } 41 42 func TestEquivalentToHCL1_ComplexConfig(t *testing.T) { 43 name := "./test-fixtures/config-compatibility.hcl" 44 f, err := os.Open(name) 45 require.NoError(t, err) 46 defer f.Close() 47 48 job1, err := jobspec.Parse(f) 49 require.NoError(t, err) 50 51 f.Seek(0, 0) 52 53 job2, err := Parse(name, f) 54 require.NoError(t, err) 55 56 require.Equal(t, job1, job2) 57 } 58 59 func TestParse_VarsAndFunctions(t *testing.T) { 60 hcl := ` 61 variables { 62 region_var = "default" 63 } 64 job "example" { 65 datacenters = [for s in ["dc1", "dc2"] : upper(s)] 66 region = var.region_var 67 } 68 ` 69 70 out, err := ParseWithConfig(&ParseConfig{ 71 Path: "input.hcl", 72 Body: []byte(hcl), 73 ArgVars: []string{"region_var=aug"}, 74 AllowFS: true, 75 }) 76 require.NoError(t, err) 77 78 require.Equal(t, []string{"DC1", "DC2"}, out.Datacenters) 79 require.NotNil(t, out.Region) 80 require.Equal(t, "aug", *out.Region) 81 } 82 83 func TestParse_VariablesDefaultsAndSet(t *testing.T) { 84 hcl := ` 85 variables { 86 region_var = "default_region" 87 } 88 89 variable "dc_var" { 90 default = "default_dc" 91 } 92 93 job "example" { 94 datacenters = [var.dc_var] 95 region = var.region_var 96 } 97 ` 98 99 t.Run("defaults", func(t *testing.T) { 100 out, err := ParseWithConfig(&ParseConfig{ 101 Path: "input.hcl", 102 Body: []byte(hcl), 103 AllowFS: true, 104 }) 105 require.NoError(t, err) 106 107 require.Equal(t, []string{"default_dc"}, out.Datacenters) 108 require.NotNil(t, out.Region) 109 require.Equal(t, "default_region", *out.Region) 110 }) 111 112 t.Run("set via -var args", func(t *testing.T) { 113 out, err := ParseWithConfig(&ParseConfig{ 114 Path: "input.hcl", 115 Body: []byte(hcl), 116 ArgVars: []string{"dc_var=set_dc", "region_var=set_region"}, 117 AllowFS: true, 118 }) 119 require.NoError(t, err) 120 121 require.Equal(t, []string{"set_dc"}, out.Datacenters) 122 require.NotNil(t, out.Region) 123 require.Equal(t, "set_region", *out.Region) 124 }) 125 126 t.Run("set via envvars", func(t *testing.T) { 127 out, err := ParseWithConfig(&ParseConfig{ 128 Path: "input.hcl", 129 Body: []byte(hcl), 130 Envs: []string{ 131 "NOMAD_VAR_dc_var=set_dc", 132 "NOMAD_VAR_region_var=set_region", 133 }, 134 AllowFS: true, 135 }) 136 require.NoError(t, err) 137 138 require.Equal(t, []string{"set_dc"}, out.Datacenters) 139 require.NotNil(t, out.Region) 140 require.Equal(t, "set_region", *out.Region) 141 }) 142 143 t.Run("set via var-files", func(t *testing.T) { 144 varFile, err := ioutil.TempFile("", "") 145 require.NoError(t, err) 146 defer os.Remove(varFile.Name()) 147 148 content := `dc_var = "set_dc" 149 region_var = "set_region"` 150 _, err = varFile.WriteString(content) 151 require.NoError(t, err) 152 153 out, err := ParseWithConfig(&ParseConfig{ 154 Path: "input.hcl", 155 Body: []byte(hcl), 156 VarFiles: []string{varFile.Name()}, 157 AllowFS: true, 158 }) 159 require.NoError(t, err) 160 161 require.Equal(t, []string{"set_dc"}, out.Datacenters) 162 require.NotNil(t, out.Region) 163 require.Equal(t, "set_region", *out.Region) 164 }) 165 } 166 167 // TestParse_UnknownVariables asserts that unknown variables are left intact for further processing 168 func TestParse_UnknownVariables(t *testing.T) { 169 hcl := ` 170 variables { 171 region_var = "default" 172 } 173 job "example" { 174 datacenters = [for s in ["dc1", "dc2"] : upper(s)] 175 region = var.region_var 176 meta { 177 known_var = "${var.region_var}" 178 unknown_var = "${UNKNOWN}" 179 } 180 } 181 ` 182 183 out, err := ParseWithConfig(&ParseConfig{ 184 Path: "input.hcl", 185 Body: []byte(hcl), 186 ArgVars: []string{"region_var=aug"}, 187 AllowFS: true, 188 }) 189 require.NoError(t, err) 190 191 meta := map[string]string{ 192 "known_var": "aug", 193 "unknown_var": "${UNKNOWN}", 194 } 195 196 require.Equal(t, meta, out.Meta) 197 } 198 199 // TestParse_UnsetVariables asserts that variables that have neither types nor 200 // values return early instead of panicking. 201 func TestParse_UnsetVariables(t *testing.T) { 202 hcl := ` 203 variable "region_var" {} 204 job "example" { 205 datacenters = [for s in ["dc1", "dc2"] : upper(s)] 206 region = var.region_var 207 } 208 ` 209 210 _, err := ParseWithConfig(&ParseConfig{ 211 Path: "input.hcl", 212 Body: []byte(hcl), 213 ArgVars: []string{}, 214 AllowFS: true, 215 }) 216 217 require.Error(t, err) 218 require.Contains(t, err.Error(), "Unset variable") 219 } 220 221 func TestParse_Locals(t *testing.T) { 222 hcl := ` 223 variables { 224 region_var = "default_region" 225 } 226 227 locals { 228 # literal local 229 dc = "local_dc" 230 # local that depends on a variable 231 region = "${var.region_var}.example" 232 } 233 234 job "example" { 235 datacenters = [local.dc] 236 region = local.region 237 } 238 ` 239 240 t.Run("defaults", func(t *testing.T) { 241 out, err := ParseWithConfig(&ParseConfig{ 242 Path: "input.hcl", 243 Body: []byte(hcl), 244 AllowFS: true, 245 }) 246 require.NoError(t, err) 247 248 require.Equal(t, []string{"local_dc"}, out.Datacenters) 249 require.NotNil(t, out.Region) 250 require.Equal(t, "default_region.example", *out.Region) 251 }) 252 253 t.Run("set via -var argments", func(t *testing.T) { 254 out, err := ParseWithConfig(&ParseConfig{ 255 Path: "input.hcl", 256 Body: []byte(hcl), 257 ArgVars: []string{"region_var=set_region"}, 258 AllowFS: true, 259 }) 260 require.NoError(t, err) 261 262 require.Equal(t, []string{"local_dc"}, out.Datacenters) 263 require.NotNil(t, out.Region) 264 require.Equal(t, "set_region.example", *out.Region) 265 }) 266 } 267 268 func TestParse_FileOperators(t *testing.T) { 269 hcl := ` 270 job "example" { 271 region = file("parse_test.go") 272 } 273 ` 274 275 t.Run("enabled", func(t *testing.T) { 276 out, err := ParseWithConfig(&ParseConfig{ 277 Path: "input.hcl", 278 Body: []byte(hcl), 279 ArgVars: nil, 280 AllowFS: true, 281 }) 282 require.NoError(t, err) 283 284 expected, err := ioutil.ReadFile("parse_test.go") 285 require.NoError(t, err) 286 287 require.NotNil(t, out.Region) 288 require.Equal(t, string(expected), *out.Region) 289 }) 290 291 t.Run("disabled", func(t *testing.T) { 292 _, err := ParseWithConfig(&ParseConfig{ 293 Path: "input.hcl", 294 Body: []byte(hcl), 295 ArgVars: nil, 296 AllowFS: false, 297 }) 298 require.Error(t, err) 299 require.Contains(t, err.Error(), "filesystem function disabled") 300 }) 301 } 302 303 func TestParseDynamic(t *testing.T) { 304 hcl := ` 305 job "example" { 306 307 dynamic "group" { 308 for_each = [ 309 { name = "groupA", idx = 1 }, 310 { name = "groupB", idx = 2 }, 311 { name = "groupC", idx = 3 }, 312 ] 313 labels = [group.value.name] 314 315 content { 316 count = group.value.idx 317 318 service { 319 port = group.value.name 320 } 321 322 task "simple" { 323 driver = "raw_exec" 324 config { 325 command = group.value.name 326 } 327 meta { 328 VERSION = group.value.idx 329 } 330 env { 331 ID = format("id:%s", group.value.idx) 332 } 333 resources { 334 cpu = group.value.idx 335 } 336 } 337 } 338 } 339 } 340 ` 341 out, err := ParseWithConfig(&ParseConfig{ 342 Path: "input.hcl", 343 Body: []byte(hcl), 344 ArgVars: nil, 345 AllowFS: false, 346 }) 347 require.NoError(t, err) 348 349 require.Len(t, out.TaskGroups, 3) 350 require.Equal(t, "groupA", *out.TaskGroups[0].Name) 351 require.Equal(t, "groupB", *out.TaskGroups[1].Name) 352 require.Equal(t, "groupC", *out.TaskGroups[2].Name) 353 require.Equal(t, 1, *out.TaskGroups[0].Tasks[0].Resources.CPU) 354 require.Equal(t, "groupA", out.TaskGroups[0].Services[0].PortLabel) 355 356 // interpolation inside maps 357 require.Equal(t, "groupA", out.TaskGroups[0].Tasks[0].Config["command"]) 358 require.Equal(t, "1", out.TaskGroups[0].Tasks[0].Meta["VERSION"]) 359 require.Equal(t, "id:1", out.TaskGroups[0].Tasks[0].Env["ID"]) 360 require.Equal(t, "id:2", out.TaskGroups[1].Tasks[0].Env["ID"]) 361 require.Equal(t, "3", out.TaskGroups[2].Tasks[0].Meta["VERSION"]) 362 } 363 364 func TestParse_InvalidScalingSyntax(t *testing.T) { 365 cases := []struct { 366 name string 367 expectedErr string 368 hcl string 369 }{ 370 { 371 "valid", 372 "", 373 ` 374 job "example" { 375 group "g1" { 376 scaling { 377 max = 40 378 type = "horizontal" 379 } 380 381 task "t1" { 382 scaling "cpu" { 383 max = 20 384 } 385 scaling "mem" { 386 max = 15 387 } 388 } 389 } 390 } 391 `, 392 }, 393 { 394 "group missing max", 395 `argument "max" is required`, 396 ` 397 job "example" { 398 group "g1" { 399 scaling { 400 #max = 40 401 type = "horizontal" 402 } 403 404 task "t1" { 405 scaling "cpu" { 406 max = 20 407 } 408 scaling "mem" { 409 max = 15 410 } 411 } 412 } 413 } 414 `, 415 }, 416 { 417 "group invalid type", 418 `task group scaling policy had invalid type`, 419 ` 420 job "example" { 421 group "g1" { 422 scaling { 423 max = 40 424 type = "invalid_type" 425 } 426 427 task "t1" { 428 scaling "cpu" { 429 max = 20 430 } 431 scaling "mem" { 432 max = 15 433 } 434 } 435 } 436 } 437 `, 438 }, 439 { 440 "task invalid label", 441 `scaling policy name must be "cpu" or "mem"`, 442 ` 443 job "example" { 444 group "g1" { 445 scaling { 446 max = 40 447 type = "horizontal" 448 } 449 450 task "t1" { 451 scaling "not_cpu" { 452 max = 20 453 } 454 scaling "mem" { 455 max = 15 456 } 457 } 458 } 459 } 460 `, 461 }, 462 { 463 "task duplicate blocks", 464 `Duplicate scaling "cpu" block`, 465 ` 466 job "example" { 467 group "g1" { 468 scaling { 469 max = 40 470 type = "horizontal" 471 } 472 473 task "t1" { 474 scaling "cpu" { 475 max = 20 476 } 477 scaling "cpu" { 478 max = 15 479 } 480 } 481 } 482 } 483 `, 484 }, 485 { 486 "task invalid type", 487 `Invalid scaling policy type`, 488 ` 489 job "example" { 490 group "g1" { 491 scaling { 492 max = 40 493 type = "horizontal" 494 } 495 496 task "t1" { 497 scaling "cpu" { 498 max = 20 499 type = "invalid" 500 } 501 scaling "mem" { 502 max = 15 503 } 504 } 505 } 506 } 507 `, 508 }, 509 } 510 511 for _, c := range cases { 512 t.Run(c.name, func(t *testing.T) { 513 _, err := ParseWithConfig(&ParseConfig{ 514 Path: c.name + ".hcl", 515 Body: []byte(c.hcl), 516 AllowFS: false, 517 }) 518 if c.expectedErr == "" { 519 require.NoError(t, err) 520 } else { 521 require.Error(t, err) 522 require.Contains(t, err.Error(), c.expectedErr) 523 } 524 }) 525 } 526 } 527 528 func TestParseJob_JobWithFunctionsAndLookups(t *testing.T) { 529 hcl := ` 530 variable "env" { 531 description = "target environment for the job" 532 } 533 534 locals { 535 environments = { 536 prod = { count = 20, dcs = ["prod-dc1", "prod-dc2"] }, 537 staging = { count = 3, dcs = ["dc1"] }, 538 } 539 540 env = lookup(local.environments, var.env, { count = 0, dcs = [] }) 541 } 542 543 job "job-webserver" { 544 datacenters = local.env.dcs 545 group "group-webserver" { 546 count = local.env.count 547 548 task "server" { 549 driver = "docker" 550 551 config { 552 image = "hashicorp/http-echo" 553 args = ["-text", "Hello from ${var.env}"] 554 } 555 } 556 } 557 } 558 ` 559 cases := []struct { 560 env string 561 expectedJob *api.Job 562 }{ 563 { 564 "prod", 565 &api.Job{ 566 ID: stringToPtr("job-webserver"), 567 Name: stringToPtr("job-webserver"), 568 Datacenters: []string{"prod-dc1", "prod-dc2"}, 569 TaskGroups: []*api.TaskGroup{ 570 { 571 Name: stringToPtr("group-webserver"), 572 Count: intToPtr(20), 573 574 Tasks: []*api.Task{ 575 { 576 Name: "server", 577 Driver: "docker", 578 579 Config: map[string]interface{}{ 580 "image": "hashicorp/http-echo", 581 "args": []interface{}{"-text", "Hello from prod"}, 582 }, 583 }, 584 }, 585 }, 586 }, 587 }, 588 }, 589 { 590 "staging", 591 &api.Job{ 592 ID: stringToPtr("job-webserver"), 593 Name: stringToPtr("job-webserver"), 594 Datacenters: []string{"dc1"}, 595 TaskGroups: []*api.TaskGroup{ 596 { 597 Name: stringToPtr("group-webserver"), 598 Count: intToPtr(3), 599 600 Tasks: []*api.Task{ 601 { 602 Name: "server", 603 Driver: "docker", 604 605 Config: map[string]interface{}{ 606 "image": "hashicorp/http-echo", 607 "args": []interface{}{"-text", "Hello from staging"}, 608 }, 609 }, 610 }, 611 }, 612 }, 613 }, 614 }, 615 { 616 "unknown", 617 &api.Job{ 618 ID: stringToPtr("job-webserver"), 619 Name: stringToPtr("job-webserver"), 620 Datacenters: []string{}, 621 TaskGroups: []*api.TaskGroup{ 622 { 623 Name: stringToPtr("group-webserver"), 624 Count: intToPtr(0), 625 626 Tasks: []*api.Task{ 627 { 628 Name: "server", 629 Driver: "docker", 630 631 Config: map[string]interface{}{ 632 "image": "hashicorp/http-echo", 633 "args": []interface{}{"-text", "Hello from unknown"}, 634 }, 635 }, 636 }, 637 }, 638 }, 639 }, 640 }, 641 } 642 643 for _, c := range cases { 644 t.Run(c.env, func(t *testing.T) { 645 found, err := ParseWithConfig(&ParseConfig{ 646 Path: "example.hcl", 647 Body: []byte(hcl), 648 AllowFS: false, 649 ArgVars: []string{"env=" + c.env}, 650 }) 651 require.NoError(t, err) 652 require.Equal(t, c.expectedJob, found) 653 }) 654 } 655 } 656 657 func TestParse_TaskEnvs(t *testing.T) { 658 cases := []struct { 659 name string 660 envSnippet string 661 expected map[string]string 662 }{ 663 { 664 "none", 665 ``, 666 nil, 667 }, 668 { 669 "block", 670 ` 671 env { 672 key = "value" 673 } `, 674 map[string]string{"key": "value"}, 675 }, 676 { 677 "attribute", 678 ` 679 env = { 680 "key.dot" = "val1" 681 key_unquoted_without_dot = "val2" 682 } `, 683 map[string]string{"key.dot": "val1", "key_unquoted_without_dot": "val2"}, 684 }, 685 { 686 "attribute_colons", 687 `env = { 688 "key.dot" : "val1" 689 key_unquoted_without_dot : "val2" 690 } `, 691 map[string]string{"key.dot": "val1", "key_unquoted_without_dot": "val2"}, 692 }, 693 { 694 "attribute_empty", 695 `env = {}`, 696 map[string]string{}, 697 }, 698 { 699 "attribute_expression", 700 `env = {for k in ["a", "b"]: k => "val-${k}" }`, 701 map[string]string{"a": "val-a", "b": "val-b"}, 702 }, 703 } 704 705 for _, c := range cases { 706 t.Run(c.name, func(t *testing.T) { 707 hcl := ` 708 job "example" { 709 group "group" { 710 task "task" { 711 driver = "docker" 712 config {} 713 714 ` + c.envSnippet + ` 715 } 716 } 717 }` 718 719 out, err := ParseWithConfig(&ParseConfig{ 720 Path: "input.hcl", 721 Body: []byte(hcl), 722 }) 723 require.NoError(t, err) 724 725 require.Equal(t, c.expected, out.TaskGroups[0].Tasks[0].Env) 726 }) 727 } 728 } 729 730 func TestParse_TaskEnvs_Multiple(t *testing.T) { 731 hcl := ` 732 job "example" { 733 group "group" { 734 task "task" { 735 driver = "docker" 736 config {} 737 738 env = {"a": "b"} 739 env { 740 c = "d" 741 } 742 } 743 } 744 }` 745 746 _, err := ParseWithConfig(&ParseConfig{ 747 Path: "input.hcl", 748 Body: []byte(hcl), 749 }) 750 require.Error(t, err) 751 require.Contains(t, err.Error(), "Duplicate env block") 752 } 753 754 func Test_TaskEnvs_Invalid(t *testing.T) { 755 cases := []struct { 756 name string 757 envSnippet string 758 expectedErr string 759 }{ 760 { 761 "attr: invalid expression", 762 `env = { key = local.undefined_local }`, 763 `does not have an attribute named "undefined_local"`, 764 }, 765 { 766 "block: invalid block expression", 767 `env { 768 for k in ["a", "b"]: k => k 769 }`, 770 "Invalid block definition", 771 }, 772 { 773 "attr: not make sense", 774 `env = [ "a" ]`, 775 "Unsuitable value: map of string required", 776 }, 777 } 778 779 for _, c := range cases { 780 t.Run(c.name, func(t *testing.T) { 781 hcl := ` 782 job "example" { 783 group "group" { 784 task "task" { 785 driver = "docker" 786 config {} 787 788 ` + c.envSnippet + ` 789 } 790 } 791 }` 792 _, err := ParseWithConfig(&ParseConfig{ 793 Path: "input.hcl", 794 Body: []byte(hcl), 795 }) 796 require.Error(t, err) 797 require.Contains(t, err.Error(), c.expectedErr) 798 }) 799 } 800 } 801 802 func TestParse_Meta_Alternatives(t *testing.T) { 803 hcl := ` job "example" { 804 group "group" { 805 task "task" { 806 driver = "config" 807 config {} 808 809 meta { 810 source = "task" 811 } 812 } 813 814 meta { 815 source = "group" 816 817 } 818 } 819 820 meta { 821 source = "job" 822 } 823 } 824 ` 825 826 asBlock, err := ParseWithConfig(&ParseConfig{ 827 Path: "input.hcl", 828 Body: []byte(hcl), 829 }) 830 require.NoError(t, err) 831 832 hclAsAttr := strings.ReplaceAll(hcl, "meta {", "meta = {") 833 require.Equal(t, 3, strings.Count(hclAsAttr, "meta = {")) 834 835 asAttr, err := ParseWithConfig(&ParseConfig{ 836 Path: "input.hcl", 837 Body: []byte(hclAsAttr), 838 }) 839 require.NoError(t, err) 840 841 require.Equal(t, asBlock, asAttr) 842 require.Equal(t, map[string]string{"source": "job"}, asBlock.Meta) 843 require.Equal(t, map[string]string{"source": "group"}, asBlock.TaskGroups[0].Meta) 844 require.Equal(t, map[string]string{"source": "task"}, asBlock.TaskGroups[0].Tasks[0].Meta) 845 846 }