github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/internal/commands/build_test.go (about) 1 package commands_test 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "path/filepath" 8 "reflect" 9 "testing" 10 "time" 11 12 "github.com/buildpacks/lifecycle/api" 13 "github.com/golang/mock/gomock" 14 "github.com/heroku/color" 15 "github.com/pkg/errors" 16 "github.com/sclevine/spec" 17 "github.com/sclevine/spec/report" 18 "github.com/spf13/cobra" 19 20 "github.com/buildpacks/pack/internal/paths" 21 22 "github.com/buildpacks/pack/internal/commands" 23 "github.com/buildpacks/pack/internal/commands/testmocks" 24 "github.com/buildpacks/pack/internal/config" 25 "github.com/buildpacks/pack/pkg/client" 26 "github.com/buildpacks/pack/pkg/image" 27 "github.com/buildpacks/pack/pkg/logging" 28 projectTypes "github.com/buildpacks/pack/pkg/project/types" 29 h "github.com/buildpacks/pack/testhelpers" 30 ) 31 32 func TestBuildCommand(t *testing.T) { 33 color.Disable(true) 34 defer color.Disable(false) 35 36 spec.Run(t, "Commands", testBuildCommand, spec.Random(), spec.Report(report.Terminal{})) 37 } 38 39 func testBuildCommand(t *testing.T, when spec.G, it spec.S) { 40 var ( 41 command *cobra.Command 42 logger *logging.LogWithWriters 43 outBuf bytes.Buffer 44 mockController *gomock.Controller 45 mockClient *testmocks.MockPackClient 46 cfg config.Config 47 ) 48 49 it.Before(func() { 50 logger = logging.NewLogWithWriters(&outBuf, &outBuf) 51 cfg = config.Config{} 52 mockController = gomock.NewController(t) 53 mockClient = testmocks.NewMockPackClient(mockController) 54 55 command = commands.Build(logger, cfg, mockClient) 56 }) 57 58 when("#BuildCommand", func() { 59 when("no builder is specified", func() { 60 it("returns a soft error", func() { 61 mockClient.EXPECT(). 62 InspectBuilder(gomock.Any(), false). 63 Return(&client.BuilderInfo{Description: ""}, nil). 64 AnyTimes() 65 66 command.SetArgs([]string{"image"}) 67 err := command.Execute() 68 h.AssertError(t, err, client.NewSoftError().Error()) 69 }) 70 }) 71 72 when("a builder and image are set", func() { 73 it("builds an image with a builder", func() { 74 mockClient.EXPECT(). 75 Build(gomock.Any(), EqBuildOptionsWithImage("my-builder", "image")). 76 Return(nil) 77 78 command.SetArgs([]string{"--builder", "my-builder", "image"}) 79 h.AssertNil(t, command.Execute()) 80 }) 81 82 it("builds an image with a builder short command arg", func() { 83 mockClient.EXPECT(). 84 Build(gomock.Any(), EqBuildOptionsWithImage("my-builder", "image")). 85 Return(nil) 86 87 logger.WantVerbose(true) 88 command.SetArgs([]string{"-B", "my-builder", "image"}) 89 h.AssertNil(t, command.Execute()) 90 h.AssertContains(t, outBuf.String(), "Builder 'my-builder' is untrusted") 91 }) 92 93 when("the builder is trusted", func() { 94 it.Before(func() { 95 mockClient.EXPECT(). 96 Build(gomock.Any(), EqBuildOptionsWithTrustedBuilder(true)). 97 Return(nil) 98 99 cfg := config.Config{TrustedBuilders: []config.TrustedBuilder{{Name: "my-builder"}}} 100 command = commands.Build(logger, cfg, mockClient) 101 }) 102 it("sets the trust builder option", func() { 103 logger.WantVerbose(true) 104 command.SetArgs([]string{"image", "--builder", "my-builder"}) 105 h.AssertNil(t, command.Execute()) 106 h.AssertContains(t, outBuf.String(), "Builder 'my-builder' is trusted") 107 }) 108 when("a lifecycle-image is provided", func() { 109 it("ignoring the mentioned lifecycle image, going with default version", func() { 110 command.SetArgs([]string{"--builder", "my-builder", "image", "--lifecycle-image", "some-lifecycle-image"}) 111 h.AssertNil(t, command.Execute()) 112 h.AssertContains(t, outBuf.String(), "Warning: Ignoring the provided lifecycle image as the builder is trusted, running the creator in a single container using the provided builder") 113 }) 114 }) 115 }) 116 117 when("the builder is suggested", func() { 118 it("sets the trust builder option", func() { 119 mockClient.EXPECT(). 120 Build(gomock.Any(), EqBuildOptionsWithTrustedBuilder(true)). 121 Return(nil) 122 123 logger.WantVerbose(true) 124 command.SetArgs([]string{"image", "--builder", "heroku/builder:22"}) 125 h.AssertNil(t, command.Execute()) 126 h.AssertContains(t, outBuf.String(), "Builder 'heroku/builder:22' is trusted") 127 }) 128 }) 129 }) 130 131 when("--buildpack-registry flag is specified but experimental isn't set in the config", func() { 132 it("errors with a descriptive message", func() { 133 command.SetArgs([]string{"image", "--builder", "my-builder", "--buildpack-registry", "some-registry"}) 134 err := command.Execute() 135 h.AssertNotNil(t, err) 136 h.AssertError(t, err, "Support for buildpack registries is currently experimental.") 137 }) 138 }) 139 140 when("a network is given", func() { 141 it("forwards the network onto the client", func() { 142 mockClient.EXPECT(). 143 Build(gomock.Any(), EqBuildOptionsWithNetwork("my-network")). 144 Return(nil) 145 146 command.SetArgs([]string{"image", "--builder", "my-builder", "--network", "my-network"}) 147 h.AssertNil(t, command.Execute()) 148 }) 149 }) 150 151 when("--pull-policy", func() { 152 it("sets pull-policy=never", func() { 153 mockClient.EXPECT(). 154 Build(gomock.Any(), EqBuildOptionsWithPullPolicy(image.PullNever)). 155 Return(nil) 156 157 command.SetArgs([]string{"image", "--builder", "my-builder", "--pull-policy", "never"}) 158 h.AssertNil(t, command.Execute()) 159 }) 160 it("returns error for unknown policy", func() { 161 command.SetArgs([]string{"image", "--builder", "my-builder", "--pull-policy", "unknown-policy"}) 162 h.AssertError(t, command.Execute(), "parsing pull policy") 163 }) 164 it("takes precedence over a configured pull policy", func() { 165 mockClient.EXPECT(). 166 Build(gomock.Any(), EqBuildOptionsWithPullPolicy(image.PullNever)). 167 Return(nil) 168 169 cfg := config.Config{PullPolicy: "if-not-present"} 170 command := commands.Build(logger, cfg, mockClient) 171 172 logger.WantVerbose(true) 173 command.SetArgs([]string{"image", "--builder", "my-builder", "--pull-policy", "never"}) 174 h.AssertNil(t, command.Execute()) 175 }) 176 }) 177 178 when("--pull-policy is not specified", func() { 179 when("no pull policy set in config", func() { 180 it("uses the default policy", func() { 181 mockClient.EXPECT(). 182 Build(gomock.Any(), EqBuildOptionsWithPullPolicy(image.PullAlways)). 183 Return(nil) 184 185 command.SetArgs([]string{"image", "--builder", "my-builder"}) 186 h.AssertNil(t, command.Execute()) 187 }) 188 }) 189 when("pull policy is set in config", func() { 190 it("uses the set policy", func() { 191 mockClient.EXPECT(). 192 Build(gomock.Any(), EqBuildOptionsWithPullPolicy(image.PullNever)). 193 Return(nil) 194 195 cfg := config.Config{PullPolicy: "never"} 196 command := commands.Build(logger, cfg, mockClient) 197 198 logger.WantVerbose(true) 199 command.SetArgs([]string{"image", "--builder", "my-builder"}) 200 h.AssertNil(t, command.Execute()) 201 }) 202 }) 203 }) 204 205 when("volume mounts are specified", func() { 206 it("mounts the volumes", func() { 207 mockClient.EXPECT(). 208 Build(gomock.Any(), EqBuildOptionsWithVolumes([]string{"a:b", "c:d"})). 209 Return(nil) 210 211 command.SetArgs([]string{"image", "--builder", "my-builder", "--volume", "a:b", "--volume", "c:d"}) 212 h.AssertNil(t, command.Execute()) 213 }) 214 215 it("warns when running with an untrusted builder", func() { 216 mockClient.EXPECT(). 217 Build(gomock.Any(), EqBuildOptionsWithVolumes([]string{"a:b", "c:d"})). 218 Return(nil) 219 220 command.SetArgs([]string{"image", "--builder", "my-builder", "--volume", "a:b", "--volume", "c:d"}) 221 h.AssertNil(t, command.Execute()) 222 h.AssertContains(t, outBuf.String(), "Warning: Using untrusted builder with volume mounts") 223 }) 224 }) 225 226 when("a default process is specified", func() { 227 it("sets that process", func() { 228 mockClient.EXPECT(). 229 Build(gomock.Any(), EqBuildOptionsDefaultProcess("my-proc")). 230 Return(nil) 231 232 command.SetArgs([]string{"image", "--builder", "my-builder", "--default-process", "my-proc"}) 233 h.AssertNil(t, command.Execute()) 234 }) 235 }) 236 237 when("env file", func() { 238 when("an env file is provided", func() { 239 var envPath string 240 241 it.Before(func() { 242 envfile, err := os.CreateTemp("", "envfile") 243 h.AssertNil(t, err) 244 defer envfile.Close() 245 246 envfile.WriteString(`KEY=VALUE`) 247 envPath = envfile.Name() 248 }) 249 250 it.After(func() { 251 h.AssertNil(t, os.RemoveAll(envPath)) 252 }) 253 254 it("builds an image env variables read from the env file", func() { 255 mockClient.EXPECT(). 256 Build(gomock.Any(), EqBuildOptionsWithEnv(map[string]string{ 257 "KEY": "VALUE", 258 })). 259 Return(nil) 260 261 command.SetArgs([]string{"--builder", "my-builder", "image", "--env-file", envPath}) 262 h.AssertNil(t, command.Execute()) 263 }) 264 }) 265 266 when("a env file is provided but doesn't exist", func() { 267 it("fails to run", func() { 268 command.SetArgs([]string{"--builder", "my-builder", "image", "--env-file", ""}) 269 err := command.Execute() 270 h.AssertError(t, err, "parse env file") 271 }) 272 }) 273 274 when("an empty env file is provided", func() { 275 var envPath string 276 277 it.Before(func() { 278 envfile, err := os.CreateTemp("", "envfile") 279 h.AssertNil(t, err) 280 defer envfile.Close() 281 282 envfile.WriteString(``) 283 envPath = envfile.Name() 284 }) 285 286 it.After(func() { 287 h.AssertNil(t, os.RemoveAll(envPath)) 288 }) 289 290 it("successfully builds", func() { 291 mockClient.EXPECT(). 292 Build(gomock.Any(), EqBuildOptionsWithEnv(map[string]string{})). 293 Return(nil) 294 295 command.SetArgs([]string{"--builder", "my-builder", "image", "--env-file", envPath}) 296 h.AssertNil(t, command.Execute()) 297 }) 298 }) 299 300 when("two env files are provided with conflicted keys", func() { 301 var envPath1 string 302 var envPath2 string 303 304 it.Before(func() { 305 envfile1, err := os.CreateTemp("", "envfile") 306 h.AssertNil(t, err) 307 defer envfile1.Close() 308 309 envfile1.WriteString("KEY1=VALUE1\nKEY2=IGNORED") 310 envPath1 = envfile1.Name() 311 312 envfile2, err := os.CreateTemp("", "envfile") 313 h.AssertNil(t, err) 314 defer envfile2.Close() 315 316 envfile2.WriteString("KEY2=VALUE2") 317 envPath2 = envfile2.Name() 318 }) 319 320 it.After(func() { 321 h.AssertNil(t, os.RemoveAll(envPath1)) 322 h.AssertNil(t, os.RemoveAll(envPath2)) 323 }) 324 325 it("builds an image with the last value of each env variable", func() { 326 mockClient.EXPECT(). 327 Build(gomock.Any(), EqBuildOptionsWithEnv(map[string]string{ 328 "KEY1": "VALUE1", 329 "KEY2": "VALUE2", 330 })). 331 Return(nil) 332 333 command.SetArgs([]string{"--builder", "my-builder", "image", "--env-file", envPath1, "--env-file", envPath2}) 334 h.AssertNil(t, command.Execute()) 335 }) 336 }) 337 }) 338 339 when("a cache-image passed", func() { 340 when("--publish is not used", func() { 341 it("errors", func() { 342 command.SetArgs([]string{"--builder", "my-builder", "image", "--cache-image", "some-cache-image"}) 343 err := command.Execute() 344 h.AssertError(t, err, "cache-image flag requires the publish flag") 345 }) 346 }) 347 when("--publish is used", func() { 348 it("succeeds", func() { 349 mockClient.EXPECT(). 350 Build(gomock.Any(), EqBuildOptionsWithCacheImage("some-cache-image")). 351 Return(nil) 352 353 command.SetArgs([]string{"--builder", "my-builder", "image", "--cache-image", "some-cache-image", "--publish"}) 354 h.AssertNil(t, command.Execute()) 355 }) 356 }) 357 }) 358 359 when("cache flag with 'format=image' is passed", func() { 360 when("--publish is not used", func() { 361 it("errors", func() { 362 command.SetArgs([]string{"--builder", "my-builder", "image", "--cache", "type=build;format=image;name=myorg/myimage:cache"}) 363 err := command.Execute() 364 h.AssertError(t, err, "image cache format requires the 'publish' flag") 365 }) 366 }) 367 when("--publish is used", func() { 368 it("succeeds", func() { 369 mockClient.EXPECT(). 370 Build(gomock.Any(), EqBuildOptionsWithCacheFlags("type=build;format=image;name=myorg/myimage:cache;type=launch;format=volume;")). 371 Return(nil) 372 373 command.SetArgs([]string{"--builder", "my-builder", "image", "--cache", "type=build;format=image;name=myorg/myimage:cache", "--publish"}) 374 h.AssertNil(t, command.Execute()) 375 }) 376 }) 377 when("used together with --cache-image", func() { 378 it("errors", func() { 379 command.SetArgs([]string{"--builder", "my-builder", "image", "--cache-image", "some-cache-image", "--cache", "type=build;format=image;name=myorg/myimage:cache"}) 380 err := command.Execute() 381 h.AssertError(t, err, "'cache' flag with 'image' format cannot be used with 'cache-image' flag") 382 }) 383 }) 384 when("'type=launch;format=image' is used", func() { 385 it("warns", func() { 386 mockClient.EXPECT(). 387 Build(gomock.Any(), EqBuildOptionsWithCacheFlags("type=build;format=volume;type=launch;format=image;name=myorg/myimage:cache;")). 388 Return(nil) 389 390 command.SetArgs([]string{"--builder", "my-builder", "image", "--cache", "type=launch;format=image;name=myorg/myimage:cache", "--publish"}) 391 h.AssertNil(t, command.Execute()) 392 h.AssertContains(t, outBuf.String(), "Warning: cache definition: 'launch' cache in format 'image' is not supported.") 393 }) 394 }) 395 }) 396 397 when("a valid lifecycle-image is provided", func() { 398 when("only the image repo is provided", func() { 399 it("uses the provided lifecycle-image and parses it correctly", func() { 400 mockClient.EXPECT(). 401 Build(gomock.Any(), EqBuildOptionsWithLifecycleImage("index.docker.io/library/some-lifecycle-image:latest")). 402 Return(nil) 403 404 command.SetArgs([]string{"--builder", "my-builder", "image", "--lifecycle-image", "some-lifecycle-image"}) 405 h.AssertNil(t, command.Execute()) 406 }) 407 }) 408 when("a custom image repo is provided", func() { 409 it("uses the provided lifecycle-image and parses it correctly", func() { 410 mockClient.EXPECT(). 411 Build(gomock.Any(), EqBuildOptionsWithLifecycleImage("test.com/some-lifecycle-image:latest")). 412 Return(nil) 413 414 command.SetArgs([]string{"--builder", "my-builder", "image", "--lifecycle-image", "test.com/some-lifecycle-image"}) 415 h.AssertNil(t, command.Execute()) 416 }) 417 }) 418 when("a custom image repo is provided with a tag", func() { 419 it("uses the provided lifecycle-image and parses it correctly", func() { 420 mockClient.EXPECT(). 421 Build(gomock.Any(), EqBuildOptionsWithLifecycleImage("test.com/some-lifecycle-image:v1")). 422 Return(nil) 423 424 command.SetArgs([]string{"--builder", "my-builder", "image", "--lifecycle-image", "test.com/some-lifecycle-image:v1"}) 425 h.AssertNil(t, command.Execute()) 426 }) 427 }) 428 when("a custom image repo is provided with a digest", func() { 429 it("uses the provided lifecycle-image and parses it correctly", func() { 430 mockClient.EXPECT(). 431 Build(gomock.Any(), EqBuildOptionsWithLifecycleImage("test.com/some-lifecycle-image@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")). 432 Return(nil) 433 434 command.SetArgs([]string{"--builder", "my-builder", "image", "--lifecycle-image", "test.com/some-lifecycle-image@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}) 435 h.AssertNil(t, command.Execute()) 436 }) 437 }) 438 }) 439 440 when("an invalid lifecycle-image is provided", func() { 441 when("the repo name is invalid", func() { 442 it("returns a parse error", func() { 443 command.SetArgs([]string{"--builder", "my-builder", "image", "--lifecycle-image", "some-!nv@l!d-image"}) 444 err := command.Execute() 445 h.AssertError(t, err, "could not parse reference: some-!nv@l!d-image") 446 }) 447 }) 448 }) 449 450 when("a lifecycle-image is not provided", func() { 451 when("a lifecycle-image is set in the config", func() { 452 it("uses the lifecycle-image from the config after parsing it", func() { 453 mockClient.EXPECT(). 454 Build(gomock.Any(), EqBuildOptionsWithLifecycleImage("index.docker.io/library/some-lifecycle-image:latest")). 455 Return(nil) 456 457 cfg := config.Config{LifecycleImage: "some-lifecycle-image"} 458 command := commands.Build(logger, cfg, mockClient) 459 460 logger.WantVerbose(true) 461 command.SetArgs([]string{"image", "--builder", "my-builder"}) 462 h.AssertNil(t, command.Execute()) 463 }) 464 }) 465 when("a lifecycle-image is not set in the config", func() { 466 it("passes an empty lifecycle image and does not throw an error", func() { 467 mockClient.EXPECT(). 468 Build(gomock.Any(), EqBuildOptionsWithLifecycleImage("")). 469 Return(nil) 470 471 command.SetArgs([]string{"--builder", "my-builder", "image"}) 472 h.AssertNil(t, command.Execute()) 473 }) 474 }) 475 }) 476 477 when("env vars are passed as flags", func() { 478 var ( 479 tmpVar = "tmpVar" 480 tmpValue = "tmpKey" 481 ) 482 483 it.Before(func() { 484 h.AssertNil(t, os.Setenv(tmpVar, tmpValue)) 485 }) 486 487 it.After(func() { 488 h.AssertNil(t, os.Unsetenv(tmpVar)) 489 }) 490 491 it("sets flag variables", func() { 492 mockClient.EXPECT(). 493 Build(gomock.Any(), EqBuildOptionsWithEnv(map[string]string{ 494 "KEY": "VALUE", 495 tmpVar: tmpValue, 496 })). 497 Return(nil) 498 499 command.SetArgs([]string{"image", "--builder", "my-builder", "--env", "KEY=VALUE", "--env", tmpVar}) 500 h.AssertNil(t, command.Execute()) 501 }) 502 }) 503 504 when("build fails", func() { 505 it("should show an error", func() { 506 mockClient.EXPECT(). 507 Build(gomock.Any(), gomock.Any()). 508 Return(errors.New("")) 509 510 command.SetArgs([]string{"--builder", "my-builder", "image"}) 511 err := command.Execute() 512 h.AssertError(t, err, "failed to build") 513 }) 514 }) 515 516 when("user specifies an invalid project descriptor file", func() { 517 it("should show an error", func() { 518 projectTomlPath := "/incorrect/path/to/project.toml" 519 520 command.SetArgs([]string{"--builder", "my-builder", "--descriptor", projectTomlPath, "image"}) 521 h.AssertNotNil(t, command.Execute()) 522 }) 523 }) 524 525 when("parsing project descriptor", func() { 526 when("file is valid", func() { 527 var projectTomlPath string 528 529 it.Before(func() { 530 projectToml, err := os.CreateTemp("", "project.toml") 531 h.AssertNil(t, err) 532 defer projectToml.Close() 533 534 projectToml.WriteString(` 535 [project] 536 name = "Sample" 537 538 [[build.buildpacks]] 539 id = "example/lua" 540 version = "1.0" 541 `) 542 projectTomlPath = projectToml.Name() 543 }) 544 545 it.After(func() { 546 h.AssertNil(t, os.RemoveAll(projectTomlPath)) 547 }) 548 549 it("should build an image with configuration in descriptor", func() { 550 mockClient.EXPECT(). 551 Build(gomock.Any(), EqBuildOptionsWithProjectDescriptor(projectTypes.Descriptor{ 552 Project: projectTypes.Project{ 553 Name: "Sample", 554 }, 555 Build: projectTypes.Build{ 556 Buildpacks: []projectTypes.Buildpack{{ 557 ID: "example/lua", 558 Version: "1.0", 559 }}, 560 }, 561 SchemaVersion: api.MustParse("0.1"), 562 })). 563 Return(nil) 564 565 command.SetArgs([]string{"--builder", "my-builder", "--descriptor", projectTomlPath, "image"}) 566 h.AssertNil(t, command.Execute()) 567 }) 568 }) 569 570 when("file has a builder specified", func() { 571 var projectTomlPath string 572 573 it.Before(func() { 574 projectToml, err := os.CreateTemp("", "project.toml") 575 h.AssertNil(t, err) 576 defer projectToml.Close() 577 578 projectToml.WriteString(` 579 [project] 580 name = "Sample" 581 582 [build] 583 builder = "my-builder" 584 `) 585 projectTomlPath = projectToml.Name() 586 }) 587 588 it.After(func() { 589 h.AssertNil(t, os.RemoveAll(projectTomlPath)) 590 }) 591 when("a builder is not explicitly passed by the user", func() { 592 it("should build an image with configuration in descriptor", func() { 593 mockClient.EXPECT(). 594 Build(gomock.Any(), EqBuildOptionsWithBuilder("my-builder")). 595 Return(nil) 596 597 command.SetArgs([]string{"--descriptor", projectTomlPath, "image"}) 598 h.AssertNil(t, command.Execute()) 599 }) 600 }) 601 when("a builder is explicitly passed by the user", func() { 602 it("should build an image with the passed builder flag", func() { 603 mockClient.EXPECT(). 604 Build(gomock.Any(), EqBuildOptionsWithBuilder("flag-builder")). 605 Return(nil) 606 607 command.SetArgs([]string{"--builder", "flag-builder", "--descriptor", projectTomlPath, "image"}) 608 h.AssertNil(t, command.Execute()) 609 }) 610 }) 611 }) 612 613 when("file is invalid", func() { 614 var projectTomlPath string 615 616 it.Before(func() { 617 projectToml, err := os.CreateTemp("", "project.toml") 618 h.AssertNil(t, err) 619 defer projectToml.Close() 620 621 projectToml.WriteString("project]") 622 projectTomlPath = projectToml.Name() 623 }) 624 625 it.After(func() { 626 h.AssertNil(t, os.RemoveAll(projectTomlPath)) 627 }) 628 629 it("should fail to build", func() { 630 command.SetArgs([]string{"--builder", "my-builder", "--descriptor", projectTomlPath, "image"}) 631 h.AssertNotNil(t, command.Execute()) 632 }) 633 }) 634 635 when("descriptor path is NOT specified", func() { 636 when("project.toml exists in source repo", func() { 637 it.Before(func() { 638 h.AssertNil(t, os.Chdir("testdata")) 639 }) 640 641 it.After(func() { 642 h.AssertNil(t, os.Chdir("..")) 643 }) 644 645 it("should use project.toml in source repo", func() { 646 mockClient.EXPECT(). 647 Build(gomock.Any(), EqBuildOptionsWithProjectDescriptor(projectTypes.Descriptor{ 648 Project: projectTypes.Project{ 649 Name: "Sample", 650 }, 651 Build: projectTypes.Build{ 652 Buildpacks: []projectTypes.Buildpack{{ 653 ID: "example/lua", 654 Version: "1.0", 655 }}, 656 Env: []projectTypes.EnvVar{{ 657 Name: "KEY1", 658 Value: "VALUE1", 659 }}, 660 }, 661 SchemaVersion: api.MustParse("0.1"), 662 })). 663 Return(nil) 664 665 command.SetArgs([]string{"--builder", "my-builder", "image"}) 666 h.AssertNil(t, command.Execute()) 667 }) 668 }) 669 670 when("project.toml does NOT exist in source repo", func() { 671 it("should use empty descriptor", func() { 672 mockClient.EXPECT(). 673 Build(gomock.Any(), EqBuildOptionsWithEnv(map[string]string{})). 674 Return(nil) 675 676 command.SetArgs([]string{"--builder", "my-builder", "image"}) 677 h.AssertNil(t, command.Execute()) 678 }) 679 }) 680 }) 681 682 when("descriptor path is specified", func() { 683 when("descriptor file exists", func() { 684 var projectTomlPath string 685 it.Before(func() { 686 projectTomlPath = filepath.Join("testdata", "project.toml") 687 }) 688 689 it("should use specified descriptor", func() { 690 mockClient.EXPECT(). 691 Build(gomock.Any(), EqBuildOptionsWithProjectDescriptor(projectTypes.Descriptor{ 692 Project: projectTypes.Project{ 693 Name: "Sample", 694 }, 695 Build: projectTypes.Build{ 696 Buildpacks: []projectTypes.Buildpack{{ 697 ID: "example/lua", 698 Version: "1.0", 699 }}, 700 Env: []projectTypes.EnvVar{{ 701 Name: "KEY1", 702 Value: "VALUE1", 703 }}, 704 }, 705 SchemaVersion: api.MustParse("0.1"), 706 })). 707 Return(nil) 708 709 command.SetArgs([]string{"--builder", "my-builder", "--descriptor", projectTomlPath, "image"}) 710 h.AssertNil(t, command.Execute()) 711 }) 712 }) 713 714 when("descriptor file does NOT exist in source repo", func() { 715 it("should fail with an error message", func() { 716 command.SetArgs([]string{"--builder", "my-builder", "--descriptor", "non-existent-path", "image"}) 717 h.AssertError(t, command.Execute(), "stat project descriptor") 718 }) 719 }) 720 }) 721 }) 722 723 when("additional tags are specified", func() { 724 it("forwards additional tags to lifecycle", func() { 725 expectedTags := []string{"additional-tag-1", "additional-tag-2"} 726 mockClient.EXPECT(). 727 Build(gomock.Any(), EqBuildOptionsWithAdditionalTags(expectedTags)). 728 Return(nil) 729 730 command.SetArgs([]string{"image", "--builder", "my-builder", "--tag", expectedTags[0], "--tag", expectedTags[1]}) 731 h.AssertNil(t, command.Execute()) 732 }) 733 }) 734 735 when("gid flag is provided", func() { 736 when("--gid is a valid value", func() { 737 it("override build option should be set to true", func() { 738 mockClient.EXPECT(). 739 Build(gomock.Any(), EqBuildOptionsWithOverrideGroupID(1)). 740 Return(nil) 741 742 command.SetArgs([]string{"--builder", "my-builder", "image", "--gid", "1"}) 743 h.AssertNil(t, command.Execute()) 744 }) 745 }) 746 when("--gid is an invalid value", func() { 747 it("error must be thrown", func() { 748 command.SetArgs([]string{"--builder", "my-builder", "image", "--gid", "-1"}) 749 err := command.Execute() 750 h.AssertError(t, err, "gid flag must be in the range of 0-2147483647") 751 }) 752 }) 753 }) 754 755 when("gid flag is not provided", func() { 756 it("override build option should be set to false", func() { 757 mockClient.EXPECT(). 758 Build(gomock.Any(), EqBuildOptionsWithOverrideGroupID(-1)). 759 Return(nil) 760 761 command.SetArgs([]string{"--builder", "my-builder", "image"}) 762 h.AssertNil(t, command.Execute()) 763 }) 764 }) 765 766 when("previous-image flag is provided", func() { 767 when("image is invalid", func() { 768 it("error must be thrown", func() { 769 mockClient.EXPECT(). 770 Build(gomock.Any(), EqBuildOptionsWithPreviousImage("previous-image")). 771 Return(errors.New("")) 772 773 command.SetArgs([]string{"--builder", "my-builder", "/x@/y/?!z", "--previous-image", "previous-image"}) 774 err := command.Execute() 775 h.AssertError(t, err, "failed to build") 776 }) 777 }) 778 779 when("previous-image is invalid", func() { 780 it("error must be thrown", func() { 781 mockClient.EXPECT(). 782 Build(gomock.Any(), EqBuildOptionsWithPreviousImage("%%%")). 783 Return(errors.New("")) 784 785 command.SetArgs([]string{"--builder", "my-builder", "image", "--previous-image", "%%%"}) 786 err := command.Execute() 787 h.AssertError(t, err, "failed to build") 788 }) 789 }) 790 791 when("--publish is false", func() { 792 it("previous-image should be passed to builder", func() { 793 mockClient.EXPECT(). 794 Build(gomock.Any(), EqBuildOptionsWithPreviousImage("previous-image")). 795 Return(nil) 796 797 command.SetArgs([]string{"--builder", "my-builder", "image", "--previous-image", "previous-image"}) 798 h.AssertNil(t, command.Execute()) 799 }) 800 }) 801 802 when("--publish is true", func() { 803 when("image and previous-image are in same registry", func() { 804 it("previous-image should be passed to builder", func() { 805 mockClient.EXPECT(). 806 Build(gomock.Any(), EqBuildOptionsWithPreviousImage("index.docker.io/some/previous:latest")). 807 Return(nil) 808 809 command.SetArgs([]string{"--builder", "my-builder", "index.docker.io/some/image:latest", "--previous-image", "index.docker.io/some/previous:latest", "--publish"}) 810 h.AssertNil(t, command.Execute()) 811 }) 812 }) 813 }) 814 }) 815 816 when("interactive flag is provided but experimental isn't set in the config", func() { 817 it("errors with a descriptive message", func() { 818 command.SetArgs([]string{"image", "--interactive"}) 819 err := command.Execute() 820 h.AssertNotNil(t, err) 821 h.AssertError(t, err, "Interactive mode is currently experimental.") 822 }) 823 }) 824 825 when("sbom destination directory is provided", func() { 826 it("forwards the network onto the client", func() { 827 mockClient.EXPECT(). 828 Build(gomock.Any(), EqBuildOptionsWithSBOMOutputDir("some-output-dir")). 829 Return(nil) 830 831 command.SetArgs([]string{"image", "--builder", "my-builder", "--sbom-output-dir", "some-output-dir"}) 832 h.AssertNil(t, command.Execute()) 833 }) 834 }) 835 836 when("--creation-time", func() { 837 when("provided as 'now'", func() { 838 it("passes it to the builder", func() { 839 expectedTime := time.Now().UTC() 840 mockClient.EXPECT(). 841 Build(gomock.Any(), EqBuildOptionsWithDateTime(&expectedTime)). 842 Return(nil) 843 844 command.SetArgs([]string{"image", "--builder", "my-builder", "--creation-time", "now"}) 845 h.AssertNil(t, command.Execute()) 846 }) 847 }) 848 849 when("provided as unix timestamp", func() { 850 it("passes it to the builder", func() { 851 expectedTime, err := time.Parse("2006-01-02T03:04:05Z", "2019-08-19T00:00:01Z") 852 h.AssertNil(t, err) 853 mockClient.EXPECT(). 854 Build(gomock.Any(), EqBuildOptionsWithDateTime(&expectedTime)). 855 Return(nil) 856 857 command.SetArgs([]string{"image", "--builder", "my-builder", "--creation-time", "1566172801"}) 858 h.AssertNil(t, command.Execute()) 859 }) 860 }) 861 862 when("not provided", func() { 863 it("is nil", func() { 864 mockClient.EXPECT(). 865 Build(gomock.Any(), EqBuildOptionsWithDateTime(nil)). 866 Return(nil) 867 868 command.SetArgs([]string{"image", "--builder", "my-builder"}) 869 h.AssertNil(t, command.Execute()) 870 }) 871 }) 872 }) 873 874 when("export to OCI layout is expected but experimental isn't set in the config", func() { 875 it("errors with a descriptive message", func() { 876 command.SetArgs([]string{"oci:image", "--builder", "my-builder"}) 877 err := command.Execute() 878 h.AssertNotNil(t, err) 879 h.AssertError(t, err, "Exporting to OCI layout is currently experimental.") 880 }) 881 }) 882 }) 883 884 when("export to OCI layout is expected", func() { 885 var ( 886 sparse bool 887 previousImage string 888 layoutDir string 889 ) 890 891 it.Before(func() { 892 layoutDir = filepath.Join(paths.RootDir, "local", "repo") 893 previousImage = "" 894 cfg = config.Config{ 895 Experimental: true, 896 LayoutRepositoryDir: layoutDir, 897 } 898 command = commands.Build(logger, cfg, mockClient) 899 }) 900 901 when("path to save the image is provided", func() { 902 it("build is called with oci layout configuration", func() { 903 sparse = false 904 mockClient.EXPECT(). 905 Build(gomock.Any(), EqBuildOptionsWithLayoutConfig("image", previousImage, sparse, layoutDir)). 906 Return(nil) 907 908 command.SetArgs([]string{"oci:image", "--builder", "my-builder"}) 909 err := command.Execute() 910 h.AssertNil(t, err) 911 }) 912 }) 913 914 when("previous-image flag is provided", func() { 915 it("build is called with oci layout configuration", func() { 916 sparse = false 917 previousImage = "my-previous-image" 918 mockClient.EXPECT(). 919 Build(gomock.Any(), EqBuildOptionsWithLayoutConfig("image", previousImage, sparse, layoutDir)). 920 Return(nil) 921 922 command.SetArgs([]string{"oci:image", "--previous-image", "oci:my-previous-image", "--builder", "my-builder"}) 923 err := command.Execute() 924 h.AssertNil(t, err) 925 }) 926 }) 927 928 when("-sparse flag is provided", func() { 929 it("build is called with oci layout configuration and sparse true", func() { 930 sparse = true 931 mockClient.EXPECT(). 932 Build(gomock.Any(), EqBuildOptionsWithLayoutConfig("image", previousImage, sparse, layoutDir)). 933 Return(nil) 934 935 command.SetArgs([]string{"oci:image", "--sparse", "--builder", "my-builder"}) 936 err := command.Execute() 937 h.AssertNil(t, err) 938 }) 939 }) 940 }) 941 } 942 943 func EqBuildOptionsWithImage(builder, image string) gomock.Matcher { 944 return buildOptionsMatcher{ 945 description: fmt.Sprintf("Builder=%s and Image=%s", builder, image), 946 equals: func(o client.BuildOptions) bool { 947 return o.Builder == builder && o.Image == image 948 }, 949 } 950 } 951 952 func EqBuildOptionsDefaultProcess(defaultProc string) gomock.Matcher { 953 return buildOptionsMatcher{ 954 description: fmt.Sprintf("Default Process Type=%s", defaultProc), 955 equals: func(o client.BuildOptions) bool { 956 return o.DefaultProcessType == defaultProc 957 }, 958 } 959 } 960 961 func EqBuildOptionsWithPullPolicy(policy image.PullPolicy) gomock.Matcher { 962 return buildOptionsMatcher{ 963 description: fmt.Sprintf("PullPolicy=%s", policy), 964 equals: func(o client.BuildOptions) bool { 965 return o.PullPolicy == policy 966 }, 967 } 968 } 969 970 func EqBuildOptionsWithCacheImage(cacheImage string) gomock.Matcher { 971 return buildOptionsMatcher{ 972 description: fmt.Sprintf("CacheImage=%s", cacheImage), 973 equals: func(o client.BuildOptions) bool { 974 return o.CacheImage == cacheImage 975 }, 976 } 977 } 978 979 func EqBuildOptionsWithCacheFlags(cacheFlags string) gomock.Matcher { 980 return buildOptionsMatcher{ 981 description: fmt.Sprintf("CacheFlags=%s", cacheFlags), 982 equals: func(o client.BuildOptions) bool { 983 return o.Cache.String() == cacheFlags 984 }, 985 } 986 } 987 988 func EqBuildOptionsWithLifecycleImage(lifecycleImage string) gomock.Matcher { 989 return buildOptionsMatcher{ 990 description: fmt.Sprintf("LifecycleImage=%s", lifecycleImage), 991 equals: func(o client.BuildOptions) bool { 992 return o.LifecycleImage == lifecycleImage 993 }, 994 } 995 } 996 997 func EqBuildOptionsWithNetwork(network string) gomock.Matcher { 998 return buildOptionsMatcher{ 999 description: fmt.Sprintf("Network=%s", network), 1000 equals: func(o client.BuildOptions) bool { 1001 return o.ContainerConfig.Network == network 1002 }, 1003 } 1004 } 1005 1006 func EqBuildOptionsWithBuilder(builder string) gomock.Matcher { 1007 return buildOptionsMatcher{ 1008 description: fmt.Sprintf("Builder=%s", builder), 1009 equals: func(o client.BuildOptions) bool { 1010 return o.Builder == builder 1011 }, 1012 } 1013 } 1014 1015 func EqBuildOptionsWithTrustedBuilder(trustBuilder bool) gomock.Matcher { 1016 return buildOptionsMatcher{ 1017 description: fmt.Sprintf("Trust Builder=%t", trustBuilder), 1018 equals: func(o client.BuildOptions) bool { 1019 return o.TrustBuilder(o.Builder) 1020 }, 1021 } 1022 } 1023 1024 func EqBuildOptionsWithVolumes(volumes []string) gomock.Matcher { 1025 return buildOptionsMatcher{ 1026 description: fmt.Sprintf("Volumes=%s", volumes), 1027 equals: func(o client.BuildOptions) bool { 1028 return reflect.DeepEqual(o.ContainerConfig.Volumes, volumes) 1029 }, 1030 } 1031 } 1032 1033 func EqBuildOptionsWithAdditionalTags(additionalTags []string) gomock.Matcher { 1034 return buildOptionsMatcher{ 1035 description: fmt.Sprintf("AdditionalTags=%s", additionalTags), 1036 equals: func(o client.BuildOptions) bool { 1037 return reflect.DeepEqual(o.AdditionalTags, additionalTags) 1038 }, 1039 } 1040 } 1041 1042 func EqBuildOptionsWithProjectDescriptor(descriptor projectTypes.Descriptor) gomock.Matcher { 1043 return buildOptionsMatcher{ 1044 description: fmt.Sprintf("Descriptor=%s", descriptor), 1045 equals: func(o client.BuildOptions) bool { 1046 return reflect.DeepEqual(o.ProjectDescriptor, descriptor) 1047 }, 1048 } 1049 } 1050 1051 func EqBuildOptionsWithEnv(env map[string]string) gomock.Matcher { 1052 return buildOptionsMatcher{ 1053 description: fmt.Sprintf("Env=%+v", env), 1054 equals: func(o client.BuildOptions) bool { 1055 for k, v := range o.Env { 1056 if env[k] != v { 1057 return false 1058 } 1059 } 1060 for k, v := range env { 1061 if o.Env[k] != v { 1062 return false 1063 } 1064 } 1065 return true 1066 }, 1067 } 1068 } 1069 1070 func EqBuildOptionsWithOverrideGroupID(gid int) gomock.Matcher { 1071 return buildOptionsMatcher{ 1072 description: fmt.Sprintf("GID=%d", gid), 1073 equals: func(o client.BuildOptions) bool { 1074 return o.GroupID == gid 1075 }, 1076 } 1077 } 1078 1079 func EqBuildOptionsWithPreviousImage(prevImage string) gomock.Matcher { 1080 return buildOptionsMatcher{ 1081 description: fmt.Sprintf("Previous image=%s", prevImage), 1082 equals: func(o client.BuildOptions) bool { 1083 return o.PreviousImage == prevImage 1084 }, 1085 } 1086 } 1087 1088 func EqBuildOptionsWithSBOMOutputDir(s string) interface{} { 1089 return buildOptionsMatcher{ 1090 description: fmt.Sprintf("sbom-destination-dir=%s", s), 1091 equals: func(o client.BuildOptions) bool { 1092 return o.SBOMDestinationDir == s 1093 }, 1094 } 1095 } 1096 1097 func EqBuildOptionsWithDateTime(t *time.Time) interface{} { 1098 return buildOptionsMatcher{ 1099 description: fmt.Sprintf("CreationTime=%s", t), 1100 equals: func(o client.BuildOptions) bool { 1101 if t == nil { 1102 return o.CreationTime == nil 1103 } 1104 return o.CreationTime.Sub(*t) < 5*time.Second && t.Sub(*o.CreationTime) < 5*time.Second 1105 }, 1106 } 1107 } 1108 1109 func EqBuildOptionsWithLayoutConfig(image, previousImage string, sparse bool, layoutDir string) interface{} { 1110 return buildOptionsMatcher{ 1111 description: fmt.Sprintf("image=%s, previous-image=%s, sparse=%t, layout-dir=%s", image, previousImage, sparse, layoutDir), 1112 equals: func(o client.BuildOptions) bool { 1113 if o.Layout() { 1114 result := o.Image == image 1115 if previousImage != "" { 1116 result = result && previousImage == o.PreviousImage 1117 } 1118 return result && o.LayoutConfig.Sparse == sparse && o.LayoutConfig.LayoutRepoDir == layoutDir 1119 } 1120 return false 1121 }, 1122 } 1123 } 1124 1125 type buildOptionsMatcher struct { 1126 equals func(client.BuildOptions) bool 1127 description string 1128 } 1129 1130 func (m buildOptionsMatcher) Matches(x interface{}) bool { 1131 if b, ok := x.(client.BuildOptions); ok { 1132 return m.equals(b) 1133 } 1134 return false 1135 } 1136 1137 func (m buildOptionsMatcher) String() string { 1138 return "is a BuildOptions with " + m.description 1139 }