github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/pkg/client/create_builder_test.go (about)

     1  package client_test
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/buildpacks/imgutil/fakes"
    13  	"github.com/buildpacks/lifecycle/api"
    14  	"github.com/docker/docker/api/types/system"
    15  	"github.com/golang/mock/gomock"
    16  	"github.com/heroku/color"
    17  	"github.com/pkg/errors"
    18  	"github.com/sclevine/spec"
    19  	"github.com/sclevine/spec/report"
    20  
    21  	pubbldr "github.com/buildpacks/pack/builder"
    22  	pubbldpkg "github.com/buildpacks/pack/buildpackage"
    23  	"github.com/buildpacks/pack/internal/builder"
    24  	ifakes "github.com/buildpacks/pack/internal/fakes"
    25  	"github.com/buildpacks/pack/internal/paths"
    26  	"github.com/buildpacks/pack/internal/style"
    27  	"github.com/buildpacks/pack/pkg/archive"
    28  	"github.com/buildpacks/pack/pkg/blob"
    29  	"github.com/buildpacks/pack/pkg/buildpack"
    30  	"github.com/buildpacks/pack/pkg/client"
    31  	"github.com/buildpacks/pack/pkg/dist"
    32  	"github.com/buildpacks/pack/pkg/image"
    33  	"github.com/buildpacks/pack/pkg/logging"
    34  	"github.com/buildpacks/pack/pkg/testmocks"
    35  	h "github.com/buildpacks/pack/testhelpers"
    36  )
    37  
    38  func TestCreateBuilder(t *testing.T) {
    39  	color.Disable(true)
    40  	defer color.Disable(false)
    41  	spec.Run(t, "create_builder", testCreateBuilder, spec.Parallel(), spec.Report(report.Terminal{}))
    42  }
    43  
    44  func testCreateBuilder(t *testing.T, when spec.G, it spec.S) {
    45  	when("#CreateBuilder", func() {
    46  		var (
    47  			mockController          *gomock.Controller
    48  			mockDownloader          *testmocks.MockBlobDownloader
    49  			mockBuildpackDownloader *testmocks.MockBuildpackDownloader
    50  			mockImageFactory        *testmocks.MockImageFactory
    51  			mockImageFetcher        *testmocks.MockImageFetcher
    52  			mockDockerClient        *testmocks.MockCommonAPIClient
    53  			fakeBuildImage          *fakes.Image
    54  			fakeRunImage            *fakes.Image
    55  			fakeRunImageMirror      *fakes.Image
    56  			opts                    client.CreateBuilderOptions
    57  			subject                 *client.Client
    58  			logger                  logging.Logger
    59  			out                     bytes.Buffer
    60  			tmpDir                  string
    61  		)
    62  		var prepareFetcherWithRunImages = func() {
    63  			mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/run-image", gomock.Any()).Return(fakeRunImage, nil).AnyTimes()
    64  			mockImageFetcher.EXPECT().Fetch(gomock.Any(), "localhost:5000/some/run-image", gomock.Any()).Return(fakeRunImageMirror, nil).AnyTimes()
    65  		}
    66  
    67  		var prepareFetcherWithBuildImage = func() {
    68  			mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", gomock.Any()).Return(fakeBuildImage, nil)
    69  		}
    70  
    71  		var createBuildpack = func(descriptor dist.BuildpackDescriptor) buildpack.BuildModule {
    72  			buildpack, err := ifakes.NewFakeBuildpack(descriptor, 0644)
    73  			h.AssertNil(t, err)
    74  			return buildpack
    75  		}
    76  
    77  		var shouldCallBuildpackDownloaderWith = func(uri string, buildpackDownloadOptions buildpack.DownloadOptions) {
    78  			buildpack := createBuildpack(dist.BuildpackDescriptor{
    79  				WithAPI:    api.MustParse("0.3"),
    80  				WithInfo:   dist.ModuleInfo{ID: "example/foo", Version: "1.1.0"},
    81  				WithStacks: []dist.Stack{{ID: "some.stack.id"}},
    82  			})
    83  			mockBuildpackDownloader.EXPECT().Download(gomock.Any(), uri, gomock.Any()).Return(buildpack, nil, nil)
    84  		}
    85  
    86  		it.Before(func() {
    87  			logger = logging.NewLogWithWriters(&out, &out, logging.WithVerbose())
    88  			mockController = gomock.NewController(t)
    89  			mockDownloader = testmocks.NewMockBlobDownloader(mockController)
    90  			mockImageFetcher = testmocks.NewMockImageFetcher(mockController)
    91  			mockImageFactory = testmocks.NewMockImageFactory(mockController)
    92  			mockDockerClient = testmocks.NewMockCommonAPIClient(mockController)
    93  			mockBuildpackDownloader = testmocks.NewMockBuildpackDownloader(mockController)
    94  
    95  			fakeBuildImage = fakes.NewImage("some/build-image", "", nil)
    96  			h.AssertNil(t, fakeBuildImage.SetLabel("io.buildpacks.stack.id", "some.stack.id"))
    97  			h.AssertNil(t, fakeBuildImage.SetLabel("io.buildpacks.stack.mixins", `["mixinX", "build:mixinY"]`))
    98  			h.AssertNil(t, fakeBuildImage.SetEnv("CNB_USER_ID", "1234"))
    99  			h.AssertNil(t, fakeBuildImage.SetEnv("CNB_GROUP_ID", "4321"))
   100  
   101  			fakeRunImage = fakes.NewImage("some/run-image", "", nil)
   102  			h.AssertNil(t, fakeRunImage.SetLabel("io.buildpacks.stack.id", "some.stack.id"))
   103  
   104  			fakeRunImageMirror = fakes.NewImage("localhost:5000/some/run-image", "", nil)
   105  			h.AssertNil(t, fakeRunImageMirror.SetLabel("io.buildpacks.stack.id", "some.stack.id"))
   106  
   107  			exampleBuildpackBlob := blob.NewBlob(filepath.Join("testdata", "buildpack"))
   108  			mockDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/bp-one.tgz").Return(exampleBuildpackBlob, nil).AnyTimes()
   109  			exampleExtensionBlob := blob.NewBlob(filepath.Join("testdata", "extension"))
   110  			mockDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/ext-one.tgz").Return(exampleExtensionBlob, nil).AnyTimes()
   111  			mockDownloader.EXPECT().Download(gomock.Any(), "some/buildpack/dir").Return(blob.NewBlob(filepath.Join("testdata", "buildpack")), nil).AnyTimes()
   112  			mockDownloader.EXPECT().Download(gomock.Any(), "file:///some-lifecycle").Return(blob.NewBlob(filepath.Join("testdata", "lifecycle", "platform-0.4")), nil).AnyTimes()
   113  			mockDownloader.EXPECT().Download(gomock.Any(), "file:///some-lifecycle-platform-0-1").Return(blob.NewBlob(filepath.Join("testdata", "lifecycle-platform-0.1")), nil).AnyTimes()
   114  
   115  			bp, err := buildpack.FromBuildpackRootBlob(exampleBuildpackBlob, archive.DefaultTarWriterFactory(), nil)
   116  			h.AssertNil(t, err)
   117  			mockBuildpackDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/bp-one.tgz", gomock.Any()).Return(bp, nil, nil).AnyTimes()
   118  			ext, err := buildpack.FromExtensionRootBlob(exampleExtensionBlob, archive.DefaultTarWriterFactory(), nil)
   119  			h.AssertNil(t, err)
   120  			mockBuildpackDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/ext-one.tgz", gomock.Any()).Return(ext, nil, nil).AnyTimes()
   121  
   122  			subject, err = client.NewClient(
   123  				client.WithLogger(logger),
   124  				client.WithDownloader(mockDownloader),
   125  				client.WithImageFactory(mockImageFactory),
   126  				client.WithFetcher(mockImageFetcher),
   127  				client.WithDockerClient(mockDockerClient),
   128  				client.WithBuildpackDownloader(mockBuildpackDownloader),
   129  			)
   130  			h.AssertNil(t, err)
   131  
   132  			mockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: "linux"}, nil).AnyTimes()
   133  
   134  			opts = client.CreateBuilderOptions{
   135  				RelativeBaseDir: "/",
   136  				BuilderName:     "some/builder",
   137  				Config: pubbldr.Config{
   138  					Description: "Some description",
   139  					Buildpacks: []pubbldr.ModuleConfig{
   140  						{
   141  							ModuleInfo: dist.ModuleInfo{ID: "bp.one", Version: "1.2.3", Homepage: "http://one.buildpack"},
   142  							ImageOrURI: dist.ImageOrURI{
   143  								BuildpackURI: dist.BuildpackURI{
   144  									URI: "https://example.fake/bp-one.tgz",
   145  								},
   146  							},
   147  						},
   148  					},
   149  					Extensions: []pubbldr.ModuleConfig{
   150  						{
   151  							ModuleInfo: dist.ModuleInfo{ID: "ext.one", Version: "1.2.3", Homepage: "http://one.extension"},
   152  							ImageOrURI: dist.ImageOrURI{
   153  								BuildpackURI: dist.BuildpackURI{
   154  									URI: "https://example.fake/ext-one.tgz",
   155  								},
   156  							},
   157  						},
   158  					},
   159  					Order: []dist.OrderEntry{{
   160  						Group: []dist.ModuleRef{
   161  							{ModuleInfo: dist.ModuleInfo{ID: "bp.one", Version: "1.2.3"}, Optional: false},
   162  						}},
   163  					},
   164  					OrderExtensions: []dist.OrderEntry{{
   165  						Group: []dist.ModuleRef{
   166  							{ModuleInfo: dist.ModuleInfo{ID: "ext.one", Version: "1.2.3"}, Optional: true},
   167  						}},
   168  					},
   169  					Stack: pubbldr.StackConfig{
   170  						ID: "some.stack.id",
   171  					},
   172  					Run: pubbldr.RunConfig{
   173  						Images: []pubbldr.RunImageConfig{{
   174  							Image:   "some/run-image",
   175  							Mirrors: []string{"localhost:5000/some/run-image"},
   176  						}},
   177  					},
   178  					Build: pubbldr.BuildConfig{
   179  						Image: "some/build-image",
   180  					},
   181  					Lifecycle: pubbldr.LifecycleConfig{URI: "file:///some-lifecycle"},
   182  				},
   183  				Publish:    false,
   184  				PullPolicy: image.PullAlways,
   185  			}
   186  
   187  			tmpDir, err = os.MkdirTemp("", "create-builder-test")
   188  			h.AssertNil(t, err)
   189  		})
   190  
   191  		it.After(func() {
   192  			mockController.Finish()
   193  			h.AssertNil(t, os.RemoveAll(tmpDir))
   194  		})
   195  
   196  		var successfullyCreateBuilder = func() *builder.Builder {
   197  			t.Helper()
   198  
   199  			err := subject.CreateBuilder(context.TODO(), opts)
   200  			h.AssertNil(t, err)
   201  
   202  			h.AssertEq(t, fakeBuildImage.IsSaved(), true)
   203  			bldr, err := builder.FromImage(fakeBuildImage)
   204  			h.AssertNil(t, err)
   205  
   206  			return bldr
   207  		}
   208  
   209  		when("validating the builder config", func() {
   210  			it("should not fail when the stack ID is empty", func() {
   211  				opts.Config.Stack.ID = ""
   212  				prepareFetcherWithBuildImage()
   213  				prepareFetcherWithRunImages()
   214  
   215  				err := subject.CreateBuilder(context.TODO(), opts)
   216  
   217  				h.AssertNil(t, err)
   218  			})
   219  
   220  			it("should fail when the stack ID from the builder config does not match the stack ID from the build image", func() {
   221  				mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(fakeBuildImage, nil)
   222  				h.AssertNil(t, fakeBuildImage.SetLabel("io.buildpacks.stack.id", "other.stack.id"))
   223  				prepareFetcherWithRunImages()
   224  
   225  				err := subject.CreateBuilder(context.TODO(), opts)
   226  
   227  				h.AssertError(t, err, "stack 'some.stack.id' from builder config is incompatible with stack 'other.stack.id' from build image")
   228  			})
   229  
   230  			it("should not fail when the stack is empty", func() {
   231  				opts.Config.Stack.ID = ""
   232  				opts.Config.Stack.BuildImage = ""
   233  				opts.Config.Stack.RunImage = ""
   234  				prepareFetcherWithBuildImage()
   235  				prepareFetcherWithRunImages()
   236  
   237  				err := subject.CreateBuilder(context.TODO(), opts)
   238  
   239  				h.AssertNil(t, err)
   240  			})
   241  
   242  			it("should fail when the run images and stack are empty", func() {
   243  				opts.Config.Stack.BuildImage = ""
   244  				opts.Config.Stack.RunImage = ""
   245  
   246  				opts.Config.Run = pubbldr.RunConfig{}
   247  
   248  				err := subject.CreateBuilder(context.TODO(), opts)
   249  
   250  				h.AssertError(t, err, "run.images are required")
   251  			})
   252  
   253  			it("should fail when the run images image and stack are empty", func() {
   254  				opts.Config.Stack.BuildImage = ""
   255  				opts.Config.Stack.RunImage = ""
   256  
   257  				opts.Config.Run = pubbldr.RunConfig{
   258  					Images: []pubbldr.RunImageConfig{{}},
   259  				}
   260  
   261  				err := subject.CreateBuilder(context.TODO(), opts)
   262  
   263  				h.AssertError(t, err, "run.images.image is required")
   264  			})
   265  
   266  			it("should fail if stack and run image are different", func() {
   267  				opts.Config.Stack.RunImage = "some-other-stack-run-image"
   268  
   269  				err := subject.CreateBuilder(context.TODO(), opts)
   270  
   271  				h.AssertError(t, err, "run.images and stack.run-image do not match")
   272  			})
   273  
   274  			it("should fail if stack and build image are different", func() {
   275  				opts.Config.Stack.BuildImage = "some-other-stack-build-image"
   276  
   277  				err := subject.CreateBuilder(context.TODO(), opts)
   278  
   279  				h.AssertError(t, err, "build.image and stack.build-image do not match")
   280  			})
   281  
   282  			it("should fail when lifecycle version is not a semver", func() {
   283  				prepareFetcherWithBuildImage()
   284  				prepareFetcherWithRunImages()
   285  				opts.Config.Lifecycle.URI = ""
   286  				opts.Config.Lifecycle.Version = "not-semver"
   287  
   288  				err := subject.CreateBuilder(context.TODO(), opts)
   289  
   290  				h.AssertError(t, err, "'lifecycle.version' must be a valid semver")
   291  			})
   292  
   293  			it("should fail when both lifecycle version and uri are present", func() {
   294  				prepareFetcherWithBuildImage()
   295  				prepareFetcherWithRunImages()
   296  				opts.Config.Lifecycle.URI = "file://some-lifecycle"
   297  				opts.Config.Lifecycle.Version = "something"
   298  
   299  				err := subject.CreateBuilder(context.TODO(), opts)
   300  
   301  				h.AssertError(t, err, "'lifecycle' can only declare 'version' or 'uri', not both")
   302  			})
   303  
   304  			it("should fail when buildpack ID does not match downloaded buildpack", func() {
   305  				prepareFetcherWithBuildImage()
   306  				prepareFetcherWithRunImages()
   307  				opts.Config.Buildpacks[0].ID = "does.not.match"
   308  
   309  				err := subject.CreateBuilder(context.TODO(), opts)
   310  
   311  				h.AssertError(t, err, "buildpack from URI 'https://example.fake/bp-one.tgz' has ID 'bp.one' which does not match ID 'does.not.match' from builder config")
   312  			})
   313  
   314  			it("should fail when buildpack version does not match downloaded buildpack", func() {
   315  				prepareFetcherWithBuildImage()
   316  				prepareFetcherWithRunImages()
   317  				opts.Config.Buildpacks[0].Version = "0.0.0"
   318  
   319  				err := subject.CreateBuilder(context.TODO(), opts)
   320  
   321  				h.AssertError(t, err, "buildpack from URI 'https://example.fake/bp-one.tgz' has version '1.2.3' which does not match version '0.0.0' from builder config")
   322  			})
   323  
   324  			it("should fail when extension ID does not match downloaded extension", func() {
   325  				prepareFetcherWithBuildImage()
   326  				prepareFetcherWithRunImages()
   327  				opts.Config.Extensions[0].ID = "does.not.match"
   328  
   329  				err := subject.CreateBuilder(context.TODO(), opts)
   330  
   331  				h.AssertError(t, err, "extension from URI 'https://example.fake/ext-one.tgz' has ID 'ext.one' which does not match ID 'does.not.match' from builder config")
   332  			})
   333  
   334  			it("should fail when extension version does not match downloaded extension", func() {
   335  				prepareFetcherWithBuildImage()
   336  				prepareFetcherWithRunImages()
   337  				opts.Config.Extensions[0].Version = "0.0.0"
   338  
   339  				err := subject.CreateBuilder(context.TODO(), opts)
   340  
   341  				h.AssertError(t, err, "extension from URI 'https://example.fake/ext-one.tgz' has version '1.2.3' which does not match version '0.0.0' from builder config")
   342  			})
   343  		})
   344  
   345  		when("validating the run image config", func() {
   346  			it("should fail when the stack ID from the builder config does not match the stack ID from the run image", func() {
   347  				prepareFetcherWithRunImages()
   348  				h.AssertNil(t, fakeRunImage.SetLabel("io.buildpacks.stack.id", "other.stack.id"))
   349  
   350  				err := subject.CreateBuilder(context.TODO(), opts)
   351  
   352  				h.AssertError(t, err, "stack 'some.stack.id' from builder config is incompatible with stack 'other.stack.id' from run image 'some/run-image'")
   353  			})
   354  
   355  			it("should fail when the stack ID from the builder config does not match the stack ID from the run image mirrors", func() {
   356  				prepareFetcherWithRunImages()
   357  				h.AssertNil(t, fakeRunImageMirror.SetLabel("io.buildpacks.stack.id", "other.stack.id"))
   358  
   359  				err := subject.CreateBuilder(context.TODO(), opts)
   360  
   361  				h.AssertError(t, err, "stack 'some.stack.id' from builder config is incompatible with stack 'other.stack.id' from run image 'localhost:5000/some/run-image'")
   362  			})
   363  
   364  			it("should warn when the run image cannot be found", func() {
   365  				mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(fakeBuildImage, nil)
   366  
   367  				mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/run-image", image.FetchOptions{Daemon: false, PullPolicy: image.PullAlways}).Return(nil, errors.Wrap(image.ErrNotFound, "yikes"))
   368  				mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/run-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(nil, errors.Wrap(image.ErrNotFound, "yikes"))
   369  
   370  				mockImageFetcher.EXPECT().Fetch(gomock.Any(), "localhost:5000/some/run-image", image.FetchOptions{Daemon: false, PullPolicy: image.PullAlways}).Return(nil, errors.Wrap(image.ErrNotFound, "yikes"))
   371  				mockImageFetcher.EXPECT().Fetch(gomock.Any(), "localhost:5000/some/run-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(nil, errors.Wrap(image.ErrNotFound, "yikes"))
   372  
   373  				err := subject.CreateBuilder(context.TODO(), opts)
   374  				h.AssertNil(t, err)
   375  
   376  				h.AssertContains(t, out.String(), "Warning: run image 'some/run-image' is not accessible")
   377  			})
   378  
   379  			it("should fail when not publish and the run image cannot be fetched", func() {
   380  				mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/run-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(nil, errors.New("yikes"))
   381  
   382  				err := subject.CreateBuilder(context.TODO(), opts)
   383  				h.AssertError(t, err, "failed to fetch image: yikes")
   384  			})
   385  
   386  			it("should fail when publish and the run image cannot be fetched", func() {
   387  				mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/run-image", image.FetchOptions{Daemon: false, PullPolicy: image.PullAlways}).Return(nil, errors.New("yikes"))
   388  
   389  				opts.Publish = true
   390  				err := subject.CreateBuilder(context.TODO(), opts)
   391  				h.AssertError(t, err, "failed to fetch image: yikes")
   392  			})
   393  
   394  			it("should fail when the run image isn't a valid image", func() {
   395  				fakeImage := fakeBadImageStruct{}
   396  
   397  				mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/run-image", gomock.Any()).Return(fakeImage, nil).AnyTimes()
   398  				mockImageFetcher.EXPECT().Fetch(gomock.Any(), "localhost:5000/some/run-image", gomock.Any()).Return(fakeImage, nil).AnyTimes()
   399  
   400  				err := subject.CreateBuilder(context.TODO(), opts)
   401  				h.AssertError(t, err, "failed to label image")
   402  			})
   403  
   404  			when("publish is true", func() {
   405  				it("should only try to validate the remote run image", func() {
   406  					mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", image.FetchOptions{Daemon: true}).Times(0)
   407  					mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/run-image", image.FetchOptions{Daemon: true}).Times(0)
   408  					mockImageFetcher.EXPECT().Fetch(gomock.Any(), "localhost:5000/some/run-image", image.FetchOptions{Daemon: true}).Times(0)
   409  
   410  					mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", image.FetchOptions{Daemon: false}).Return(fakeBuildImage, nil)
   411  					mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/run-image", image.FetchOptions{Daemon: false}).Return(fakeRunImage, nil)
   412  					mockImageFetcher.EXPECT().Fetch(gomock.Any(), "localhost:5000/some/run-image", image.FetchOptions{Daemon: false}).Return(fakeRunImageMirror, nil)
   413  
   414  					opts.Publish = true
   415  
   416  					err := subject.CreateBuilder(context.TODO(), opts)
   417  					h.AssertNil(t, err)
   418  				})
   419  			})
   420  		})
   421  
   422  		when("creating the base builder", func() {
   423  			when("build image not found", func() {
   424  				it("should fail", func() {
   425  					prepareFetcherWithRunImages()
   426  					mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(nil, image.ErrNotFound)
   427  
   428  					err := subject.CreateBuilder(context.TODO(), opts)
   429  					h.AssertError(t, err, "fetch build image: not found")
   430  				})
   431  			})
   432  
   433  			when("build image isn't a valid image", func() {
   434  				it("should fail", func() {
   435  					fakeImage := fakeBadImageStruct{}
   436  
   437  					prepareFetcherWithRunImages()
   438  					mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(fakeImage, nil)
   439  
   440  					err := subject.CreateBuilder(context.TODO(), opts)
   441  					h.AssertError(t, err, "failed to create builder: invalid build-image")
   442  				})
   443  			})
   444  
   445  			when("windows containers", func() {
   446  				when("experimental enabled", func() {
   447  					it("succeeds", func() {
   448  						opts.Config.Extensions = nil      // TODO: downloading extensions doesn't work yet; to be implemented in https://github.com/buildpacks/pack/issues/1489
   449  						opts.Config.OrderExtensions = nil // TODO: downloading extensions doesn't work yet; to be implemented in https://github.com/buildpacks/pack/issues/1489
   450  						packClientWithExperimental, err := client.NewClient(
   451  							client.WithLogger(logger),
   452  							client.WithDownloader(mockDownloader),
   453  							client.WithImageFactory(mockImageFactory),
   454  							client.WithFetcher(mockImageFetcher),
   455  							client.WithExperimental(true),
   456  						)
   457  						h.AssertNil(t, err)
   458  
   459  						prepareFetcherWithRunImages()
   460  
   461  						h.AssertNil(t, fakeBuildImage.SetOS("windows"))
   462  						mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(fakeBuildImage, nil)
   463  
   464  						err = packClientWithExperimental.CreateBuilder(context.TODO(), opts)
   465  						h.AssertNil(t, err)
   466  					})
   467  				})
   468  
   469  				when("experimental disabled", func() {
   470  					it("fails", func() {
   471  						prepareFetcherWithRunImages()
   472  
   473  						h.AssertNil(t, fakeBuildImage.SetOS("windows"))
   474  						mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(fakeBuildImage, nil)
   475  
   476  						err := subject.CreateBuilder(context.TODO(), opts)
   477  						h.AssertError(t, err, "failed to create builder: Windows containers support is currently experimental.")
   478  					})
   479  				})
   480  			})
   481  
   482  			when("error downloading lifecycle", func() {
   483  				it("should fail", func() {
   484  					prepareFetcherWithBuildImage()
   485  					prepareFetcherWithRunImages()
   486  					opts.Config.Lifecycle.URI = "fake"
   487  
   488  					uri, err := paths.FilePathToURI(opts.Config.Lifecycle.URI, opts.RelativeBaseDir)
   489  					h.AssertNil(t, err)
   490  
   491  					mockDownloader.EXPECT().Download(gomock.Any(), uri).Return(nil, errors.New("error here")).AnyTimes()
   492  
   493  					err = subject.CreateBuilder(context.TODO(), opts)
   494  					h.AssertError(t, err, "downloading lifecycle")
   495  				})
   496  			})
   497  
   498  			when("lifecycle isn't a valid lifecycle", func() {
   499  				it("should fail", func() {
   500  					prepareFetcherWithBuildImage()
   501  					prepareFetcherWithRunImages()
   502  					opts.Config.Lifecycle.URI = "fake"
   503  
   504  					uri, err := paths.FilePathToURI(opts.Config.Lifecycle.URI, opts.RelativeBaseDir)
   505  					h.AssertNil(t, err)
   506  
   507  					mockDownloader.EXPECT().Download(gomock.Any(), uri).Return(blob.NewBlob(filepath.Join("testdata", "empty-file")), nil).AnyTimes()
   508  
   509  					err = subject.CreateBuilder(context.TODO(), opts)
   510  					h.AssertError(t, err, "invalid lifecycle")
   511  				})
   512  			})
   513  		})
   514  
   515  		when("only lifecycle version is provided", func() {
   516  			it("should download from predetermined uri", func() {
   517  				prepareFetcherWithBuildImage()
   518  				prepareFetcherWithRunImages()
   519  				opts.Config.Lifecycle.URI = ""
   520  				opts.Config.Lifecycle.Version = "3.4.5"
   521  
   522  				mockDownloader.EXPECT().Download(
   523  					gomock.Any(),
   524  					"https://github.com/buildpacks/lifecycle/releases/download/v3.4.5/lifecycle-v3.4.5+linux.x86-64.tgz",
   525  				).Return(
   526  					blob.NewBlob(filepath.Join("testdata", "lifecycle", "platform-0.4")), nil,
   527  				)
   528  
   529  				err := subject.CreateBuilder(context.TODO(), opts)
   530  				h.AssertNil(t, err)
   531  			})
   532  
   533  			it("should download from predetermined uri for arm64", func() {
   534  				prepareFetcherWithBuildImage()
   535  				prepareFetcherWithRunImages()
   536  				opts.Config.Lifecycle.URI = ""
   537  				opts.Config.Lifecycle.Version = "3.4.5"
   538  				h.AssertNil(t, fakeBuildImage.SetArchitecture("arm64"))
   539  
   540  				mockDownloader.EXPECT().Download(
   541  					gomock.Any(),
   542  					"https://github.com/buildpacks/lifecycle/releases/download/v3.4.5/lifecycle-v3.4.5+linux.arm64.tgz",
   543  				).Return(
   544  					blob.NewBlob(filepath.Join("testdata", "lifecycle", "platform-0.4")), nil,
   545  				)
   546  
   547  				err := subject.CreateBuilder(context.TODO(), opts)
   548  				h.AssertNil(t, err)
   549  			})
   550  
   551  			when("windows", func() {
   552  				it("should download from predetermined uri", func() {
   553  					opts.Config.Extensions = nil      // TODO: downloading extensions doesn't work yet; to be implemented in https://github.com/buildpacks/pack/issues/1489
   554  					opts.Config.OrderExtensions = nil // TODO: downloading extensions doesn't work yet; to be implemented in https://github.com/buildpacks/pack/issues/1489
   555  					packClientWithExperimental, err := client.NewClient(
   556  						client.WithLogger(logger),
   557  						client.WithDownloader(mockDownloader),
   558  						client.WithImageFactory(mockImageFactory),
   559  						client.WithFetcher(mockImageFetcher),
   560  						client.WithExperimental(true),
   561  					)
   562  					h.AssertNil(t, err)
   563  
   564  					prepareFetcherWithBuildImage()
   565  					prepareFetcherWithRunImages()
   566  					opts.Config.Lifecycle.URI = ""
   567  					opts.Config.Lifecycle.Version = "3.4.5"
   568  					h.AssertNil(t, fakeBuildImage.SetOS("windows"))
   569  
   570  					mockDownloader.EXPECT().Download(
   571  						gomock.Any(),
   572  						"https://github.com/buildpacks/lifecycle/releases/download/v3.4.5/lifecycle-v3.4.5+windows.x86-64.tgz",
   573  					).Return(
   574  						blob.NewBlob(filepath.Join("testdata", "lifecycle", "platform-0.4")), nil,
   575  					)
   576  
   577  					err = packClientWithExperimental.CreateBuilder(context.TODO(), opts)
   578  					h.AssertNil(t, err)
   579  				})
   580  			})
   581  		})
   582  
   583  		when("no lifecycle version or URI is provided", func() {
   584  			it("should download default lifecycle", func() {
   585  				prepareFetcherWithBuildImage()
   586  				prepareFetcherWithRunImages()
   587  				opts.Config.Lifecycle.URI = ""
   588  				opts.Config.Lifecycle.Version = ""
   589  
   590  				mockDownloader.EXPECT().Download(
   591  					gomock.Any(),
   592  					fmt.Sprintf(
   593  						"https://github.com/buildpacks/lifecycle/releases/download/v%s/lifecycle-v%s+linux.x86-64.tgz",
   594  						builder.DefaultLifecycleVersion,
   595  						builder.DefaultLifecycleVersion,
   596  					),
   597  				).Return(
   598  					blob.NewBlob(filepath.Join("testdata", "lifecycle", "platform-0.4")), nil,
   599  				)
   600  
   601  				err := subject.CreateBuilder(context.TODO(), opts)
   602  				h.AssertNil(t, err)
   603  			})
   604  
   605  			it("should download default lifecycle on arm64", func() {
   606  				prepareFetcherWithBuildImage()
   607  				prepareFetcherWithRunImages()
   608  				opts.Config.Lifecycle.URI = ""
   609  				opts.Config.Lifecycle.Version = ""
   610  				h.AssertNil(t, fakeBuildImage.SetArchitecture("arm64"))
   611  
   612  				mockDownloader.EXPECT().Download(
   613  					gomock.Any(),
   614  					fmt.Sprintf(
   615  						"https://github.com/buildpacks/lifecycle/releases/download/v%s/lifecycle-v%s+linux.arm64.tgz",
   616  						builder.DefaultLifecycleVersion,
   617  						builder.DefaultLifecycleVersion,
   618  					),
   619  				).Return(
   620  					blob.NewBlob(filepath.Join("testdata", "lifecycle", "platform-0.4")), nil,
   621  				)
   622  
   623  				err := subject.CreateBuilder(context.TODO(), opts)
   624  				h.AssertNil(t, err)
   625  			})
   626  
   627  			when("windows", func() {
   628  				it("should download default lifecycle", func() {
   629  					opts.Config.Extensions = nil      // TODO: downloading extensions doesn't work yet; to be implemented in https://github.com/buildpacks/pack/issues/1489
   630  					opts.Config.OrderExtensions = nil // TODO: downloading extensions doesn't work yet; to be implemented in https://github.com/buildpacks/pack/issues/1489
   631  					packClientWithExperimental, err := client.NewClient(
   632  						client.WithLogger(logger),
   633  						client.WithDownloader(mockDownloader),
   634  						client.WithImageFactory(mockImageFactory),
   635  						client.WithFetcher(mockImageFetcher),
   636  						client.WithExperimental(true),
   637  					)
   638  					h.AssertNil(t, err)
   639  
   640  					prepareFetcherWithBuildImage()
   641  					prepareFetcherWithRunImages()
   642  					opts.Config.Lifecycle.URI = ""
   643  					opts.Config.Lifecycle.Version = ""
   644  					h.AssertNil(t, fakeBuildImage.SetOS("windows"))
   645  
   646  					mockDownloader.EXPECT().Download(
   647  						gomock.Any(),
   648  						fmt.Sprintf(
   649  							"https://github.com/buildpacks/lifecycle/releases/download/v%s/lifecycle-v%s+windows.x86-64.tgz",
   650  							builder.DefaultLifecycleVersion,
   651  							builder.DefaultLifecycleVersion,
   652  						),
   653  					).Return(
   654  						blob.NewBlob(filepath.Join("testdata", "lifecycle", "platform-0.4")), nil,
   655  					)
   656  
   657  					err = packClientWithExperimental.CreateBuilder(context.TODO(), opts)
   658  					h.AssertNil(t, err)
   659  				})
   660  			})
   661  		})
   662  
   663  		when("buildpack mixins are not satisfied", func() {
   664  			it("should return an error", func() {
   665  				prepareFetcherWithBuildImage()
   666  				prepareFetcherWithRunImages()
   667  				h.AssertNil(t, fakeBuildImage.SetLabel("io.buildpacks.stack.mixins", ""))
   668  
   669  				err := subject.CreateBuilder(context.TODO(), opts)
   670  
   671  				h.AssertError(t, err, "validating buildpacks: buildpack 'bp.one@1.2.3' requires missing mixin(s): build:mixinY, mixinX")
   672  			})
   673  		})
   674  
   675  		when("creation succeeds", func() {
   676  			it("should set basic metadata", func() {
   677  				prepareFetcherWithBuildImage()
   678  				prepareFetcherWithRunImages()
   679  
   680  				bldr := successfullyCreateBuilder()
   681  
   682  				h.AssertEq(t, bldr.Name(), "some/builder")
   683  				h.AssertEq(t, bldr.Description(), "Some description")
   684  				h.AssertEq(t, bldr.UID(), 1234)
   685  				h.AssertEq(t, bldr.GID(), 4321)
   686  				h.AssertEq(t, bldr.StackID, "some.stack.id")
   687  			})
   688  
   689  			it("should set buildpack and order metadata", func() {
   690  				prepareFetcherWithBuildImage()
   691  				prepareFetcherWithRunImages()
   692  
   693  				bldr := successfullyCreateBuilder()
   694  
   695  				bpInfo := dist.ModuleInfo{
   696  					ID:       "bp.one",
   697  					Version:  "1.2.3",
   698  					Homepage: "http://one.buildpack",
   699  				}
   700  				h.AssertEq(t, bldr.Buildpacks(), []dist.ModuleInfo{bpInfo})
   701  				bpInfo.Homepage = ""
   702  				h.AssertEq(t, bldr.Order(), dist.Order{{
   703  					Group: []dist.ModuleRef{{
   704  						ModuleInfo: bpInfo,
   705  						Optional:   false,
   706  					}},
   707  				}})
   708  			})
   709  
   710  			it("should set extensions and order-extensions metadata", func() {
   711  				prepareFetcherWithBuildImage()
   712  				prepareFetcherWithRunImages()
   713  
   714  				bldr := successfullyCreateBuilder()
   715  
   716  				extInfo := dist.ModuleInfo{
   717  					ID:       "ext.one",
   718  					Version:  "1.2.3",
   719  					Homepage: "http://one.extension",
   720  				}
   721  				h.AssertEq(t, bldr.Extensions(), []dist.ModuleInfo{extInfo})
   722  				extInfo.Homepage = ""
   723  				h.AssertEq(t, bldr.OrderExtensions(), dist.Order{{
   724  					Group: []dist.ModuleRef{{
   725  						ModuleInfo: extInfo,
   726  						Optional:   false, // extensions are always optional
   727  					}},
   728  				}})
   729  			})
   730  
   731  			it("should embed the lifecycle", func() {
   732  				prepareFetcherWithBuildImage()
   733  				prepareFetcherWithRunImages()
   734  				successfullyCreateBuilder()
   735  
   736  				layerTar, err := fakeBuildImage.FindLayerWithPath("/cnb/lifecycle")
   737  				h.AssertNil(t, err)
   738  				h.AssertTarHasFile(t, layerTar, "/cnb/lifecycle/detector")
   739  				h.AssertTarHasFile(t, layerTar, "/cnb/lifecycle/restorer")
   740  				h.AssertTarHasFile(t, layerTar, "/cnb/lifecycle/analyzer")
   741  				h.AssertTarHasFile(t, layerTar, "/cnb/lifecycle/builder")
   742  				h.AssertTarHasFile(t, layerTar, "/cnb/lifecycle/exporter")
   743  				h.AssertTarHasFile(t, layerTar, "/cnb/lifecycle/launcher")
   744  			})
   745  
   746  			it("should set lifecycle descriptor", func() {
   747  				prepareFetcherWithBuildImage()
   748  				prepareFetcherWithRunImages()
   749  				bldr := successfullyCreateBuilder()
   750  
   751  				h.AssertEq(t, bldr.LifecycleDescriptor().Info.Version.String(), "0.0.0")
   752  				//nolint:staticcheck
   753  				h.AssertEq(t, bldr.LifecycleDescriptor().API.BuildpackVersion.String(), "0.2")
   754  				//nolint:staticcheck
   755  				h.AssertEq(t, bldr.LifecycleDescriptor().API.PlatformVersion.String(), "0.2")
   756  				h.AssertEq(t, bldr.LifecycleDescriptor().APIs.Buildpack.Deprecated.AsStrings(), []string{"0.2", "0.3"})
   757  				h.AssertEq(t, bldr.LifecycleDescriptor().APIs.Buildpack.Supported.AsStrings(), []string{"0.2", "0.3", "0.4", "0.9"})
   758  				h.AssertEq(t, bldr.LifecycleDescriptor().APIs.Platform.Deprecated.AsStrings(), []string{"0.2"})
   759  				h.AssertEq(t, bldr.LifecycleDescriptor().APIs.Platform.Supported.AsStrings(), []string{"0.3", "0.4"})
   760  			})
   761  
   762  			it("should warn when deprecated Buildpack API version is used", func() {
   763  				prepareFetcherWithBuildImage()
   764  				prepareFetcherWithRunImages()
   765  				bldr := successfullyCreateBuilder()
   766  
   767  				h.AssertEq(t, bldr.LifecycleDescriptor().APIs.Buildpack.Deprecated.AsStrings(), []string{"0.2", "0.3"})
   768  				h.AssertContains(t, out.String(), fmt.Sprintf("Buildpack %s is using deprecated Buildpacks API version %s", style.Symbol("bp.one@1.2.3"), style.Symbol("0.3")))
   769  				h.AssertContains(t, out.String(), fmt.Sprintf("Extension %s is using deprecated Buildpacks API version %s", style.Symbol("ext.one@1.2.3"), style.Symbol("0.3")))
   770  			})
   771  
   772  			it("shouldn't warn when Buildpack API version used isn't deprecated", func() {
   773  				prepareFetcherWithBuildImage()
   774  				prepareFetcherWithRunImages()
   775  				opts.Config.Buildpacks[0].URI = "https://example.fake/bp-one-with-api-4.tgz"
   776  				opts.Config.Extensions[0].URI = "https://example.fake/ext-one-with-api-9.tgz"
   777  
   778  				buildpackBlob := blob.NewBlob(filepath.Join("testdata", "buildpack-api-0.4"))
   779  				bp, err := buildpack.FromBuildpackRootBlob(buildpackBlob, archive.DefaultTarWriterFactory(), nil)
   780  				h.AssertNil(t, err)
   781  				mockBuildpackDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/bp-one-with-api-4.tgz", gomock.Any()).Return(bp, nil, nil)
   782  
   783  				extensionBlob := blob.NewBlob(filepath.Join("testdata", "extension-api-0.9"))
   784  				extension, err := buildpack.FromExtensionRootBlob(extensionBlob, archive.DefaultTarWriterFactory(), nil)
   785  				h.AssertNil(t, err)
   786  				mockBuildpackDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/ext-one-with-api-9.tgz", gomock.Any()).Return(extension, nil, nil)
   787  
   788  				bldr := successfullyCreateBuilder()
   789  
   790  				h.AssertEq(t, bldr.LifecycleDescriptor().APIs.Buildpack.Deprecated.AsStrings(), []string{"0.2", "0.3"})
   791  				h.AssertNotContains(t, out.String(), "is using deprecated Buildpacks API version")
   792  			})
   793  
   794  			it("should set labels", func() {
   795  				opts.Labels = map[string]string{"test.label.one": "1", "test.label.two": "2"}
   796  				prepareFetcherWithBuildImage()
   797  				prepareFetcherWithRunImages()
   798  
   799  				err := subject.CreateBuilder(context.TODO(), opts)
   800  				h.AssertNil(t, err)
   801  
   802  				imageLabels, err := fakeBuildImage.Labels()
   803  				h.AssertNil(t, err)
   804  				h.AssertEq(t, imageLabels["test.label.one"], "1")
   805  				h.AssertEq(t, imageLabels["test.label.two"], "2")
   806  			})
   807  
   808  			when("Buildpack dependencies are provided", func() {
   809  				var (
   810  					bp1v1          buildpack.BuildModule
   811  					bp1v2          buildpack.BuildModule
   812  					bp2v1          buildpack.BuildModule
   813  					bp2v2          buildpack.BuildModule
   814  					fakeLayerImage *h.FakeAddedLayerImage
   815  					err            error
   816  				)
   817  				it.Before(func() {
   818  					fakeLayerImage = &h.FakeAddedLayerImage{Image: fakeBuildImage}
   819  				})
   820  
   821  				var prepareBuildpackDependencies = func() []buildpack.BuildModule {
   822  					bp1v1Blob := blob.NewBlob(filepath.Join("testdata", "buildpack-non-deterministic", "buildpack-1-version-1"))
   823  					bp1v2Blob := blob.NewBlob(filepath.Join("testdata", "buildpack-non-deterministic", "buildpack-1-version-2"))
   824  					bp2v1Blob := blob.NewBlob(filepath.Join("testdata", "buildpack-non-deterministic", "buildpack-2-version-1"))
   825  					bp2v2Blob := blob.NewBlob(filepath.Join("testdata", "buildpack-non-deterministic", "buildpack-2-version-2"))
   826  
   827  					bp1v1, err = buildpack.FromBuildpackRootBlob(bp1v1Blob, archive.DefaultTarWriterFactory(), nil)
   828  					h.AssertNil(t, err)
   829  
   830  					bp1v2, err = buildpack.FromBuildpackRootBlob(bp1v2Blob, archive.DefaultTarWriterFactory(), nil)
   831  					h.AssertNil(t, err)
   832  
   833  					bp2v1, err = buildpack.FromBuildpackRootBlob(bp2v1Blob, archive.DefaultTarWriterFactory(), nil)
   834  					h.AssertNil(t, err)
   835  
   836  					bp2v2, err = buildpack.FromBuildpackRootBlob(bp2v2Blob, archive.DefaultTarWriterFactory(), nil)
   837  					h.AssertNil(t, err)
   838  
   839  					return []buildpack.BuildModule{bp2v2, bp2v1, bp1v1, bp1v2}
   840  				}
   841  
   842  				var successfullyCreateDeterministicBuilder = func() {
   843  					t.Helper()
   844  
   845  					err := subject.CreateBuilder(context.TODO(), opts)
   846  					h.AssertNil(t, err)
   847  					h.AssertEq(t, fakeLayerImage.IsSaved(), true)
   848  				}
   849  
   850  				it("should add dependencies buildpacks layers order by ID and version", func() {
   851  					mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", gomock.Any()).Return(fakeLayerImage, nil)
   852  					prepareFetcherWithRunImages()
   853  					opts.Config.Buildpacks[0].URI = "https://example.fake/bp-one-with-api-4.tgz"
   854  					opts.Config.Extensions[0].URI = "https://example.fake/ext-one-with-api-9.tgz"
   855  					bpDependencies := prepareBuildpackDependencies()
   856  
   857  					buildpackBlob := blob.NewBlob(filepath.Join("testdata", "buildpack-api-0.4"))
   858  					bp, err := buildpack.FromBuildpackRootBlob(buildpackBlob, archive.DefaultTarWriterFactory(), nil)
   859  					h.AssertNil(t, err)
   860  					mockBuildpackDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/bp-one-with-api-4.tgz", gomock.Any()).DoAndReturn(
   861  						func(ctx context.Context, buildpackURI string, opts buildpack.DownloadOptions) (buildpack.BuildModule, []buildpack.BuildModule, error) {
   862  							// test options
   863  							h.AssertEq(t, opts.Platform, "linux/amd64")
   864  							return bp, bpDependencies, nil
   865  						})
   866  
   867  					extensionBlob := blob.NewBlob(filepath.Join("testdata", "extension-api-0.9"))
   868  					extension, err := buildpack.FromExtensionRootBlob(extensionBlob, archive.DefaultTarWriterFactory(), nil)
   869  					h.AssertNil(t, err)
   870  					mockBuildpackDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/ext-one-with-api-9.tgz", gomock.Any()).DoAndReturn(
   871  						func(ctx context.Context, buildpackURI string, opts buildpack.DownloadOptions) (buildpack.BuildModule, []buildpack.BuildModule, error) {
   872  							// test options
   873  							h.AssertEq(t, opts.Platform, "linux/amd64")
   874  							return extension, nil, nil
   875  						})
   876  
   877  					successfullyCreateDeterministicBuilder()
   878  
   879  					layers := fakeLayerImage.AddedLayersOrder()
   880  					// Main buildpack + 4 dependencies + 1 extension
   881  					h.AssertEq(t, len(layers), 6)
   882  
   883  					// [0] bp.one.1.2.3.tar - main buildpack
   884  					h.AssertTrue(t, strings.Contains(layers[1], h.LayerFileName(bp1v1)))
   885  					h.AssertTrue(t, strings.Contains(layers[2], h.LayerFileName(bp1v2)))
   886  					h.AssertTrue(t, strings.Contains(layers[3], h.LayerFileName(bp2v1)))
   887  					h.AssertTrue(t, strings.Contains(layers[4], h.LayerFileName(bp2v2)))
   888  					// [5] ext.one.1.2.3.tar - extension
   889  				})
   890  			})
   891  		})
   892  
   893  		it("supports directory buildpacks", func() {
   894  			prepareFetcherWithBuildImage()
   895  			prepareFetcherWithRunImages()
   896  			opts.RelativeBaseDir = ""
   897  			directoryPath := "testdata/buildpack"
   898  			opts.Config.Buildpacks[0].URI = directoryPath
   899  
   900  			buildpackBlob := blob.NewBlob(directoryPath)
   901  			buildpack, err := buildpack.FromBuildpackRootBlob(buildpackBlob, archive.DefaultTarWriterFactory(), nil)
   902  			h.AssertNil(t, err)
   903  			mockBuildpackDownloader.EXPECT().Download(gomock.Any(), directoryPath, gomock.Any()).Return(buildpack, nil, nil)
   904  
   905  			err = subject.CreateBuilder(context.TODO(), opts)
   906  			h.AssertNil(t, err)
   907  		})
   908  
   909  		it("supports directory extensions", func() {
   910  			prepareFetcherWithBuildImage()
   911  			prepareFetcherWithRunImages()
   912  			opts.RelativeBaseDir = ""
   913  			directoryPath := "testdata/extension"
   914  			opts.Config.Extensions[0].URI = directoryPath
   915  
   916  			extensionBlob := blob.NewBlob(directoryPath)
   917  			extension, err := buildpack.FromExtensionRootBlob(extensionBlob, archive.DefaultTarWriterFactory(), nil)
   918  			h.AssertNil(t, err)
   919  			mockBuildpackDownloader.EXPECT().Download(gomock.Any(), directoryPath, gomock.Any()).Return(extension, nil, nil)
   920  
   921  			err = subject.CreateBuilder(context.TODO(), opts)
   922  			h.AssertNil(t, err)
   923  		})
   924  
   925  		when("package file", func() {
   926  			it.Before(func() {
   927  				fileURI := func(path string) (original, uri string) {
   928  					absPath, err := paths.FilePathToURI(path, "")
   929  					h.AssertNil(t, err)
   930  					return path, absPath
   931  				}
   932  
   933  				cnbFile, _ := fileURI(filepath.Join(tmpDir, "bp_one1.cnb"))
   934  				buildpackPath, buildpackPathURI := fileURI(filepath.Join("testdata", "buildpack"))
   935  				mockDownloader.EXPECT().Download(gomock.Any(), buildpackPathURI).Return(blob.NewBlob(buildpackPath), nil)
   936  
   937  				h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   938  					Name: cnbFile,
   939  					Config: pubbldpkg.Config{
   940  						Platform:  dist.Platform{OS: "linux"},
   941  						Buildpack: dist.BuildpackURI{URI: buildpackPath},
   942  					},
   943  					Format: "file",
   944  				}))
   945  
   946  				buildpack, _, err := buildpack.BuildpacksFromOCILayoutBlob(blob.NewBlob(cnbFile))
   947  				h.AssertNil(t, err)
   948  				mockBuildpackDownloader.EXPECT().Download(gomock.Any(), cnbFile, gomock.Any()).Return(buildpack, nil, nil).AnyTimes()
   949  				opts.Config.Buildpacks = []pubbldr.ModuleConfig{{
   950  					ImageOrURI: dist.ImageOrURI{BuildpackURI: dist.BuildpackURI{URI: cnbFile}},
   951  				}}
   952  			})
   953  
   954  			it("package file is valid", func() {
   955  				prepareFetcherWithBuildImage()
   956  				prepareFetcherWithRunImages()
   957  				bldr := successfullyCreateBuilder()
   958  
   959  				bpInfo := dist.ModuleInfo{
   960  					ID:       "bp.one",
   961  					Version:  "1.2.3",
   962  					Homepage: "http://one.buildpack",
   963  				}
   964  				h.AssertEq(t, bldr.Buildpacks(), []dist.ModuleInfo{bpInfo})
   965  				bpInfo.Homepage = ""
   966  				h.AssertEq(t, bldr.Order(), dist.Order{{
   967  					Group: []dist.ModuleRef{{
   968  						ModuleInfo: bpInfo,
   969  						Optional:   false,
   970  					}},
   971  				}})
   972  			})
   973  		})
   974  
   975  		when("packages", func() {
   976  			when("package image lives in cnb registry", func() {
   977  				when("publish=false and pull-policy=always", func() {
   978  					it("should call BuildpackDownloader with the proper argumentss", func() {
   979  						prepareFetcherWithBuildImage()
   980  						prepareFetcherWithRunImages()
   981  						opts.BuilderName = "some/builder"
   982  						opts.Publish = false
   983  						opts.PullPolicy = image.PullAlways
   984  						opts.Registry = "some-registry"
   985  						opts.Config.Buildpacks = append(
   986  							opts.Config.Buildpacks,
   987  							pubbldr.ModuleConfig{
   988  								ImageOrURI: dist.ImageOrURI{
   989  									BuildpackURI: dist.BuildpackURI{
   990  										URI: "urn:cnb:registry:example/foo@1.1.0",
   991  									},
   992  								},
   993  							},
   994  						)
   995  
   996  						shouldCallBuildpackDownloaderWith("urn:cnb:registry:example/foo@1.1.0", buildpack.DownloadOptions{Daemon: true, PullPolicy: image.PullAlways, RegistryName: "some-"})
   997  						h.AssertNil(t, subject.CreateBuilder(context.TODO(), opts))
   998  					})
   999  				})
  1000  			})
  1001  		})
  1002  
  1003  		when("flatten option is set", func() {
  1004  			/*       1
  1005  			 *    /    \
  1006  			 *   2      3
  1007  			 *         /  \
  1008  			 *        4     5
  1009  			 *	          /  \
  1010  			 *           6   7
  1011  			 */
  1012  			var (
  1013  				fakeLayerImage *h.FakeAddedLayerImage
  1014  				err            error
  1015  			)
  1016  
  1017  			var successfullyCreateFlattenBuilder = func() {
  1018  				t.Helper()
  1019  
  1020  				err := subject.CreateBuilder(context.TODO(), opts)
  1021  				h.AssertNil(t, err)
  1022  				h.AssertEq(t, fakeLayerImage.IsSaved(), true)
  1023  			}
  1024  
  1025  			it.Before(func() {
  1026  				fakeLayerImage = &h.FakeAddedLayerImage{Image: fakeBuildImage}
  1027  				mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", gomock.Any()).Return(fakeLayerImage, nil)
  1028  
  1029  				var depBPs []buildpack.BuildModule
  1030  				blob1 := blob.NewBlob(filepath.Join("testdata", "buildpack-flatten", "buildpack-1"))
  1031  				for i := 2; i <= 7; i++ {
  1032  					b := blob.NewBlob(filepath.Join("testdata", "buildpack-flatten", fmt.Sprintf("buildpack-%d", i)))
  1033  					bp, err := buildpack.FromBuildpackRootBlob(b, archive.DefaultTarWriterFactory(), nil)
  1034  					h.AssertNil(t, err)
  1035  					depBPs = append(depBPs, bp)
  1036  				}
  1037  				mockDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/flatten-bp-1.tgz").Return(blob1, nil).AnyTimes()
  1038  
  1039  				bp, err := buildpack.FromBuildpackRootBlob(blob1, archive.DefaultTarWriterFactory(), nil)
  1040  				h.AssertNil(t, err)
  1041  				mockBuildpackDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/flatten-bp-1.tgz", gomock.Any()).Return(bp, depBPs, nil).AnyTimes()
  1042  
  1043  				opts = client.CreateBuilderOptions{
  1044  					RelativeBaseDir: "/",
  1045  					BuilderName:     "some/builder",
  1046  					Config: pubbldr.Config{
  1047  						Description: "Some description",
  1048  						Buildpacks: []pubbldr.ModuleConfig{
  1049  							{
  1050  								ModuleInfo: dist.ModuleInfo{ID: "flatten/bp-1", Version: "1", Homepage: "http://buildpack-1"},
  1051  								ImageOrURI: dist.ImageOrURI{
  1052  									BuildpackURI: dist.BuildpackURI{
  1053  										URI: "https://example.fake/flatten-bp-1.tgz",
  1054  									},
  1055  								},
  1056  							},
  1057  						},
  1058  						Order: []dist.OrderEntry{{
  1059  							Group: []dist.ModuleRef{
  1060  								{ModuleInfo: dist.ModuleInfo{ID: "flatten/bp-2", Version: "2"}, Optional: false},
  1061  								{ModuleInfo: dist.ModuleInfo{ID: "flatten/bp-4", Version: "4"}, Optional: false},
  1062  								{ModuleInfo: dist.ModuleInfo{ID: "flatten/bp-6", Version: "6"}, Optional: false},
  1063  								{ModuleInfo: dist.ModuleInfo{ID: "flatten/bp-7", Version: "7"}, Optional: false},
  1064  							}},
  1065  						},
  1066  						Stack: pubbldr.StackConfig{
  1067  							ID: "some.stack.id",
  1068  						},
  1069  						Run: pubbldr.RunConfig{
  1070  							Images: []pubbldr.RunImageConfig{{
  1071  								Image:   "some/run-image",
  1072  								Mirrors: []string{"localhost:5000/some/run-image"},
  1073  							}},
  1074  						},
  1075  						Build: pubbldr.BuildConfig{
  1076  							Image: "some/build-image",
  1077  						},
  1078  						Lifecycle: pubbldr.LifecycleConfig{URI: "file:///some-lifecycle"},
  1079  					},
  1080  					Publish:    false,
  1081  					PullPolicy: image.PullAlways,
  1082  				}
  1083  			})
  1084  
  1085  			when("flatten all", func() {
  1086  				it("creates 1 layer for all buildpacks", func() {
  1087  					prepareFetcherWithRunImages()
  1088  					opts.Flatten, err = buildpack.ParseFlattenBuildModules([]string{"flatten/bp-1@1,flatten/bp-2@2,flatten/bp-4@4,flatten/bp-6@6,flatten/bp-7@7,flatten/bp-3@3,flatten/bp-5@5"})
  1089  					h.AssertNil(t, err)
  1090  
  1091  					successfullyCreateFlattenBuilder()
  1092  
  1093  					layers := fakeLayerImage.AddedLayersOrder()
  1094  
  1095  					h.AssertEq(t, len(layers), 1)
  1096  				})
  1097  			})
  1098  
  1099  			when("only some modules are flattened", func() {
  1100  				it("creates 1 layer for buildpacks [1,2,3,4,5,6] and 1 layer for buildpack [7]", func() {
  1101  					prepareFetcherWithRunImages()
  1102  					opts.Flatten, err = buildpack.ParseFlattenBuildModules([]string{"flatten/bp-1@1,flatten/bp-2@2,flatten/bp-4@4,flatten/bp-6@6,flatten/bp-3@3,flatten/bp-5@5"})
  1103  					h.AssertNil(t, err)
  1104  
  1105  					successfullyCreateFlattenBuilder()
  1106  
  1107  					layers := fakeLayerImage.AddedLayersOrder()
  1108  					h.AssertEq(t, len(layers), 2)
  1109  				})
  1110  
  1111  				it("creates 1 layer for buildpacks [1,2,3] and 1 layer for [4,5,6] and 1 layer for [7]", func() {
  1112  					prepareFetcherWithRunImages()
  1113  					opts.Flatten, err = buildpack.ParseFlattenBuildModules([]string{"flatten/bp-1@1,flatten/bp-2@2,flatten/bp-3@3", "flatten/bp-4@4,flatten/bp-6@6,flatten/bp-5@5"})
  1114  					h.AssertNil(t, err)
  1115  
  1116  					successfullyCreateFlattenBuilder()
  1117  
  1118  					layers := fakeLayerImage.AddedLayersOrder()
  1119  					h.AssertEq(t, len(layers), 3)
  1120  				})
  1121  			})
  1122  		})
  1123  	})
  1124  }
  1125  
  1126  type fakeBadImageStruct struct {
  1127  	*fakes.Image
  1128  }
  1129  
  1130  func (i fakeBadImageStruct) Name() string {
  1131  	return "fake image"
  1132  }
  1133  
  1134  func (i fakeBadImageStruct) Label(str string) (string, error) {
  1135  	return "", errors.New("error here")
  1136  }