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  }