github.com/YousefHaggyHeroku/pack@v1.5.5/internal/commands/build_test.go (about) 1 package commands_test 2 3 import ( 4 "bytes" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "path/filepath" 9 "reflect" 10 "testing" 11 12 pubcfg "github.com/YousefHaggyHeroku/pack/config" 13 14 "github.com/golang/mock/gomock" 15 "github.com/heroku/color" 16 "github.com/pkg/errors" 17 "github.com/sclevine/spec" 18 "github.com/sclevine/spec/report" 19 "github.com/spf13/cobra" 20 21 "github.com/YousefHaggyHeroku/pack" 22 "github.com/YousefHaggyHeroku/pack/internal/commands" 23 "github.com/YousefHaggyHeroku/pack/internal/commands/testmocks" 24 "github.com/YousefHaggyHeroku/pack/internal/config" 25 ilogging "github.com/YousefHaggyHeroku/pack/internal/logging" 26 h "github.com/YousefHaggyHeroku/pack/testhelpers" 27 ) 28 29 func TestBuildCommand(t *testing.T) { 30 color.Disable(true) 31 defer color.Disable(false) 32 33 spec.Run(t, "Commands", testBuildCommand, spec.Random(), spec.Report(report.Terminal{})) 34 } 35 36 func testBuildCommand(t *testing.T, when spec.G, it spec.S) { 37 var ( 38 command *cobra.Command 39 logger *ilogging.LogWithWriters 40 outBuf bytes.Buffer 41 mockController *gomock.Controller 42 mockClient *testmocks.MockPackClient 43 cfg config.Config 44 ) 45 46 it.Before(func() { 47 logger = ilogging.NewLogWithWriters(&outBuf, &outBuf) 48 cfg = config.Config{} 49 mockController = gomock.NewController(t) 50 mockClient = testmocks.NewMockPackClient(mockController) 51 52 command = commands.Build(logger, cfg, mockClient) 53 }) 54 55 when("#BuildCommand", func() { 56 when("no builder is specified", func() { 57 it("returns a soft error", func() { 58 mockClient.EXPECT().InspectBuilder(gomock.Any(), false).Return(&pack.BuilderInfo{ 59 Description: "", 60 }, nil).AnyTimes() 61 62 command.SetArgs([]string{"image"}) 63 err := command.Execute() 64 h.AssertError(t, err, pack.NewSoftError().Error()) 65 }) 66 }) 67 68 when("a builder and image are set", func() { 69 it("builds an image with a builder", func() { 70 mockClient.EXPECT(). 71 Build(gomock.Any(), EqBuildOptionsWithImage("my-builder", "image")). 72 Return(nil) 73 74 command.SetArgs([]string{"--builder", "my-builder", "image"}) 75 h.AssertNil(t, command.Execute()) 76 }) 77 78 it("builds an image with a builder short command arg", func() { 79 mockClient.EXPECT(). 80 Build(gomock.Any(), EqBuildOptionsWithImage("my-builder", "image")). 81 Return(nil) 82 83 logger.WantVerbose(true) 84 command.SetArgs([]string{"-B", "my-builder", "image"}) 85 h.AssertNil(t, command.Execute()) 86 h.AssertContains(t, outBuf.String(), "Builder 'my-builder' is untrusted") 87 }) 88 89 when("the builder is trusted", func() { 90 it("sets the trust builder option", func() { 91 mockClient.EXPECT(). 92 Build(gomock.Any(), EqBuildOptionsWithTrustedBuilder(true)). 93 Return(nil) 94 95 cfg := config.Config{TrustedBuilders: []config.TrustedBuilder{{Name: "my-builder"}}} 96 command := commands.Build(logger, cfg, mockClient) 97 98 logger.WantVerbose(true) 99 command.SetArgs([]string{"image", "--builder", "my-builder"}) 100 h.AssertNil(t, command.Execute()) 101 h.AssertContains(t, outBuf.String(), "Builder 'my-builder' is trusted") 102 }) 103 }) 104 105 when("the builder is suggested", func() { 106 it("sets the trust builder option", func() { 107 mockClient.EXPECT(). 108 Build(gomock.Any(), EqBuildOptionsWithTrustedBuilder(true)). 109 Return(nil) 110 111 logger.WantVerbose(true) 112 command.SetArgs([]string{"image", "--builder", "heroku/buildpacks:18"}) 113 h.AssertNil(t, command.Execute()) 114 h.AssertContains(t, outBuf.String(), "Builder 'heroku/buildpacks:18' is trusted") 115 }) 116 }) 117 }) 118 119 when("--buildpack-registry flag is specified but experimental isn't set in the config", func() { 120 it("errors with a descriptive message", func() { 121 command.SetArgs([]string{"image", "--builder", "my-builder", "--buildpack-registry", "some-registry"}) 122 err := command.Execute() 123 h.AssertNotNil(t, err) 124 h.AssertError(t, err, "Support for buildpack registries is currently experimental.") 125 }) 126 }) 127 128 when("a network is given", func() { 129 it("forwards the network onto the client", func() { 130 mockClient.EXPECT(). 131 Build(gomock.Any(), EqBuildOptionsWithNetwork("my-network")). 132 Return(nil) 133 134 command.SetArgs([]string{"image", "--builder", "my-builder", "--network", "my-network"}) 135 h.AssertNil(t, command.Execute()) 136 }) 137 }) 138 139 when("--pull-policy", func() { 140 it("sets pull-policy=never", func() { 141 mockClient.EXPECT(). 142 Build(gomock.Any(), EqBuildOptionsWithPullPolicy(pubcfg.PullNever)). 143 Return(nil) 144 145 command.SetArgs([]string{"image", "--builder", "my-builder", "--pull-policy", "never"}) 146 h.AssertNil(t, command.Execute()) 147 }) 148 149 it("returns error for unknown policy", func() { 150 command.SetArgs([]string{"image", "--builder", "my-builder", "--pull-policy", "unknown-policy"}) 151 h.AssertError(t, command.Execute(), "parsing pull policy") 152 }) 153 }) 154 155 when("volume mounts are specified", func() { 156 it("mounts the volumes", func() { 157 mockClient.EXPECT(). 158 Build(gomock.Any(), EqBuildOptionsWithVolumes([]string{"a:b", "c:d"})). 159 Return(nil) 160 161 command.SetArgs([]string{"image", "--builder", "my-builder", "--volume", "a:b", "--volume", "c:d"}) 162 h.AssertNil(t, command.Execute()) 163 }) 164 165 it("warns when running with an untrusted builder", func() { 166 mockClient.EXPECT(). 167 Build(gomock.Any(), EqBuildOptionsWithVolumes([]string{"a:b", "c:d"})). 168 Return(nil) 169 170 command.SetArgs([]string{"image", "--builder", "my-builder", "--volume", "a:b", "--volume", "c:d"}) 171 h.AssertNil(t, command.Execute()) 172 h.AssertContains(t, outBuf.String(), "Warning: Using untrusted builder with volume mounts") 173 }) 174 }) 175 176 when("a default process is specified", func() { 177 it("sets that process", func() { 178 mockClient.EXPECT(). 179 Build(gomock.Any(), EqBuildOptionsDefaultProcess("my-proc")). 180 Return(nil) 181 182 command.SetArgs([]string{"image", "--builder", "my-builder", "--default-process", "my-proc"}) 183 h.AssertNil(t, command.Execute()) 184 }) 185 }) 186 187 when("env file", func() { 188 when("an env file is provided", func() { 189 var envPath string 190 191 it.Before(func() { 192 envfile, err := ioutil.TempFile("", "envfile") 193 h.AssertNil(t, err) 194 defer envfile.Close() 195 196 envfile.WriteString(`KEY=VALUE`) 197 envPath = envfile.Name() 198 }) 199 200 it.After(func() { 201 h.AssertNil(t, os.RemoveAll(envPath)) 202 }) 203 204 it("builds an image env variables read from the env file", func() { 205 mockClient.EXPECT(). 206 Build(gomock.Any(), EqBuildOptionsWithEnv(map[string]string{ 207 "KEY": "VALUE", 208 })). 209 Return(nil) 210 211 command.SetArgs([]string{"--builder", "my-builder", "image", "--env-file", envPath}) 212 h.AssertNil(t, command.Execute()) 213 }) 214 }) 215 216 when("a env file is provided but doesn't exist", func() { 217 it("fails to run", func() { 218 command.SetArgs([]string{"--builder", "my-builder", "image", "--env-file", ""}) 219 err := command.Execute() 220 h.AssertError(t, err, "parse env file") 221 }) 222 }) 223 224 when("an empty env file is provided", func() { 225 var envPath string 226 227 it.Before(func() { 228 envfile, err := ioutil.TempFile("", "envfile") 229 h.AssertNil(t, err) 230 defer envfile.Close() 231 232 envfile.WriteString(``) 233 envPath = envfile.Name() 234 }) 235 236 it.After(func() { 237 h.AssertNil(t, os.RemoveAll(envPath)) 238 }) 239 240 it("successfully builds", func() { 241 mockClient.EXPECT(). 242 Build(gomock.Any(), EqBuildOptionsWithEnv(map[string]string{})). 243 Return(nil) 244 245 command.SetArgs([]string{"--builder", "my-builder", "image", "--env-file", envPath}) 246 h.AssertNil(t, command.Execute()) 247 }) 248 }) 249 250 when("two env files are provided with conflicted keys", func() { 251 var envPath1 string 252 var envPath2 string 253 254 it.Before(func() { 255 envfile1, err := ioutil.TempFile("", "envfile") 256 h.AssertNil(t, err) 257 defer envfile1.Close() 258 259 envfile1.WriteString("KEY1=VALUE1\nKEY2=IGNORED") 260 envPath1 = envfile1.Name() 261 262 envfile2, err := ioutil.TempFile("", "envfile") 263 h.AssertNil(t, err) 264 defer envfile2.Close() 265 266 envfile2.WriteString("KEY2=VALUE2") 267 envPath2 = envfile2.Name() 268 }) 269 270 it.After(func() { 271 h.AssertNil(t, os.RemoveAll(envPath1)) 272 h.AssertNil(t, os.RemoveAll(envPath2)) 273 }) 274 275 it("builds an image with the last value of each env variable", func() { 276 mockClient.EXPECT(). 277 Build(gomock.Any(), EqBuildOptionsWithEnv(map[string]string{ 278 "KEY1": "VALUE1", 279 "KEY2": "VALUE2", 280 })). 281 Return(nil) 282 283 command.SetArgs([]string{"--builder", "my-builder", "image", "--env-file", envPath1, "--env-file", envPath2}) 284 h.AssertNil(t, command.Execute()) 285 }) 286 }) 287 }) 288 289 when("env vars are passed as flags", func() { 290 var ( 291 tmpVar = "tmpVar" 292 tmpValue = "tmpKey" 293 ) 294 295 it.Before(func() { 296 h.AssertNil(t, os.Setenv(tmpVar, tmpValue)) 297 }) 298 299 it.After(func() { 300 h.AssertNil(t, os.Unsetenv(tmpVar)) 301 }) 302 303 it("sets flag variables", func() { 304 mockClient.EXPECT(). 305 Build(gomock.Any(), EqBuildOptionsWithEnv(map[string]string{ 306 "KEY": "VALUE", 307 tmpVar: tmpValue, 308 })). 309 Return(nil) 310 311 command.SetArgs([]string{"image", "--builder", "my-builder", "--env", "KEY=VALUE", "--env", tmpVar}) 312 h.AssertNil(t, command.Execute()) 313 }) 314 }) 315 316 when("build fails", func() { 317 it("should show an error", func() { 318 mockClient.EXPECT(). 319 Build(gomock.Any(), gomock.Any()). 320 Return(errors.New("")) 321 322 command.SetArgs([]string{"--builder", "my-builder", "image"}) 323 err := command.Execute() 324 h.AssertError(t, err, "failed to build") 325 }) 326 }) 327 328 when("user specifies an invalid project descriptor file", func() { 329 it("should show an error", func() { 330 projectTomlPath := "/incorrect/path/to/project.toml" 331 mockClient.EXPECT(). 332 Build(gomock.Any(), EqBuildOptionsWithImage("my-builder", "image")). 333 Return(nil) 334 335 command.SetArgs([]string{"--builder", "my-builder", "--descriptor", projectTomlPath, "image"}) 336 h.AssertNotNil(t, command.Execute()) 337 }) 338 }) 339 340 when("parsing project descriptor", func() { 341 when("file is valid", func() { 342 var projectTomlPath string 343 344 it.Before(func() { 345 projectToml, err := ioutil.TempFile("", "project.toml") 346 h.AssertNil(t, err) 347 defer projectToml.Close() 348 349 projectToml.WriteString(` 350 [project] 351 name = "Sample" 352 353 [[build.buildpacks]] 354 id = "example/lua" 355 version = "1.0" 356 `) 357 projectTomlPath = projectToml.Name() 358 }) 359 360 it.After(func() { 361 h.AssertNil(t, os.RemoveAll(projectTomlPath)) 362 }) 363 364 it("should build an image with configuration in descriptor", func() { 365 mockClient.EXPECT(). 366 Build(gomock.Any(), EqBuildOptionsWithBuildpacks([]string{ 367 "example/lua@1.0", 368 })). 369 Return(nil) 370 371 command.SetArgs([]string{"--builder", "my-builder", "--descriptor", projectTomlPath, "image"}) 372 h.AssertNil(t, command.Execute()) 373 }) 374 }) 375 376 when("file is invalid", func() { 377 var projectTomlPath string 378 379 it.Before(func() { 380 projectToml, err := ioutil.TempFile("", "project.toml") 381 h.AssertNil(t, err) 382 defer projectToml.Close() 383 384 projectToml.WriteString("project]") 385 projectTomlPath = projectToml.Name() 386 }) 387 388 it.After(func() { 389 h.AssertNil(t, os.RemoveAll(projectTomlPath)) 390 }) 391 392 it("should fail to build", func() { 393 mockClient.EXPECT(). 394 Build(gomock.Any(), EqBuildOptionsWithImage("my-builder", "image")). 395 Return(nil) 396 397 command.SetArgs([]string{"--builder", "my-builder", "--descriptor", projectTomlPath, "image"}) 398 h.AssertNotNil(t, command.Execute()) 399 }) 400 }) 401 402 when("descriptor path is NOT specified", func() { 403 when("project.toml exists in source repo", func() { 404 it.Before(func() { 405 h.AssertNil(t, os.Chdir("testdata")) 406 }) 407 408 it.After(func() { 409 h.AssertNil(t, os.Chdir("..")) 410 }) 411 412 it("should use project.toml in source repo", func() { 413 mockClient.EXPECT(). 414 Build(gomock.Any(), EqBuildOptionsWithEnv(map[string]string{ 415 "KEY1": "VALUE1", 416 })). 417 Return(nil) 418 419 command.SetArgs([]string{"--builder", "my-builder", "image"}) 420 h.AssertNil(t, command.Execute()) 421 }) 422 }) 423 424 when("project.toml does NOT exist in source repo", func() { 425 it("should use empty descriptor", func() { 426 mockClient.EXPECT(). 427 Build(gomock.Any(), EqBuildOptionsWithEnv(map[string]string{})). 428 Return(nil) 429 430 command.SetArgs([]string{"--builder", "my-builder", "image"}) 431 h.AssertNil(t, command.Execute()) 432 }) 433 }) 434 }) 435 436 when("descriptor path is specified", func() { 437 when("descriptor file exists", func() { 438 var projectTomlPath string 439 it.Before(func() { 440 projectTomlPath = filepath.Join("testdata", "project.toml") 441 }) 442 443 it("should use specified descriptor", func() { 444 mockClient.EXPECT(). 445 Build(gomock.Any(), EqBuildOptionsWithEnv(map[string]string{ 446 "KEY1": "VALUE1", 447 })). 448 Return(nil) 449 450 command.SetArgs([]string{"--builder", "my-builder", "--descriptor", projectTomlPath, "image"}) 451 h.AssertNil(t, command.Execute()) 452 }) 453 }) 454 455 when("descriptor file does NOT exist in source repo", func() { 456 it("should fail with an error message", func() { 457 command.SetArgs([]string{"--builder", "my-builder", "--descriptor", "non-existent-path", "image"}) 458 h.AssertError(t, command.Execute(), "stat project descriptor") 459 }) 460 }) 461 }) 462 463 when("descriptor buildpack has uri", func() { 464 var projectTomlPath string 465 466 it.Before(func() { 467 projectToml, err := ioutil.TempFile("", "project.toml") 468 h.AssertNil(t, err) 469 defer projectToml.Close() 470 471 projectToml.WriteString(` 472 [project] 473 name = "Sample" 474 475 [[build.buildpacks]] 476 id = "example/lua" 477 uri = "https://www.test.tgz" 478 `) 479 projectTomlPath = projectToml.Name() 480 }) 481 482 it.After(func() { 483 h.AssertNil(t, os.RemoveAll(projectTomlPath)) 484 }) 485 486 it("should build an image with configuration in descriptor", func() { 487 mockClient.EXPECT(). 488 Build(gomock.Any(), EqBuildOptionsWithBuildpacks([]string{ 489 "https://www.test.tgz", 490 })). 491 Return(nil) 492 493 command.SetArgs([]string{"image", "--builder", "my-builder", "--descriptor", projectTomlPath}) 494 h.AssertNil(t, command.Execute()) 495 }) 496 }) 497 498 when("descriptor buildpack has malformed uri", func() { 499 var projectTomlPath string 500 501 it.Before(func() { 502 projectToml, err := ioutil.TempFile("", "project.toml") 503 h.AssertNil(t, err) 504 defer projectToml.Close() 505 506 projectToml.WriteString(` 507 [project] 508 name = "Sample" 509 510 [[build.buildpacks]] 511 id = "example/lua" 512 uri = "://bad-uri" 513 `) 514 projectTomlPath = projectToml.Name() 515 }) 516 517 it.After(func() { 518 h.AssertNil(t, os.RemoveAll(projectTomlPath)) 519 }) 520 521 it("should build an image with configuration in descriptor", func() { 522 mockClient.EXPECT(). 523 Build(gomock.Any(), EqBuildOptionsWithBuildpacks([]string{ 524 "https://www.test.tgz", 525 })). 526 Return(nil) 527 528 command.SetArgs([]string{"image", "--builder", "my-builder", "--descriptor", projectTomlPath}) 529 err := command.Execute() 530 h.AssertError(t, err, "parse") 531 }) 532 }) 533 534 when("descriptor has exclude", func() { 535 var projectTomlPath string 536 537 it.Before(func() { 538 projectToml, err := ioutil.TempFile("", "project.toml") 539 h.AssertNil(t, err) 540 defer projectToml.Close() 541 542 projectToml.WriteString(` 543 [project] 544 name = "Sample" 545 546 [build] 547 exclude = [ "*.jar" ] 548 `) 549 projectTomlPath = projectToml.Name() 550 }) 551 552 it.After(func() { 553 h.AssertNil(t, os.RemoveAll(projectTomlPath)) 554 }) 555 556 it("should return appropriate fileFilter function", func() { 557 mockFilter := func(string) bool { 558 return false 559 } 560 561 mockClient.EXPECT(). 562 Build(gomock.Any(), EqBuildOptionsWithFileFilter(mockFilter, "test.jar")). 563 Return(nil) 564 565 command.SetArgs([]string{"image", "--builder", "my-builder", "--descriptor", projectTomlPath}) 566 h.AssertNil(t, command.Execute()) 567 }) 568 }) 569 570 when("descriptor has include", func() { 571 var projectTomlPath string 572 it.Before(func() { 573 projectToml, err := ioutil.TempFile("", "project.toml") 574 h.AssertNil(t, err) 575 defer projectToml.Close() 576 577 projectToml.WriteString(` 578 [project] 579 name = "Sample" 580 581 [build] 582 include = [ "*.jar" ] 583 `) 584 projectTomlPath = projectToml.Name() 585 }) 586 587 it.After(func() { 588 h.AssertNil(t, os.RemoveAll(projectTomlPath)) 589 }) 590 591 it("should return appropriate fileFilter function", func() { 592 mockFilter := func(string) bool { 593 return true 594 } 595 596 mockClient.EXPECT(). 597 Build(gomock.Any(), EqBuildOptionsWithFileFilter(mockFilter, "test.jar")). 598 Return(nil) 599 600 command.SetArgs([]string{"image", "--builder", "my-builder", "--descriptor", projectTomlPath}) 601 h.AssertNil(t, command.Execute()) 602 }) 603 }) 604 }) 605 }) 606 } 607 608 func EqBuildOptionsWithImage(builder, image string) gomock.Matcher { 609 return buildOptionsMatcher{ 610 description: fmt.Sprintf("Builder=%s and Image=%s", builder, image), 611 equals: func(o pack.BuildOptions) bool { 612 return o.Builder == builder && o.Image == image 613 }, 614 } 615 } 616 617 func EqBuildOptionsDefaultProcess(defaultProc string) gomock.Matcher { 618 return buildOptionsMatcher{ 619 description: fmt.Sprintf("Default Process Type=%s", defaultProc), 620 equals: func(o pack.BuildOptions) bool { 621 return o.DefaultProcessType == defaultProc 622 }, 623 } 624 } 625 626 func EqBuildOptionsWithPullPolicy(policy pubcfg.PullPolicy) gomock.Matcher { 627 return buildOptionsMatcher{ 628 description: fmt.Sprintf("PullPolicy=%s", policy), 629 equals: func(o pack.BuildOptions) bool { 630 return o.PullPolicy == policy 631 }, 632 } 633 } 634 635 func EqBuildOptionsWithNetwork(network string) gomock.Matcher { 636 return buildOptionsMatcher{ 637 description: fmt.Sprintf("Network=%s", network), 638 equals: func(o pack.BuildOptions) bool { 639 return o.ContainerConfig.Network == network 640 }, 641 } 642 } 643 644 func EqBuildOptionsWithTrustedBuilder(trustBuilder bool) gomock.Matcher { 645 return buildOptionsMatcher{ 646 description: fmt.Sprintf("Trust Builder=%t", trustBuilder), 647 equals: func(o pack.BuildOptions) bool { 648 return o.TrustBuilder == trustBuilder 649 }, 650 } 651 } 652 653 func EqBuildOptionsWithFileFilter(fileFilter func(string) bool, fileName string) gomock.Matcher { 654 return buildOptionsMatcher{ 655 description: fmt.Sprintf("File Filter=%p", fileFilter), 656 equals: func(o pack.BuildOptions) bool { 657 return o.FileFilter(fileName) == fileFilter(fileName) 658 }, 659 } 660 } 661 662 func EqBuildOptionsWithVolumes(volumes []string) gomock.Matcher { 663 return buildOptionsMatcher{ 664 description: fmt.Sprintf("Volumes=%s", volumes), 665 equals: func(o pack.BuildOptions) bool { 666 return reflect.DeepEqual(o.ContainerConfig.Volumes, volumes) 667 }, 668 } 669 } 670 671 func EqBuildOptionsWithEnv(env map[string]string) gomock.Matcher { 672 return buildOptionsMatcher{ 673 description: fmt.Sprintf("Env=%+v", env), 674 equals: func(o pack.BuildOptions) bool { 675 for k, v := range o.Env { 676 if env[k] != v { 677 return false 678 } 679 } 680 for k, v := range env { 681 if o.Env[k] != v { 682 return false 683 } 684 } 685 return true 686 }, 687 } 688 } 689 690 func EqBuildOptionsWithBuildpacks(buildpacks []string) gomock.Matcher { 691 return buildOptionsMatcher{ 692 description: fmt.Sprintf("Buildpacks=%+v", buildpacks), 693 equals: func(o pack.BuildOptions) bool { 694 for _, bp := range o.Buildpacks { 695 if !contains(buildpacks, bp) { 696 return false 697 } 698 } 699 for _, bp := range buildpacks { 700 if !contains(o.Buildpacks, bp) { 701 return false 702 } 703 } 704 return true 705 }, 706 } 707 } 708 709 type buildOptionsMatcher struct { 710 equals func(pack.BuildOptions) bool 711 description string 712 } 713 714 func (m buildOptionsMatcher) Matches(x interface{}) bool { 715 if b, ok := x.(pack.BuildOptions); ok { 716 return m.equals(b) 717 } 718 return false 719 } 720 721 func (m buildOptionsMatcher) String() string { 722 return "is a BuildOptions with " + m.description 723 } 724 725 func contains(arr []string, str string) bool { 726 for _, a := range arr { 727 if a == str { 728 return true 729 } 730 } 731 return false 732 }