github.com/hashicorp/packer@v1.14.3/command/build_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package command 5 6 import ( 7 "fmt" 8 "math" 9 "os" 10 "path/filepath" 11 "regexp" 12 "runtime" 13 "strings" 14 "testing" 15 16 "github.com/google/go-cmp/cmp" 17 "github.com/hashicorp/go-uuid" 18 ) 19 20 var ( 21 spaghettiCarbonara = `spaghetti 22 carbonara 23 ` 24 lasagna = `lasagna 25 tomato 26 mozza 27 cooking... 28 ` 29 tiramisu = `whip_york 30 mascarpone 31 whipped_egg_white 32 dress 33 ` 34 one = "1\n" 35 two = "2\n" 36 ) 37 38 func TestBuild(t *testing.T) { 39 tc := []struct { 40 name string 41 args []string 42 expectedCode int 43 fileCheck 44 }{ 45 { 46 name: "var-args: json - json varfile sets an apple env var", 47 args: []string{ 48 "-var-file=" + filepath.Join(testFixture("var-arg"), "apple.json"), 49 filepath.Join(testFixture("var-arg"), "fruit_builder.json"), 50 }, 51 fileCheck: fileCheck{expected: []string{"apple.txt"}}, 52 }, 53 { 54 name: "json - json varfile sets an apple env var, " + 55 "override with banana cli var", 56 args: []string{ 57 "-var", "fruit=banana", 58 "-var-file=" + filepath.Join(testFixture("var-arg"), "apple.json"), 59 filepath.Join(testFixture("var-arg"), "fruit_builder.json"), 60 }, 61 fileCheck: fileCheck{expected: []string{"banana.txt"}}, 62 }, 63 { 64 name: "var-args: json - arg sets a pear env var", 65 args: []string{ 66 "-var=fruit=pear", 67 filepath.Join(testFixture("var-arg"), "fruit_builder.json"), 68 }, 69 fileCheck: fileCheck{expected: []string{"pear.txt"}}, 70 }, 71 72 { 73 name: "var-args: json - nonexistent var file errs", 74 args: []string{ 75 "-var-file=" + filepath.Join(testFixture("var-arg"), "potato.json"), 76 filepath.Join(testFixture("var-arg"), "fruit_builder.json"), 77 }, 78 expectedCode: 1, 79 fileCheck: fileCheck{notExpected: []string{"potato.txt"}}, 80 }, 81 82 { 83 name: "var-args: hcl - nonexistent json var file errs", 84 args: []string{ 85 "-var-file=" + filepath.Join(testFixture("var-arg"), "potato.json"), 86 testFixture("var-arg"), 87 }, 88 expectedCode: 1, 89 fileCheck: fileCheck{notExpected: []string{"potato.txt"}}, 90 }, 91 92 { 93 name: "var-args: hcl - nonexistent hcl var file errs", 94 args: []string{ 95 "-var-file=" + filepath.Join(testFixture("var-arg"), "potato.hcl"), 96 testFixture("var-arg"), 97 }, 98 expectedCode: 1, 99 fileCheck: fileCheck{notExpected: []string{"potato.hcl"}}, 100 }, 101 102 { 103 name: "var-args: hcl - auto varfile sets a chocolate env var", 104 args: []string{ 105 testFixture("var-arg"), 106 }, 107 fileCheck: fileCheck{expected: []string{"chocolate.txt"}}, 108 }, 109 { 110 name: "var-args: json - auto varfile sets a peanut env var", 111 args: []string{ 112 testFixture("var-arg", "var-arg-test-autovar-json"), 113 }, 114 fileCheck: fileCheck{expected: []string{"peanut.txt"}}, 115 }, 116 117 { 118 name: "var-args: hcl - auto varfile and json -auto varfile sets the value in json auto varfile", 119 args: []string{ 120 testFixture("var-arg", "var-arg-tests"), 121 }, 122 fileCheck: fileCheck{expected: []string{"peanut.txt"}}, 123 }, 124 125 { 126 name: "var-args: hcl - hcl varfile sets a apple env var", 127 args: []string{ 128 "-var-file=" + filepath.Join(testFixture("var-arg"), "apple.hcl"), 129 testFixture("var-arg"), 130 }, 131 fileCheck: fileCheck{expected: []string{"apple.txt"}}, 132 }, 133 134 { 135 name: "var-args: hcl - json varfile sets a apple env var", 136 args: []string{ 137 "-var-file=" + filepath.Join(testFixture("var-arg"), "apple.json"), 138 testFixture("var-arg"), 139 }, 140 fileCheck: fileCheck{expected: []string{"apple.txt"}}, 141 }, 142 { 143 name: "var-args: banana json var file then hcl var file sets apple env var", 144 args: []string{ 145 "-var-file=" + filepath.Join(testFixture("var-arg"), "banana.json"), 146 "-var-file=" + filepath.Join(testFixture("var-arg"), "apple.hcl"), 147 testFixture("var-arg"), 148 }, 149 fileCheck: fileCheck{expected: []string{"apple.txt"}}, 150 }, 151 { 152 name: "var-args: apple hcl var file then banana json var file sets banana env var", 153 args: []string{ 154 "-var-file=" + filepath.Join(testFixture("var-arg"), "apple.hcl"), 155 "-var-file=" + filepath.Join(testFixture("var-arg"), "banana.json"), 156 testFixture("var-arg"), 157 }, 158 fileCheck: fileCheck{expected: []string{"banana.txt"}}, 159 }, 160 161 { 162 name: "var-args: hcl - arg sets a tomato env var", 163 args: []string{ 164 "-var=fruit=tomato", 165 testFixture("var-arg"), 166 }, 167 fileCheck: fileCheck{expected: []string{"tomato.txt"}}, 168 }, 169 170 { 171 name: "source name: HCL", 172 args: []string{ 173 "-parallel-builds=1", // to ensure order is kept 174 testFixture("build-name-and-type"), 175 }, 176 fileCheck: fileCheck{ 177 expectedContent: map[string]string{ 178 "manifest.json": `{ 179 "builds": [ 180 { 181 "name": "test", 182 "builder_type": "null", 183 "files": null, 184 "artifact_id": "Null", 185 "packer_run_uuid": "", 186 "custom_data": null 187 }, 188 { 189 "name": "potato", 190 "builder_type": "null", 191 "files": null, 192 "artifact_id": "Null", 193 "packer_run_uuid": "", 194 "custom_data": null 195 } 196 ], 197 "last_run_uuid": "" 198 }`, 199 }, 200 }, 201 }, 202 203 { 204 name: "build name: JSON except potato", 205 args: []string{ 206 "-except=potato", 207 "-parallel-builds=1", // to ensure order is kept 208 filepath.Join(testFixture("build-name-and-type"), "all.json"), 209 }, 210 fileCheck: fileCheck{ 211 expected: []string{ 212 "null.test.txt", 213 "null.potato.txt", 214 }, 215 expectedContent: map[string]string{ 216 "manifest.json": `{ 217 "builds": [ 218 { 219 "name": "test", 220 "builder_type": "null", 221 "files": null, 222 "artifact_id": "Null", 223 "packer_run_uuid": "", 224 "custom_data": null 225 } 226 ], 227 "last_run_uuid": "" 228 }`, 229 }, 230 }, 231 }, 232 233 { 234 name: "build name: JSON only potato", 235 args: []string{ 236 "-only=potato", 237 "-parallel-builds=1", // to ensure order is kept 238 filepath.Join(testFixture("build-name-and-type"), "all.json"), 239 }, 240 fileCheck: fileCheck{ 241 expectedContent: map[string]string{ 242 "manifest.json": `{ 243 "builds": [ 244 { 245 "name": "potato", 246 "builder_type": "null", 247 "files": null, 248 "artifact_id": "Null", 249 "packer_run_uuid": "", 250 "custom_data": null 251 } 252 ], 253 "last_run_uuid": "" 254 }`, 255 }, 256 }, 257 }, 258 259 // only / except HCL2 260 { 261 name: "hcl - 'except' a build block", 262 args: []string{ 263 "-except=my_build.*", 264 testFixture("hcl-only-except"), 265 }, 266 fileCheck: fileCheck{ 267 expected: []string{"cherry.txt"}, 268 notExpected: []string{"chocolate.txt", "vanilla.txt"}, 269 }, 270 }, 271 272 { 273 name: "hcl - 'only' a build block", 274 args: []string{ 275 "-only=my_build.*", 276 testFixture("hcl-only-except"), 277 }, 278 fileCheck: fileCheck{ 279 notExpected: []string{"cherry.txt"}, 280 expected: []string{"chocolate.txt", "vanilla.txt"}, 281 }, 282 }, 283 284 // recipes 285 { 286 name: "hcl - recipes", 287 args: []string{ 288 testFixture("hcl", "recipes"), 289 }, 290 fileCheck: fileCheck{ 291 expectedContent: map[string]string{ 292 "NULL.spaghetti_carbonara.txt": spaghettiCarbonara, 293 "NULL.lasagna.txt": lasagna, 294 "NULL.tiramisu.txt": tiramisu, 295 }, 296 }, 297 }, 298 299 { 300 name: "hcl - recipes - except carbonara", 301 args: []string{ 302 "-except", "recipes.null.spaghetti_carbonara", 303 testFixture("hcl", "recipes"), 304 }, 305 fileCheck: fileCheck{ 306 notExpected: []string{"NULL.spaghetti_carbonara.txt"}, 307 expectedContent: map[string]string{ 308 "NULL.lasagna.txt": lasagna, 309 "NULL.tiramisu.txt": tiramisu, 310 }, 311 }, 312 }, 313 314 { 315 name: "hcl - recipes - only lasagna", 316 args: []string{ 317 "-only", "*lasagna", 318 testFixture("hcl", "recipes"), 319 }, 320 fileCheck: fileCheck{ 321 notExpected: []string{ 322 "NULL.spaghetti_carbonara.txt", 323 "NULL.tiramisu.txt", 324 }, 325 expectedContent: map[string]string{ 326 "NULL.lasagna.txt": lasagna, 327 }, 328 }, 329 }, 330 { 331 name: "hcl - recipes - only recipes", 332 args: []string{ 333 "-only", "recipes.*", 334 testFixture("hcl", "recipes"), 335 }, 336 fileCheck: fileCheck{ 337 notExpected: []string{ 338 "NULL.tiramisu.txt", 339 }, 340 expectedContent: map[string]string{ 341 "NULL.spaghetti_carbonara.txt": spaghettiCarbonara, 342 "NULL.lasagna.txt": lasagna, 343 }, 344 }, 345 }, 346 { 347 name: "hcl - build.name accessible", 348 args: []string{ 349 filepath.Join(testFixture("build-name-and-type"), "buildname.pkr.hcl"), 350 }, 351 fileCheck: fileCheck{ 352 expected: []string{ 353 "pineapple.pizza.txt", 354 }, 355 }, 356 }, 357 358 { 359 name: "hcl - valid validation rule for default value", 360 args: []string{ 361 filepath.Join(testFixture("hcl", "validation", "map")), 362 }, 363 expectedCode: 0, 364 }, 365 366 { 367 name: "hcl - valid setting from varfile", 368 args: []string{ 369 "-var-file", filepath.Join(testFixture("hcl", "validation", "map", "valid_value.pkrvars.hcl")), 370 filepath.Join(testFixture("hcl", "validation", "map")), 371 }, 372 expectedCode: 0, 373 }, 374 375 { 376 name: "hcl - invalid setting from varfile", 377 args: []string{ 378 "-var-file", filepath.Join(testFixture("hcl", "validation", "map", "invalid_value.pkrvars.hcl")), 379 filepath.Join(testFixture("hcl", "validation", "map")), 380 }, 381 expectedCode: 1, 382 }, 383 384 { 385 name: "hcl - valid cmd ( invalid varfile bypased )", 386 args: []string{ 387 "-var-file", filepath.Join(testFixture("hcl", "validation", "map", "invalid_value.pkrvars.hcl")), 388 "-var", `image_metadata={key = "new_value", something = { foo = "bar" }}`, 389 filepath.Join(testFixture("hcl", "validation", "map")), 390 }, 391 expectedCode: 0, 392 }, 393 394 { 395 name: "hcl - invalid cmd ( valid varfile bypased )", 396 args: []string{ 397 "-var-file", filepath.Join(testFixture("hcl", "validation", "map", "valid_value.pkrvars.hcl")), 398 "-var", `image_metadata={key = "?", something = { foo = "wrong" }}`, 399 filepath.Join(testFixture("hcl", "validation", "map")), 400 }, 401 expectedCode: 1, 402 }, 403 { 404 name: "hcl - execute and use datasource", 405 args: []string{ 406 testFixture("hcl", "datasource.pkr.hcl"), 407 }, 408 fileCheck: fileCheck{ 409 expectedContent: map[string]string{ 410 "chocolate.txt": "chocolate", 411 }, 412 }, 413 }, 414 { 415 name: "hcl - dynamic source blocks in a build block", 416 args: []string{ 417 testFixture("hcl", "dynamic", "build.pkr.hcl"), 418 }, 419 fileCheck: fileCheck{ 420 expectedContent: map[string]string{ 421 "dummy.txt": "layers/base/main/files", 422 "postgres/13.txt": "layers/base/main/files\nlayers/base/init/files\nlayers/postgres/files", 423 }, 424 expected: []string{"dummy-fooo.txt", "dummy-baar.txt", "postgres/13-fooo.txt", "postgres/13-baar.txt"}, 425 }, 426 }, 427 428 { 429 name: "hcl - variables can be used in shared post-processor fields", 430 args: []string{ 431 testFixture("hcl", "var-in-pp-name.pkr.hcl"), 432 }, 433 fileCheck: fileCheck{ 434 expectedContent: map[string]string{ 435 "example1.1.txt": one, 436 "example2.2.txt": two, 437 }, 438 notExpected: []string{ 439 "example1.2.txt", 440 "example2.1.txt", 441 }, 442 }, 443 }, 444 { 445 name: "hcl - using build variables in post-processor", 446 args: []string{ 447 testFixture("hcl", "build-var-in-pp.pkr.hcl"), 448 }, 449 fileCheck: fileCheck{ 450 expectedContent: map[string]string{ 451 "example.2.txt": two, 452 }, 453 }, 454 }, 455 456 { 457 name: "hcl - test crash #11381", 458 args: []string{ 459 testFixture("hcl", "nil-component-crash.pkr.hcl"), 460 }, 461 expectedCode: 1, 462 }, 463 { 464 name: "hcl - using variables in build block", 465 args: []string{ 466 testFixture("hcl", "vars-in-build-block.pkr.hcl"), 467 }, 468 fileCheck: fileCheck{ 469 expectedContent: map[string]string{ 470 "example.2.txt": two, 471 }, 472 }, 473 }, 474 { 475 name: "hcl - recursive local using input var", 476 args: []string{ 477 testFixture("hcl", "recursive_local_with_input"), 478 }, 479 fileCheck: fileCheck{ 480 expectedContent: map[string]string{ 481 "hey.txt": "hello", 482 }, 483 }, 484 }, 485 { 486 name: "hcl - recursive local using an unset input var", 487 args: []string{ 488 testFixture("hcl", "recursive_local_with_unset_input"), 489 }, 490 fileCheck: fileCheck{}, 491 expectedCode: 1, 492 }, 493 { 494 name: "hcl - var with default value empty object/list can be set", 495 args: []string{ 496 testFixture("hcl", "empty_object"), 497 }, 498 fileCheck: fileCheck{ 499 expectedContent: map[string]string{ 500 "foo.txt": "yo", 501 }, 502 }, 503 }, 504 { 505 name: "hcl - unknown ", 506 args: []string{ 507 testFixture("hcl", "data-source-validation.pkr.hcl"), 508 }, 509 fileCheck: fileCheck{ 510 expectedContent: map[string]string{ 511 "foo.txt": "foo", 512 }, 513 expected: []string{ 514 "s3cr3t", 515 }, 516 }, 517 }, 518 } 519 520 for _, tt := range tc { 521 t.Run(tt.name, func(t *testing.T) { 522 defer tt.cleanup(t) 523 t.Logf("Running build on %s", tt.args) 524 run(t, tt.args, tt.expectedCode) 525 tt.fileCheck.verify(t, "") 526 }) 527 } 528 } 529 530 func Test_build_output(t *testing.T) { 531 532 tc := []struct { 533 command []string 534 env []string 535 expected []string 536 notExpected []string 537 runtime string 538 }{ 539 {[]string{"build", "--color=false", testFixture("hcl", "reprepare", "shell-local.pkr.hcl")}, 540 nil, 541 []string{"null.example: hello from the NULL builder packeruser", "Build 'null.example' finished after"}, 542 []string{}, 543 "posix"}, 544 {[]string{"build", "--color=false", testFixture("hcl", "reprepare", "shell-local-windows.pkr.hcl")}, 545 nil, 546 []string{"null.example: hello from the NULL builder packeruser", "Build 'null.example' finished after"}, 547 []string{}, 548 "windows"}, 549 {[]string{"build", "--color=false", testFixture("hcl", "provisioner-override.pkr.hcl")}, 550 nil, 551 []string{"null.example1: yes overridden", "null.example2: not overridden"}, 552 []string{"null.example2: yes overridden", "null.example1: not overridden"}, 553 "posix"}, 554 {[]string{"build", "--color=false", testFixture("provisioners", "provisioner-override.json")}, 555 nil, 556 []string{"example1: yes overridden", "example2: not overridden"}, 557 []string{"example2: yes overridden", "example1: not overridden"}, 558 "posix"}, 559 } 560 561 for _, tc := range tc { 562 if (runtime.GOOS == "windows") != (tc.runtime == "windows") { 563 continue 564 } 565 t.Run(fmt.Sprintf("packer %s", tc.command), func(t *testing.T) { 566 p := helperCommand(t, tc.command...) 567 p.Env = append(p.Env, tc.env...) 568 bs, err := p.Output() 569 if err != nil { 570 t.Fatalf("%v: %s", err, bs) 571 } 572 for _, expected := range tc.expected { 573 if !strings.Contains(string(bs), expected) { 574 t.Fatalf("Should contain output %s.\nReceived: %s", tc.expected, string(bs)) 575 } 576 } 577 for _, notExpected := range tc.notExpected { 578 if strings.Contains(string(bs), notExpected) { 579 t.Fatalf("Should NOT contain output %s.\nReceived: %s", tc.expected, string(bs)) 580 } 581 } 582 }) 583 } 584 } 585 586 func TestBuildOnlyFileCommaFlags(t *testing.T) { 587 c := &BuildCommand{ 588 Meta: TestMetaFile(t), 589 } 590 591 args := []string{ 592 "-parallel-builds=1", 593 "-only=chocolate,vanilla", 594 filepath.Join(testFixture("build-only"), "template.json"), 595 } 596 597 defer cleanup() 598 599 if code := c.Run(args); code != 0 { 600 fatalCommand(t, c.Meta) 601 } 602 603 for _, f := range []string{"chocolate.txt", "vanilla.txt", 604 "apple.txt", "peach.txt", "pear.txt", "unnamed.txt"} { 605 if !fileExists(f) { 606 t.Errorf("Expected to find %s", f) 607 } 608 } 609 610 if fileExists("cherry.txt") { 611 t.Error("Expected NOT to find cherry.txt") 612 } 613 614 if !fileExists("tomato.txt") { 615 t.Error("Expected to find tomato.txt") 616 } 617 } 618 619 func TestBuildStdin(t *testing.T) { 620 c := &BuildCommand{ 621 Meta: TestMetaFile(t), 622 } 623 f, err := os.Open(filepath.Join(testFixture("build-only"), "template.json")) 624 if err != nil { 625 t.Fatal(err) 626 } 627 defer f.Close() 628 629 stdin := os.Stdin 630 os.Stdin = f 631 defer func() { os.Stdin = stdin }() 632 633 defer cleanup() 634 if code := c.Run([]string{"-parallel-builds=1", "-"}); code != 0 { 635 fatalCommand(t, c.Meta) 636 } 637 638 for _, f := range []string{"vanilla.txt", "cherry.txt", "chocolate.txt", 639 "unnamed.txt"} { 640 if !fileExists(f) { 641 t.Errorf("Expected to find %s", f) 642 } 643 } 644 } 645 646 func TestBuildOnlyFileMultipleFlags(t *testing.T) { 647 c := &BuildCommand{ 648 Meta: TestMetaFile(t), 649 } 650 651 args := []string{ 652 "-parallel-builds=1", 653 "-only=chocolate", 654 "-only=cherry", 655 "-only=apple", // ignored 656 "-only=peach", // ignored 657 "-only=pear", // ignored 658 filepath.Join(testFixture("build-only"), "template.json"), 659 } 660 661 defer cleanup() 662 663 if code := c.Run(args); code != 0 { 664 fatalCommand(t, c.Meta) 665 } 666 667 for _, f := range []string{"vanilla.txt", "tomato.txt"} { 668 if fileExists(f) { 669 t.Errorf("Expected NOT to find %s", f) 670 } 671 } 672 for _, f := range []string{"chocolate.txt", "cherry.txt", 673 "apple.txt", "peach.txt", "pear.txt", "unnamed.txt"} { 674 if !fileExists(f) { 675 t.Errorf("Expected to find %s", f) 676 } 677 } 678 } 679 680 func TestBuildProvisionAndPosProcessWithBuildVariablesSharing(t *testing.T) { 681 c := &BuildCommand{ 682 Meta: TestMetaFile(t), 683 } 684 685 args := []string{ 686 filepath.Join(testFixture("build-variable-sharing"), "template.json"), 687 } 688 689 files := []string{ 690 "provisioner.Null.txt", 691 "post-processor.Null.txt", 692 } 693 694 defer cleanup(files...) 695 696 if code := c.Run(args); code != 0 { 697 fatalCommand(t, c.Meta) 698 } 699 700 for _, f := range files { 701 if !fileExists(f) { 702 t.Errorf("Expected to find %s", f) 703 } 704 } 705 } 706 707 func TestBuildEverything(t *testing.T) { 708 c := &BuildCommand{ 709 Meta: TestMetaFile(t), 710 } 711 712 args := []string{ 713 "-parallel-builds=1", 714 `-except=`, 715 filepath.Join(testFixture("build-only"), "template.json"), 716 } 717 718 defer cleanup() 719 720 if code := c.Run(args); code != 0 { 721 fatalCommand(t, c.Meta) 722 } 723 724 for _, f := range []string{"chocolate.txt", "vanilla.txt", "tomato.txt", 725 "apple.txt", "cherry.txt", "pear.txt", "peach.txt", "unnamed.txt"} { 726 if !fileExists(f) { 727 t.Errorf("Expected to find %s", f) 728 } 729 } 730 } 731 732 func TestBuildExceptFileCommaFlags(t *testing.T) { 733 c := &BuildCommand{ 734 Meta: TestMetaFile(t), 735 } 736 tc := []struct { 737 name string 738 args []string 739 expectedFiles []string 740 buildNotExpectedFiles []string 741 postProcNotExpectedFiles []string 742 }{ 743 { 744 name: "JSON: except build and post-processor", 745 args: []string{ 746 "-parallel-builds=1", 747 "-except=chocolate,vanilla,tomato", 748 filepath.Join(testFixture("build-only"), "template.json"), 749 }, 750 expectedFiles: []string{"apple.txt", "cherry.txt", "peach.txt"}, 751 buildNotExpectedFiles: []string{"chocolate.txt", "vanilla.txt", "tomato.txt", "unnamed.txt"}, 752 postProcNotExpectedFiles: []string{"pear.txt, banana.txt"}, 753 }, 754 { 755 name: "HCL2: except build and post-processor", 756 args: []string{ 757 "-parallel-builds=1", 758 "-except=file.chocolate,file.vanilla,tomato", 759 filepath.Join(testFixture("build-only"), "template.pkr.hcl"), 760 }, 761 expectedFiles: []string{"apple.txt", "cherry.txt", "peach.txt"}, 762 buildNotExpectedFiles: []string{"chocolate.txt", "vanilla.txt", "tomato.txt", "unnamed.txt"}, 763 postProcNotExpectedFiles: []string{"pear.txt, banana.txt"}, 764 }, 765 { 766 name: "HCL2-JSON: except build and post-processor", 767 args: []string{ 768 "-parallel-builds=1", 769 "-except=file.chocolate,file.vanilla,tomato", 770 filepath.Join(testFixture("build-only"), "template.pkr.json"), 771 }, 772 expectedFiles: []string{"apple.txt", "cherry.txt", "peach.txt"}, 773 buildNotExpectedFiles: []string{"chocolate.txt", "vanilla.txt", "tomato.txt", "unnamed.txt"}, 774 postProcNotExpectedFiles: []string{"pear.txt, banana.txt"}, 775 }, 776 } 777 778 for _, tt := range tc { 779 t.Run(tt.name, func(t *testing.T) { 780 defer cleanup() 781 782 if code := c.Run(tt.args); code != 0 { 783 fatalCommand(t, c.Meta) 784 } 785 786 for _, f := range tt.buildNotExpectedFiles { 787 if fileExists(f) { 788 t.Errorf("build not skipped: Expected NOT to find %s", f) 789 } 790 } 791 for _, f := range tt.postProcNotExpectedFiles { 792 if fileExists(f) { 793 t.Errorf("post-processor not skipped: Expected NOT to find %s", f) 794 } 795 } 796 for _, f := range tt.expectedFiles { 797 if !fileExists(f) { 798 t.Errorf("Expected to find %s", f) 799 } 800 } 801 }) 802 } 803 } 804 805 func testHCLOnlyExceptFlags(t *testing.T, args, present, notPresent []string, expectReturn int) { 806 c := &BuildCommand{ 807 Meta: TestMetaFile(t), 808 } 809 810 defer cleanup() 811 812 finalArgs := []string{"-parallel-builds=1"} 813 finalArgs = append(finalArgs, args...) 814 finalArgs = append(finalArgs, testFixture("hcl-only-except")) 815 816 if code := c.Run(finalArgs); code != expectReturn { 817 fatalCommand(t, c.Meta) 818 } 819 820 for _, f := range notPresent { 821 if fileExists(f) { 822 t.Errorf("Expected NOT to find %s", f) 823 } 824 } 825 for _, f := range present { 826 if !fileExists(f) { 827 t.Errorf("Expected to find %s", f) 828 } 829 } 830 } 831 832 func TestHCL2PostProcessorForceFlag(t *testing.T) { 833 t.Helper() 834 835 UUID, _ := uuid.GenerateUUID() 836 // Manifest will only clean with force if the build's PACKER_RUN_UUID are different 837 t.Setenv("PACKER_RUN_UUID", UUID) 838 839 args := []string{ 840 filepath.Join(testFixture("hcl"), "force.pkr.hcl"), 841 } 842 fCheck := fileCheck{ 843 expectedContent: map[string]string{ 844 "manifest.json": fmt.Sprintf(`{ 845 "builds": [ 846 { 847 "name": "potato", 848 "builder_type": "null", 849 "files": null, 850 "artifact_id": "Null", 851 "packer_run_uuid": %q, 852 "custom_data": null 853 } 854 ], 855 "last_run_uuid": %q 856 }`, UUID, UUID), 857 }, 858 } 859 defer fCheck.cleanup(t) 860 861 c := &BuildCommand{ 862 Meta: TestMetaFile(t), 863 } 864 if code := c.Run(args); code != 0 { 865 fatalCommand(t, c.Meta) 866 } 867 fCheck.verify(t, "") 868 869 // Second build should override previous manifest 870 UUID, _ = uuid.GenerateUUID() 871 t.Setenv("PACKER_RUN_UUID", UUID) 872 873 args = []string{ 874 "-force", 875 filepath.Join(testFixture("hcl"), "force.pkr.hcl"), 876 } 877 fCheck = fileCheck{ 878 expectedContent: map[string]string{ 879 "manifest.json": fmt.Sprintf(`{ 880 "builds": [ 881 { 882 "name": "potato", 883 "builder_type": "null", 884 "files": null, 885 "artifact_id": "Null", 886 "packer_run_uuid": %q, 887 "custom_data": null 888 } 889 ], 890 "last_run_uuid": %q 891 }`, UUID, UUID), 892 }, 893 } 894 895 c = &BuildCommand{ 896 Meta: TestMetaFile(t), 897 } 898 if code := c.Run(args); code != 0 { 899 fatalCommand(t, c.Meta) 900 } 901 fCheck.verify(t, "") 902 } 903 904 func TestBuildCommand_HCLOnlyExceptOptions(t *testing.T) { 905 tests := []struct { 906 args []string 907 present []string 908 notPresent []string 909 expectReturn int 910 }{ 911 { 912 []string{"-only=chocolate"}, 913 []string{}, 914 []string{"chocolate.txt", "vanilla.txt", "cherry.txt"}, 915 1, 916 }, 917 { 918 []string{"-only=*chocolate*"}, 919 []string{"chocolate.txt"}, 920 []string{"vanilla.txt", "cherry.txt"}, 921 0, 922 }, 923 { 924 []string{"-except=*chocolate*"}, 925 []string{"vanilla.txt", "cherry.txt"}, 926 []string{"chocolate.txt"}, 927 0, 928 }, 929 { 930 []string{"-except=*ch*"}, 931 []string{"vanilla.txt"}, 932 []string{"chocolate.txt", "cherry.txt"}, 933 0, 934 }, 935 { 936 []string{"-only=*chocolate*", "-only=*vanilla*"}, 937 []string{"chocolate.txt", "vanilla.txt"}, 938 []string{"cherry.txt"}, 939 0, 940 }, 941 { 942 []string{"-except=*chocolate*", "-except=*vanilla*"}, 943 []string{"cherry.txt"}, 944 []string{"chocolate.txt", "vanilla.txt"}, 945 0, 946 }, 947 { 948 []string{"-only=my_build.file.chocolate"}, 949 []string{"chocolate.txt"}, 950 []string{"vanilla.txt", "cherry.txt"}, 951 0, 952 }, 953 { 954 []string{"-except=my_build.file.chocolate"}, 955 []string{"vanilla.txt", "cherry.txt"}, 956 []string{"chocolate.txt"}, 957 0, 958 }, 959 { 960 []string{"-only=file.cherry"}, 961 []string{"cherry.txt"}, 962 []string{"vanilla.txt", "chocolate.txt"}, 963 0, 964 }, 965 } 966 967 for _, tt := range tests { 968 t.Run(fmt.Sprintf("%s", tt.args), func(t *testing.T) { 969 testHCLOnlyExceptFlags(t, tt.args, tt.present, tt.notPresent, tt.expectReturn) 970 }) 971 } 972 } 973 974 func TestBuildWithNonExistingBuilder(t *testing.T) { 975 c := &BuildCommand{ 976 Meta: TestMetaFile(t), 977 } 978 979 args := []string{ 980 "-parallel-builds=1", 981 `-except=`, 982 filepath.Join(testFixture("build-only"), "not-found.json"), 983 } 984 985 defer cleanup() 986 987 if code := c.Run(args); code != 1 { 988 t.Errorf("Expected to find exit code 1, found %d", code) 989 } 990 if !fileExists("chocolate.txt") { 991 t.Errorf("Expected to find chocolate.txt") 992 } 993 if fileExists("vanilla.txt") { 994 t.Errorf("NOT expected to find vanilla.tx") 995 } 996 } 997 998 func run(t *testing.T, args []string, expectedCode int) { 999 t.Helper() 1000 1001 c := &BuildCommand{ 1002 Meta: TestMetaFile(t), 1003 } 1004 1005 if code := c.Run(args); code != expectedCode { 1006 fatalCommand(t, c.Meta) 1007 } 1008 } 1009 1010 type fileCheck struct { 1011 expected, notExpected []string 1012 expectedContent map[string]string 1013 } 1014 1015 func (fc fileCheck) cleanup(t *testing.T) { 1016 for _, file := range fc.expectedFiles() { 1017 t.Logf("removing %v", file) 1018 if err := os.Remove(file); err != nil { 1019 t.Errorf("failed to remove file %s: %v", file, err) 1020 } 1021 } 1022 } 1023 1024 func (fc fileCheck) expectedFiles() []string { 1025 expected := fc.expected 1026 for file := range fc.expectedContent { 1027 expected = append(expected, file) 1028 } 1029 return expected 1030 } 1031 1032 func (fc fileCheck) verify(t *testing.T, dir string) { 1033 for _, f := range fc.expectedFiles() { 1034 if _, err := os.Stat(filepath.Join(dir, f)); err != nil { 1035 t.Errorf("Expected to find %s: %v", f, err) 1036 } 1037 } 1038 for _, f := range fc.notExpected { 1039 if _, err := os.Stat(filepath.Join(dir, f)); err == nil { 1040 t.Errorf("Expected to not find %s", f) 1041 } 1042 } 1043 for file, expectedContent := range fc.expectedContent { 1044 content, err := os.ReadFile(filepath.Join(dir, file)) 1045 if err != nil { 1046 t.Fatalf("os.ReadFile: %v", err) 1047 } 1048 if diff := cmp.Diff(expectedContent, string(content)); diff != "" { 1049 t.Errorf("content of %s differs: %s", file, diff) 1050 } 1051 } 1052 } 1053 1054 func cleanup(moreFiles ...string) { 1055 os.RemoveAll("chocolate.txt") 1056 os.RemoveAll("vanilla.txt") 1057 os.RemoveAll("cherry.txt") 1058 os.RemoveAll("apple.txt") 1059 os.RemoveAll("peach.txt") 1060 os.RemoveAll("banana.txt") 1061 os.RemoveAll("pear.txt") 1062 os.RemoveAll("tomato.txt") 1063 os.RemoveAll("unnamed.txt") 1064 os.RemoveAll("roses.txt") 1065 os.RemoveAll("fuchsias.txt") 1066 os.RemoveAll("lilas.txt") 1067 os.RemoveAll("campanules.txt") 1068 os.RemoveAll("ducky.txt") 1069 os.RemoveAll("banana.txt") 1070 for _, file := range moreFiles { 1071 os.RemoveAll(file) 1072 } 1073 } 1074 1075 func TestBuildCommand_ParseArgs(t *testing.T) { 1076 defaultMeta := TestMetaFile(t) 1077 type fields struct { 1078 Meta Meta 1079 } 1080 type args struct { 1081 args []string 1082 } 1083 tests := []struct { 1084 fields fields 1085 args args 1086 wantCfg *BuildArgs 1087 wantExitCode int 1088 }{ 1089 {fields{defaultMeta}, 1090 args{[]string{"file.json"}}, 1091 &BuildArgs{ 1092 MetaArgs: MetaArgs{Path: "file.json"}, 1093 ParallelBuilds: math.MaxInt64, 1094 Color: true, 1095 }, 1096 0, 1097 }, 1098 {fields{defaultMeta}, 1099 args{[]string{"-parallel-builds=10", "file.json"}}, 1100 &BuildArgs{ 1101 MetaArgs: MetaArgs{Path: "file.json"}, 1102 ParallelBuilds: 10, 1103 Color: true, 1104 }, 1105 0, 1106 }, 1107 {fields{defaultMeta}, 1108 args{[]string{"-parallel-builds=1", "file.json"}}, 1109 &BuildArgs{ 1110 MetaArgs: MetaArgs{Path: "file.json"}, 1111 ParallelBuilds: 1, 1112 Color: true, 1113 }, 1114 0, 1115 }, 1116 {fields{defaultMeta}, 1117 args{[]string{"-parallel-builds=5", "file.json"}}, 1118 &BuildArgs{ 1119 MetaArgs: MetaArgs{Path: "file.json"}, 1120 ParallelBuilds: 5, 1121 Color: true, 1122 }, 1123 0, 1124 }, 1125 {fields{defaultMeta}, 1126 args{[]string{"-parallel-builds=1", "-parallel-builds=5", "otherfile.json"}}, 1127 &BuildArgs{ 1128 MetaArgs: MetaArgs{Path: "otherfile.json"}, 1129 ParallelBuilds: 5, 1130 Color: true, 1131 }, 1132 0, 1133 }, 1134 } 1135 for _, tt := range tests { 1136 t.Run(fmt.Sprintf("%s", tt.args.args), func(t *testing.T) { 1137 c := &BuildCommand{ 1138 Meta: tt.fields.Meta, 1139 } 1140 gotCfg, gotExitCode := c.ParseArgs(tt.args.args) 1141 if diff := cmp.Diff(gotCfg, tt.wantCfg); diff != "" { 1142 t.Fatalf("BuildCommand.ParseArgs() unexpected cfg %s", diff) 1143 } 1144 if gotExitCode != tt.wantExitCode { 1145 t.Fatalf("BuildCommand.ParseArgs() gotExitCode = %v, want %v", gotExitCode, tt.wantExitCode) 1146 } 1147 }) 1148 } 1149 } 1150 1151 // TestProvisionerOnlyExcept checks that only/except blocks in provisioners/post-processors behave as expected 1152 func TestProvisionerAndPostProcessorOnlyExcept(t *testing.T) { 1153 tests := []struct { 1154 name string 1155 args []string 1156 expectedCode int 1157 outputCheck func(string, string) error 1158 }{ 1159 { 1160 "json - only named build", 1161 []string{ 1162 "-only", "packer", 1163 testFixture("provisioners", "provisioner-only-except.json"), 1164 }, 1165 0, 1166 func(out, _ string) error { 1167 if !strings.Contains(out, "packer provisioner packer and null") { 1168 return fmt.Errorf("missing expected provisioner output") 1169 } 1170 1171 if !strings.Contains(out, "packer post-processor packer and null") { 1172 return fmt.Errorf("missing expected post-processor output") 1173 } 1174 1175 if strings.Contains(out, "null post-processor") || strings.Contains(out, "null provisioner") { 1176 return fmt.Errorf("found traces of unnamed provisioner/post-processor, should not") 1177 } 1178 1179 return nil 1180 }, 1181 }, 1182 { 1183 "json - only unnamed build", 1184 []string{ 1185 "-only", "null", 1186 testFixture("provisioners", "provisioner-only-except.json"), 1187 }, 1188 0, 1189 func(out, _ string) error { 1190 if !strings.Contains(out, "null provisioner null and null") { 1191 return fmt.Errorf("missing expected provisioner output") 1192 } 1193 1194 if !strings.Contains(out, "null post-processor null and null") { 1195 return fmt.Errorf("missing expected post-processor output") 1196 } 1197 1198 if strings.Contains(out, "packer post-processor") || strings.Contains(out, "packer provisioner") { 1199 return fmt.Errorf("found traces of named provisioner/post-processor, should not") 1200 } 1201 1202 return nil 1203 }, 1204 }, 1205 { 1206 "hcl - only one source build", 1207 []string{ 1208 "-only", "null.packer", 1209 testFixture("provisioners", "provisioner-only-except.pkr.hcl"), 1210 }, 1211 0, 1212 func(out, _ string) error { 1213 if !strings.Contains(out, "packer provisioner packer and null") { 1214 return fmt.Errorf("missing expected provisioner output") 1215 } 1216 1217 if !strings.Contains(out, "packer post-processor packer and null") { 1218 return fmt.Errorf("missing expected post-processor output") 1219 } 1220 1221 if strings.Contains(out, "other post-processor") || strings.Contains(out, "other provisioner") { 1222 return fmt.Errorf("found traces of other provisioner/post-processor, should not") 1223 } 1224 1225 return nil 1226 }, 1227 }, 1228 { 1229 "hcl - only other build", 1230 []string{ 1231 "-only", "null.other", 1232 testFixture("provisioners", "provisioner-only-except.pkr.hcl"), 1233 }, 1234 0, 1235 func(out, _ string) error { 1236 if !strings.Contains(out, "other provisioner other and null") { 1237 return fmt.Errorf("missing expected provisioner output") 1238 } 1239 1240 if !strings.Contains(out, "other post-processor other and null") { 1241 return fmt.Errorf("missing expected post-processor output") 1242 } 1243 1244 if strings.Contains(out, "packer post-processor") || strings.Contains(out, "packer provisioner") { 1245 return fmt.Errorf("found traces of \"packer\" source provisioner/post-processor, should not") 1246 } 1247 1248 return nil 1249 }, 1250 }, 1251 } 1252 1253 for _, tt := range tests { 1254 t.Run(tt.name, func(t *testing.T) { 1255 c := &BuildCommand{ 1256 Meta: TestMetaFile(t), 1257 } 1258 1259 exitCode := c.Run(tt.args) 1260 if exitCode != tt.expectedCode { 1261 t.Errorf("process exit code mismatch: expected %d, got %d", 1262 tt.expectedCode, 1263 exitCode) 1264 } 1265 1266 out, stderr := GetStdoutAndErrFromTestMeta(t, c.Meta) 1267 err := tt.outputCheck(out, stderr) 1268 if err != nil { 1269 if len(out) != 0 { 1270 t.Logf("command stdout: %q", out) 1271 } 1272 1273 if len(stderr) != 0 { 1274 t.Logf("command stderr: %q", stderr) 1275 } 1276 t.Error(err.Error()) 1277 } 1278 }) 1279 } 1280 } 1281 1282 // TestBuildCmd aims to test the build command, with output validation 1283 func TestBuildCmd(t *testing.T) { 1284 tests := []struct { 1285 name string 1286 args []string 1287 expectedCode int 1288 outputCheck func(string, string) error 1289 }{ 1290 { 1291 name: "hcl - no build block error", 1292 args: []string{ 1293 testFixture("hcl", "no_build.pkr.hcl"), 1294 }, 1295 expectedCode: 1, 1296 outputCheck: func(_, err string) error { 1297 if !strings.Contains(err, "Error: Missing build block") { 1298 return fmt.Errorf("expected 'Error: Missing build block' in output, did not find it") 1299 } 1300 1301 nbErrs := strings.Count(err, "Error: ") 1302 if nbErrs != 1 { 1303 return fmt.Errorf( 1304 "error: too many errors in stdout for build block, expected 1, got %d", 1305 nbErrs) 1306 } 1307 1308 return nil 1309 }, 1310 }, 1311 { 1312 name: "hcl - undefined var set in pkrvars", 1313 args: []string{ 1314 testFixture("hcl", "variables", "ref_non_existing"), 1315 }, 1316 expectedCode: 0, 1317 outputCheck: func(out, err string) error { 1318 nbWarns := strings.Count(out, "Warning: ") 1319 if nbWarns != 0 { 1320 return fmt.Errorf( 1321 "error: too many warnings in build output, expected 0, got %d", 1322 nbWarns) 1323 } 1324 1325 nbErrs := strings.Count(err, "Error: ") 1326 if nbErrs != 0 { 1327 return fmt.Errorf("error: expected build to succeed without errors, got %d", 1328 nbErrs) 1329 } 1330 return nil 1331 }, 1332 }, 1333 { 1334 name: "hcl - build block without source", 1335 args: []string{ 1336 testFixture("hcl", "build_no_source.pkr.hcl"), 1337 }, 1338 expectedCode: 1, 1339 outputCheck: func(_, err string) error { 1340 if !strings.Contains(err, "Error: missing source reference") { 1341 return fmt.Errorf("expected 'Error: missing source reference' in output, did not find it") 1342 } 1343 1344 nbErrs := strings.Count(err, "Error: ") 1345 if nbErrs != 1 { 1346 return fmt.Errorf( 1347 "error: too many errors in stderr for build, expected 1, got %d", 1348 nbErrs) 1349 } 1350 1351 logRegex := regexp.MustCompile("on.*build_no_source.pkr.hcl line 1") 1352 if !logRegex.MatchString(err) { 1353 return fmt.Errorf("error: missing context for error message") 1354 } 1355 1356 return nil 1357 }, 1358 }, 1359 { 1360 name: "hcl - exclude post-processor, expect no warning", 1361 args: []string{ 1362 "-except", "manifest", 1363 testFixture("hcl", "test_except_manifest.pkr.hcl"), 1364 }, 1365 expectedCode: 0, 1366 outputCheck: func(out, err string) error { 1367 for _, stream := range []string{out, err} { 1368 if strings.Contains(stream, "Warning: an 'except' option was passed, but did not match any build") { 1369 return fmt.Errorf("Unexpected warning for build no match with except") 1370 } 1371 1372 if strings.Contains(stream, "Running post-processor:") { 1373 return fmt.Errorf("Should not run post-processors, but found one") 1374 } 1375 } 1376 1377 return nil 1378 }, 1379 }, 1380 } 1381 1382 for _, tt := range tests { 1383 t.Run(tt.name, func(t *testing.T) { 1384 c := &BuildCommand{ 1385 Meta: TestMetaFile(t), 1386 } 1387 1388 exitCode := c.Run(tt.args) 1389 if exitCode != tt.expectedCode { 1390 t.Errorf("process exit code mismatch: expected %d, got %d", 1391 tt.expectedCode, 1392 exitCode) 1393 } 1394 1395 out, stderr := GetStdoutAndErrFromTestMeta(t, c.Meta) 1396 err := tt.outputCheck(out, stderr) 1397 if err != nil { 1398 if len(out) != 0 { 1399 t.Logf("command stdout: %q", out) 1400 } 1401 1402 if len(stderr) != 0 { 1403 t.Logf("command stderr: %q", stderr) 1404 } 1405 t.Error(err.Error()) 1406 } 1407 }) 1408 } 1409 }