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

     1  package client_test
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"testing"
    10  
    11  	"github.com/buildpacks/imgutil"
    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/sclevine/spec"
    18  	"github.com/sclevine/spec/report"
    19  
    20  	"github.com/buildpacks/pack/pkg/archive"
    21  
    22  	pubbldpkg "github.com/buildpacks/pack/buildpackage"
    23  	cfg "github.com/buildpacks/pack/internal/config"
    24  	ifakes "github.com/buildpacks/pack/internal/fakes"
    25  	"github.com/buildpacks/pack/internal/paths"
    26  	"github.com/buildpacks/pack/pkg/blob"
    27  	"github.com/buildpacks/pack/pkg/buildpack"
    28  	"github.com/buildpacks/pack/pkg/client"
    29  	"github.com/buildpacks/pack/pkg/dist"
    30  	"github.com/buildpacks/pack/pkg/image"
    31  	"github.com/buildpacks/pack/pkg/logging"
    32  	"github.com/buildpacks/pack/pkg/testmocks"
    33  	h "github.com/buildpacks/pack/testhelpers"
    34  )
    35  
    36  func TestPackageBuildpack(t *testing.T) {
    37  	color.Disable(true)
    38  	defer color.Disable(false)
    39  	spec.Run(t, "PackageBuildpack", testPackageBuildpack, spec.Parallel(), spec.Report(report.Terminal{}))
    40  }
    41  
    42  func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) {
    43  	var (
    44  		subject          *client.Client
    45  		mockController   *gomock.Controller
    46  		mockDownloader   *testmocks.MockBlobDownloader
    47  		mockImageFactory *testmocks.MockImageFactory
    48  		mockImageFetcher *testmocks.MockImageFetcher
    49  		mockDockerClient *testmocks.MockCommonAPIClient
    50  		out              bytes.Buffer
    51  	)
    52  
    53  	it.Before(func() {
    54  		mockController = gomock.NewController(t)
    55  		mockDownloader = testmocks.NewMockBlobDownloader(mockController)
    56  		mockImageFactory = testmocks.NewMockImageFactory(mockController)
    57  		mockImageFetcher = testmocks.NewMockImageFetcher(mockController)
    58  		mockDockerClient = testmocks.NewMockCommonAPIClient(mockController)
    59  
    60  		var err error
    61  		subject, err = client.NewClient(
    62  			client.WithLogger(logging.NewLogWithWriters(&out, &out)),
    63  			client.WithDownloader(mockDownloader),
    64  			client.WithImageFactory(mockImageFactory),
    65  			client.WithFetcher(mockImageFetcher),
    66  			client.WithDockerClient(mockDockerClient),
    67  		)
    68  		h.AssertNil(t, err)
    69  	})
    70  
    71  	it.After(func() {
    72  		mockController.Finish()
    73  	})
    74  
    75  	createBuildpack := func(descriptor dist.BuildpackDescriptor) string {
    76  		bp, err := ifakes.NewFakeBuildpackBlob(&descriptor, 0644)
    77  		h.AssertNil(t, err)
    78  		url := fmt.Sprintf("https://example.com/bp.%s.tgz", h.RandString(12))
    79  		mockDownloader.EXPECT().Download(gomock.Any(), url).Return(bp, nil).AnyTimes()
    80  		return url
    81  	}
    82  
    83  	when("buildpack has issues", func() {
    84  		when("buildpack has no URI", func() {
    85  			it("should fail", func() {
    86  				err := subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
    87  					Name: "Fake-Name",
    88  					Config: pubbldpkg.Config{
    89  						Platform:  dist.Platform{OS: "linux"},
    90  						Buildpack: dist.BuildpackURI{URI: ""},
    91  					},
    92  					Publish: true,
    93  				})
    94  				h.AssertError(t, err, "buildpack URI must be provided")
    95  			})
    96  		})
    97  
    98  		when("can't download buildpack", func() {
    99  			it("should fail", func() {
   100  				bpURL := fmt.Sprintf("https://example.com/bp.%s.tgz", h.RandString(12))
   101  				mockDownloader.EXPECT().Download(gomock.Any(), bpURL).Return(nil, image.ErrNotFound).AnyTimes()
   102  
   103  				err := subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   104  					Name: "Fake-Name",
   105  					Config: pubbldpkg.Config{
   106  						Platform:  dist.Platform{OS: "linux"},
   107  						Buildpack: dist.BuildpackURI{URI: bpURL},
   108  					},
   109  					Publish: true,
   110  				})
   111  				h.AssertError(t, err, "downloading buildpack")
   112  			})
   113  		})
   114  
   115  		when("buildpack isn't a valid buildpack", func() {
   116  			it("should fail", func() {
   117  				fakeBlob := blob.NewBlob(filepath.Join("testdata", "empty-file"))
   118  				bpURL := fmt.Sprintf("https://example.com/bp.%s.tgz", h.RandString(12))
   119  				mockDownloader.EXPECT().Download(gomock.Any(), bpURL).Return(fakeBlob, nil).AnyTimes()
   120  
   121  				err := subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   122  					Name: "Fake-Name",
   123  					Config: pubbldpkg.Config{
   124  						Platform:  dist.Platform{OS: "linux"},
   125  						Buildpack: dist.BuildpackURI{URI: bpURL},
   126  					},
   127  					Publish: true,
   128  				})
   129  				h.AssertError(t, err, "creating buildpack")
   130  			})
   131  		})
   132  	})
   133  
   134  	when("dependencies have issues", func() {
   135  		when("dependencies include a flawed packaged buildpack file", func() {
   136  			it("should fail", func() {
   137  				dependencyPath := "http://example.com/flawed.file"
   138  				mockDownloader.EXPECT().Download(gomock.Any(), dependencyPath).Return(blob.NewBlob("no-file.txt"), nil).AnyTimes()
   139  
   140  				mockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: "linux"}, nil).AnyTimes()
   141  
   142  				packageDescriptor := dist.BuildpackDescriptor{
   143  					WithAPI:  api.MustParse("0.2"),
   144  					WithInfo: dist.ModuleInfo{ID: "bp.1", Version: "1.2.3"},
   145  					WithOrder: dist.Order{{
   146  						Group: []dist.ModuleRef{{
   147  							ModuleInfo: dist.ModuleInfo{ID: "bp.nested", Version: "2.3.4"},
   148  							Optional:   false,
   149  						}},
   150  					}},
   151  				}
   152  
   153  				err := subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   154  					Name: "test",
   155  					Config: pubbldpkg.Config{
   156  						Platform:     dist.Platform{OS: "linux"},
   157  						Buildpack:    dist.BuildpackURI{URI: createBuildpack(packageDescriptor)},
   158  						Dependencies: []dist.ImageOrURI{{BuildpackURI: dist.BuildpackURI{URI: dependencyPath}}},
   159  					},
   160  					Publish:    false,
   161  					PullPolicy: image.PullAlways,
   162  				})
   163  
   164  				h.AssertError(t, err, "inspecting buildpack blob")
   165  			})
   166  		})
   167  	})
   168  
   169  	when("FormatImage", func() {
   170  		when("simple package for both OS formats (experimental only)", func() {
   171  			it("creates package image based on daemon OS", func() {
   172  				for _, daemonOS := range []string{"linux", "windows"} {
   173  					localMockDockerClient := testmocks.NewMockCommonAPIClient(mockController)
   174  					localMockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: daemonOS}, nil).AnyTimes()
   175  
   176  					packClientWithExperimental, err := client.NewClient(
   177  						client.WithDockerClient(localMockDockerClient),
   178  						client.WithDownloader(mockDownloader),
   179  						client.WithImageFactory(mockImageFactory),
   180  						client.WithExperimental(true),
   181  					)
   182  					h.AssertNil(t, err)
   183  
   184  					fakeImage := fakes.NewImage("basic/package-"+h.RandString(12), "", nil)
   185  					mockImageFactory.EXPECT().NewImage(fakeImage.Name(), true, daemonOS).Return(fakeImage, nil)
   186  
   187  					fakeBlob := blob.NewBlob(filepath.Join("testdata", "empty-file"))
   188  					bpURL := fmt.Sprintf("https://example.com/bp.%s.tgz", h.RandString(12))
   189  					mockDownloader.EXPECT().Download(gomock.Any(), bpURL).Return(fakeBlob, nil).AnyTimes()
   190  
   191  					h.AssertNil(t, packClientWithExperimental.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   192  						Format: client.FormatImage,
   193  						Name:   fakeImage.Name(),
   194  						Config: pubbldpkg.Config{
   195  							Platform: dist.Platform{OS: daemonOS},
   196  							Buildpack: dist.BuildpackURI{URI: createBuildpack(dist.BuildpackDescriptor{
   197  								WithAPI:    api.MustParse("0.2"),
   198  								WithInfo:   dist.ModuleInfo{ID: "bp.basic", Version: "2.3.4"},
   199  								WithStacks: []dist.Stack{{ID: "some.stack.id"}},
   200  							})},
   201  						},
   202  						PullPolicy: image.PullNever,
   203  					}))
   204  				}
   205  			})
   206  
   207  			it("fails without experimental on Windows daemons", func() {
   208  				windowsMockDockerClient := testmocks.NewMockCommonAPIClient(mockController)
   209  
   210  				packClientWithoutExperimental, err := client.NewClient(
   211  					client.WithDockerClient(windowsMockDockerClient),
   212  					client.WithExperimental(false),
   213  				)
   214  				h.AssertNil(t, err)
   215  
   216  				err = packClientWithoutExperimental.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   217  					Config: pubbldpkg.Config{
   218  						Platform: dist.Platform{
   219  							OS: "windows",
   220  						},
   221  					},
   222  				})
   223  				h.AssertError(t, err, "Windows buildpackage support is currently experimental.")
   224  			})
   225  
   226  			it("fails for mismatched platform and daemon os", func() {
   227  				windowsMockDockerClient := testmocks.NewMockCommonAPIClient(mockController)
   228  				windowsMockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: "windows"}, nil).AnyTimes()
   229  
   230  				packClientWithoutExperimental, err := client.NewClient(
   231  					client.WithDockerClient(windowsMockDockerClient),
   232  					client.WithExperimental(false),
   233  				)
   234  				h.AssertNil(t, err)
   235  
   236  				err = packClientWithoutExperimental.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   237  					Config: pubbldpkg.Config{
   238  						Platform: dist.Platform{
   239  							OS: "linux",
   240  						},
   241  					},
   242  				})
   243  
   244  				h.AssertError(t, err, "invalid 'platform.os' specified: DOCKER_OS is 'windows'")
   245  			})
   246  		})
   247  
   248  		when("nested package lives in registry", func() {
   249  			var nestedPackage *fakes.Image
   250  
   251  			it.Before(func() {
   252  				nestedPackage = fakes.NewImage("nested/package-"+h.RandString(12), "", nil)
   253  				mockImageFactory.EXPECT().NewImage(nestedPackage.Name(), false, "linux").Return(nestedPackage, nil)
   254  
   255  				mockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: "linux"}, nil).AnyTimes()
   256  
   257  				h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   258  					Name: nestedPackage.Name(),
   259  					Config: pubbldpkg.Config{
   260  						Platform: dist.Platform{OS: "linux"},
   261  						Buildpack: dist.BuildpackURI{URI: createBuildpack(dist.BuildpackDescriptor{
   262  							WithAPI:    api.MustParse("0.2"),
   263  							WithInfo:   dist.ModuleInfo{ID: "bp.nested", Version: "2.3.4"},
   264  							WithStacks: []dist.Stack{{ID: "some.stack.id"}},
   265  						})},
   266  					},
   267  					Publish:    true,
   268  					PullPolicy: image.PullAlways,
   269  				}))
   270  			})
   271  
   272  			shouldFetchNestedPackage := func(demon bool, pull image.PullPolicy) {
   273  				mockImageFetcher.EXPECT().Fetch(gomock.Any(), nestedPackage.Name(), image.FetchOptions{Daemon: demon, PullPolicy: pull}).Return(nestedPackage, nil)
   274  			}
   275  
   276  			shouldNotFindNestedPackageWhenCallingImageFetcherWith := func(demon bool, pull image.PullPolicy) {
   277  				mockImageFetcher.EXPECT().Fetch(gomock.Any(), nestedPackage.Name(), image.FetchOptions{Daemon: demon, PullPolicy: pull}).Return(nil, image.ErrNotFound)
   278  			}
   279  
   280  			shouldCreateLocalPackage := func() imgutil.Image {
   281  				img := fakes.NewImage("some/package-"+h.RandString(12), "", nil)
   282  				mockImageFactory.EXPECT().NewImage(img.Name(), true, "linux").Return(img, nil)
   283  				return img
   284  			}
   285  
   286  			shouldCreateRemotePackage := func() *fakes.Image {
   287  				img := fakes.NewImage("some/package-"+h.RandString(12), "", nil)
   288  				mockImageFactory.EXPECT().NewImage(img.Name(), false, "linux").Return(img, nil)
   289  				return img
   290  			}
   291  
   292  			when("publish=false and pull-policy=always", func() {
   293  				it("should pull and use local nested package image", func() {
   294  					shouldFetchNestedPackage(true, image.PullAlways)
   295  					packageImage := shouldCreateLocalPackage()
   296  
   297  					h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   298  						Name: packageImage.Name(),
   299  						Config: pubbldpkg.Config{
   300  							Platform: dist.Platform{OS: "linux"},
   301  							Buildpack: dist.BuildpackURI{URI: createBuildpack(dist.BuildpackDescriptor{
   302  								WithAPI:  api.MustParse("0.2"),
   303  								WithInfo: dist.ModuleInfo{ID: "bp.1", Version: "1.2.3"},
   304  								WithOrder: dist.Order{{
   305  									Group: []dist.ModuleRef{{
   306  										ModuleInfo: dist.ModuleInfo{ID: "bp.nested", Version: "2.3.4"},
   307  										Optional:   false,
   308  									}},
   309  								}},
   310  							})},
   311  							Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: nestedPackage.Name()}}},
   312  						},
   313  						Publish:    false,
   314  						PullPolicy: image.PullAlways,
   315  					}))
   316  				})
   317  			})
   318  
   319  			when("publish=true and pull-policy=always", func() {
   320  				it("should use remote nested package image", func() {
   321  					shouldFetchNestedPackage(false, image.PullAlways)
   322  					packageImage := shouldCreateRemotePackage()
   323  
   324  					h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   325  						Name: packageImage.Name(),
   326  						Config: pubbldpkg.Config{
   327  							Platform: dist.Platform{OS: "linux"},
   328  							Buildpack: dist.BuildpackURI{URI: createBuildpack(dist.BuildpackDescriptor{
   329  								WithAPI:  api.MustParse("0.2"),
   330  								WithInfo: dist.ModuleInfo{ID: "bp.1", Version: "1.2.3"},
   331  								WithOrder: dist.Order{{
   332  									Group: []dist.ModuleRef{{
   333  										ModuleInfo: dist.ModuleInfo{ID: "bp.nested", Version: "2.3.4"},
   334  										Optional:   false,
   335  									}},
   336  								}},
   337  							})},
   338  							Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: nestedPackage.Name()}}},
   339  						},
   340  						Publish:    true,
   341  						PullPolicy: image.PullAlways,
   342  					}))
   343  				})
   344  			})
   345  
   346  			when("publish=true and pull-policy=never", func() {
   347  				it("should push to registry and not pull nested package image", func() {
   348  					shouldFetchNestedPackage(false, image.PullNever)
   349  					packageImage := shouldCreateRemotePackage()
   350  
   351  					h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   352  						Name: packageImage.Name(),
   353  						Config: pubbldpkg.Config{
   354  							Platform: dist.Platform{OS: "linux"},
   355  							Buildpack: dist.BuildpackURI{URI: createBuildpack(dist.BuildpackDescriptor{
   356  								WithAPI:  api.MustParse("0.2"),
   357  								WithInfo: dist.ModuleInfo{ID: "bp.1", Version: "1.2.3"},
   358  								WithOrder: dist.Order{{
   359  									Group: []dist.ModuleRef{{
   360  										ModuleInfo: dist.ModuleInfo{ID: "bp.nested", Version: "2.3.4"},
   361  										Optional:   false,
   362  									}},
   363  								}},
   364  							})},
   365  							Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: nestedPackage.Name()}}},
   366  						},
   367  						Publish:    true,
   368  						PullPolicy: image.PullNever,
   369  					}))
   370  				})
   371  			})
   372  
   373  			when("publish=false pull-policy=never and there is no local image", func() {
   374  				it("should fail without trying to retrieve nested image from registry", func() {
   375  					shouldNotFindNestedPackageWhenCallingImageFetcherWith(true, image.PullNever)
   376  
   377  					h.AssertError(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   378  						Name: "some/package",
   379  						Config: pubbldpkg.Config{
   380  							Platform: dist.Platform{OS: "linux"},
   381  							Buildpack: dist.BuildpackURI{URI: createBuildpack(dist.BuildpackDescriptor{
   382  								WithAPI:    api.MustParse("0.2"),
   383  								WithInfo:   dist.ModuleInfo{ID: "bp.1", Version: "1.2.3"},
   384  								WithStacks: []dist.Stack{{ID: "some.stack.id"}},
   385  							})},
   386  							Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: nestedPackage.Name()}}},
   387  						},
   388  						Publish:    false,
   389  						PullPolicy: image.PullNever,
   390  					}), "not found")
   391  				})
   392  			})
   393  		})
   394  
   395  		when("nested package is not a valid package", func() {
   396  			it("should error", func() {
   397  				notPackageImage := fakes.NewImage("not/package", "", nil)
   398  				mockImageFetcher.EXPECT().Fetch(gomock.Any(), notPackageImage.Name(), image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(notPackageImage, nil)
   399  
   400  				mockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: "linux"}, nil).AnyTimes()
   401  
   402  				h.AssertError(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   403  					Name: "some/package",
   404  					Config: pubbldpkg.Config{
   405  						Platform: dist.Platform{OS: "linux"},
   406  						Buildpack: dist.BuildpackURI{URI: createBuildpack(dist.BuildpackDescriptor{
   407  							WithAPI:    api.MustParse("0.2"),
   408  							WithInfo:   dist.ModuleInfo{ID: "bp.1", Version: "1.2.3"},
   409  							WithStacks: []dist.Stack{{ID: "some.stack.id"}},
   410  						})},
   411  						Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: notPackageImage.Name()}}},
   412  					},
   413  					Publish:    false,
   414  					PullPolicy: image.PullAlways,
   415  				}), "extracting buildpacks from 'not/package': could not find label 'io.buildpacks.buildpackage.metadata'")
   416  			})
   417  		})
   418  
   419  		when("flatten option is set", func() {
   420  			/*       1
   421  			 *    /    \
   422  			 *   2      3
   423  			 *         /  \
   424  			 *        4     5
   425  			 *	          /  \
   426  			 *           6   7
   427  			 */
   428  			var (
   429  				fakeLayerImage          *h.FakeAddedLayerImage
   430  				opts                    client.PackageBuildpackOptions
   431  				mockBuildpackDownloader *testmocks.MockBuildpackDownloader
   432  			)
   433  
   434  			var successfullyCreateFlattenPackage = func() {
   435  				t.Helper()
   436  				err := subject.PackageBuildpack(context.TODO(), opts)
   437  				h.AssertNil(t, err)
   438  				h.AssertEq(t, fakeLayerImage.IsSaved(), true)
   439  			}
   440  
   441  			it.Before(func() {
   442  				mockBuildpackDownloader = testmocks.NewMockBuildpackDownloader(mockController)
   443  
   444  				var err error
   445  				subject, err = client.NewClient(
   446  					client.WithLogger(logging.NewLogWithWriters(&out, &out)),
   447  					client.WithDownloader(mockDownloader),
   448  					client.WithImageFactory(mockImageFactory),
   449  					client.WithFetcher(mockImageFetcher),
   450  					client.WithDockerClient(mockDockerClient),
   451  					client.WithBuildpackDownloader(mockBuildpackDownloader),
   452  				)
   453  				h.AssertNil(t, err)
   454  
   455  				mockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: "linux"}, nil).AnyTimes()
   456  
   457  				name := "basic/package-" + h.RandString(12)
   458  				fakeImage := fakes.NewImage(name, "", nil)
   459  				fakeLayerImage = &h.FakeAddedLayerImage{Image: fakeImage}
   460  				mockImageFactory.EXPECT().NewImage(fakeLayerImage.Name(), true, "linux").Return(fakeLayerImage, nil)
   461  				mockImageFetcher.EXPECT().Fetch(gomock.Any(), name, gomock.Any()).Return(fakeLayerImage, nil).AnyTimes()
   462  
   463  				blob1 := blob.NewBlob(filepath.Join("testdata", "buildpack-flatten", "buildpack-1"))
   464  				mockDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/flatten-bp-1.tgz").Return(blob1, nil).AnyTimes()
   465  				bp, err := buildpack.FromBuildpackRootBlob(blob1, archive.DefaultTarWriterFactory(), nil)
   466  				h.AssertNil(t, err)
   467  				mockBuildpackDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/flatten-bp-1.tgz", gomock.Any()).Return(bp, nil, nil).AnyTimes()
   468  
   469  				// flatten buildpack 2
   470  				blob2 := blob.NewBlob(filepath.Join("testdata", "buildpack-flatten", "buildpack-2"))
   471  				bp2, err := buildpack.FromBuildpackRootBlob(blob2, archive.DefaultTarWriterFactory(), nil)
   472  				h.AssertNil(t, err)
   473  				mockBuildpackDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/flatten-bp-2.tgz", gomock.Any()).Return(bp2, nil, nil).AnyTimes()
   474  
   475  				// flatten buildpack 3
   476  				blob3 := blob.NewBlob(filepath.Join("testdata", "buildpack-flatten", "buildpack-3"))
   477  				bp3, err := buildpack.FromBuildpackRootBlob(blob3, archive.DefaultTarWriterFactory(), nil)
   478  				h.AssertNil(t, err)
   479  
   480  				var depBPs []buildpack.BuildModule
   481  				for i := 4; i <= 7; i++ {
   482  					b := blob.NewBlob(filepath.Join("testdata", "buildpack-flatten", fmt.Sprintf("buildpack-%d", i)))
   483  					bp, err := buildpack.FromBuildpackRootBlob(b, archive.DefaultTarWriterFactory(), nil)
   484  					h.AssertNil(t, err)
   485  					depBPs = append(depBPs, bp)
   486  				}
   487  				mockBuildpackDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/flatten-bp-3.tgz", gomock.Any()).Return(bp3, depBPs, nil).AnyTimes()
   488  
   489  				opts = client.PackageBuildpackOptions{
   490  					Format: client.FormatImage,
   491  					Name:   fakeLayerImage.Name(),
   492  					Config: pubbldpkg.Config{
   493  						Platform:  dist.Platform{OS: "linux"},
   494  						Buildpack: dist.BuildpackURI{URI: "https://example.fake/flatten-bp-1.tgz"},
   495  						Dependencies: []dist.ImageOrURI{
   496  							{BuildpackURI: dist.BuildpackURI{URI: "https://example.fake/flatten-bp-2.tgz"}},
   497  							{BuildpackURI: dist.BuildpackURI{URI: "https://example.fake/flatten-bp-3.tgz"}},
   498  						},
   499  					},
   500  					PullPolicy: image.PullNever,
   501  					Flatten:    true,
   502  				}
   503  			})
   504  
   505  			when("flatten all", func() {
   506  				it("creates package image with all dependencies", func() {
   507  					successfullyCreateFlattenPackage()
   508  
   509  					layers := fakeLayerImage.AddedLayersOrder()
   510  					h.AssertEq(t, len(layers), 1)
   511  				})
   512  
   513  				// TODO add test case for flatten all with --flatten-exclude
   514  			})
   515  		})
   516  	})
   517  
   518  	when("FormatFile", func() {
   519  		when("simple package for both OS formats (experimental only)", func() {
   520  			it("creates package image in either OS format", func() {
   521  				tmpDir, err := os.MkdirTemp("", "package-buildpack")
   522  				h.AssertNil(t, err)
   523  				defer os.Remove(tmpDir)
   524  
   525  				for _, imageOS := range []string{"linux", "windows"} {
   526  					localMockDockerClient := testmocks.NewMockCommonAPIClient(mockController)
   527  					localMockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: imageOS}, nil).AnyTimes()
   528  
   529  					packClientWithExperimental, err := client.NewClient(
   530  						client.WithDockerClient(localMockDockerClient),
   531  						client.WithLogger(logging.NewLogWithWriters(&out, &out)),
   532  						client.WithDownloader(mockDownloader),
   533  						client.WithExperimental(true),
   534  					)
   535  					h.AssertNil(t, err)
   536  
   537  					fakeBlob := blob.NewBlob(filepath.Join("testdata", "empty-file"))
   538  					bpURL := fmt.Sprintf("https://example.com/bp.%s.tgz", h.RandString(12))
   539  					mockDownloader.EXPECT().Download(gomock.Any(), bpURL).Return(fakeBlob, nil).AnyTimes()
   540  
   541  					packagePath := filepath.Join(tmpDir, h.RandString(12)+"-test.cnb")
   542  					h.AssertNil(t, packClientWithExperimental.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   543  						Format: client.FormatFile,
   544  						Name:   packagePath,
   545  						Config: pubbldpkg.Config{
   546  							Platform: dist.Platform{OS: imageOS},
   547  							Buildpack: dist.BuildpackURI{URI: createBuildpack(dist.BuildpackDescriptor{
   548  								WithAPI:    api.MustParse("0.2"),
   549  								WithInfo:   dist.ModuleInfo{ID: "bp.basic", Version: "2.3.4"},
   550  								WithStacks: []dist.Stack{{ID: "some.stack.id"}},
   551  							})},
   552  						},
   553  						PullPolicy: image.PullNever,
   554  					}))
   555  				}
   556  			})
   557  		})
   558  
   559  		when("nested package", func() {
   560  			var (
   561  				nestedPackage     *fakes.Image
   562  				childDescriptor   dist.BuildpackDescriptor
   563  				packageDescriptor dist.BuildpackDescriptor
   564  				tmpDir            string
   565  				err               error
   566  			)
   567  
   568  			it.Before(func() {
   569  				childDescriptor = dist.BuildpackDescriptor{
   570  					WithAPI:    api.MustParse("0.2"),
   571  					WithInfo:   dist.ModuleInfo{ID: "bp.nested", Version: "2.3.4"},
   572  					WithStacks: []dist.Stack{{ID: "some.stack.id"}},
   573  				}
   574  
   575  				packageDescriptor = dist.BuildpackDescriptor{
   576  					WithAPI:  api.MustParse("0.2"),
   577  					WithInfo: dist.ModuleInfo{ID: "bp.1", Version: "1.2.3"},
   578  					WithOrder: dist.Order{{
   579  						Group: []dist.ModuleRef{{
   580  							ModuleInfo: dist.ModuleInfo{ID: "bp.nested", Version: "2.3.4"},
   581  							Optional:   false,
   582  						}},
   583  					}},
   584  				}
   585  
   586  				tmpDir, err = os.MkdirTemp("", "package-buildpack")
   587  				h.AssertNil(t, err)
   588  			})
   589  
   590  			it.After(func() {
   591  				h.AssertNil(t, os.RemoveAll(tmpDir))
   592  			})
   593  
   594  			when("dependencies are packaged buildpack image", func() {
   595  				it.Before(func() {
   596  					nestedPackage = fakes.NewImage("nested/package-"+h.RandString(12), "", nil)
   597  					mockImageFactory.EXPECT().NewImage(nestedPackage.Name(), false, "linux").Return(nestedPackage, nil)
   598  
   599  					h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   600  						Name: nestedPackage.Name(),
   601  						Config: pubbldpkg.Config{
   602  							Platform:  dist.Platform{OS: "linux"},
   603  							Buildpack: dist.BuildpackURI{URI: createBuildpack(childDescriptor)},
   604  						},
   605  						Publish:    true,
   606  						PullPolicy: image.PullAlways,
   607  					}))
   608  
   609  					mockImageFetcher.EXPECT().Fetch(gomock.Any(), nestedPackage.Name(), image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(nestedPackage, nil)
   610  				})
   611  
   612  				it("should pull and use local nested package image", func() {
   613  					packagePath := filepath.Join(tmpDir, "test.cnb")
   614  
   615  					h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   616  						Name: packagePath,
   617  						Config: pubbldpkg.Config{
   618  							Platform:     dist.Platform{OS: "linux"},
   619  							Buildpack:    dist.BuildpackURI{URI: createBuildpack(packageDescriptor)},
   620  							Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: nestedPackage.Name()}}},
   621  						},
   622  						Publish:    false,
   623  						PullPolicy: image.PullAlways,
   624  						Format:     client.FormatFile,
   625  					}))
   626  
   627  					assertPackageBPFileHasBuildpacks(t, packagePath, []dist.BuildpackDescriptor{packageDescriptor, childDescriptor})
   628  				})
   629  			})
   630  
   631  			when("dependencies are unpackaged buildpack", func() {
   632  				it("should work", func() {
   633  					packagePath := filepath.Join(tmpDir, "test.cnb")
   634  
   635  					h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   636  						Name: packagePath,
   637  						Config: pubbldpkg.Config{
   638  							Platform:     dist.Platform{OS: "linux"},
   639  							Buildpack:    dist.BuildpackURI{URI: createBuildpack(packageDescriptor)},
   640  							Dependencies: []dist.ImageOrURI{{BuildpackURI: dist.BuildpackURI{URI: createBuildpack(childDescriptor)}}},
   641  						},
   642  						Publish:    false,
   643  						PullPolicy: image.PullAlways,
   644  						Format:     client.FormatFile,
   645  					}))
   646  
   647  					assertPackageBPFileHasBuildpacks(t, packagePath, []dist.BuildpackDescriptor{packageDescriptor, childDescriptor})
   648  				})
   649  
   650  				when("dependency download fails", func() {
   651  					it("should error", func() {
   652  						bpURL := fmt.Sprintf("https://example.com/bp.%s.tgz", h.RandString(12))
   653  						mockDownloader.EXPECT().Download(gomock.Any(), bpURL).Return(nil, image.ErrNotFound).AnyTimes()
   654  
   655  						packagePath := filepath.Join(tmpDir, "test.cnb")
   656  
   657  						err = subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   658  							Name: packagePath,
   659  							Config: pubbldpkg.Config{
   660  								Platform:     dist.Platform{OS: "linux"},
   661  								Buildpack:    dist.BuildpackURI{URI: createBuildpack(packageDescriptor)},
   662  								Dependencies: []dist.ImageOrURI{{BuildpackURI: dist.BuildpackURI{URI: bpURL}}},
   663  							},
   664  							Publish:    false,
   665  							PullPolicy: image.PullAlways,
   666  							Format:     client.FormatFile,
   667  						})
   668  						h.AssertError(t, err, "downloading buildpack")
   669  					})
   670  				})
   671  
   672  				when("dependency isn't a valid buildpack", func() {
   673  					it("should error", func() {
   674  						fakeBlob := blob.NewBlob(filepath.Join("testdata", "empty-file"))
   675  						bpURL := fmt.Sprintf("https://example.com/bp.%s.tgz", h.RandString(12))
   676  						mockDownloader.EXPECT().Download(gomock.Any(), bpURL).Return(fakeBlob, nil).AnyTimes()
   677  
   678  						packagePath := filepath.Join(tmpDir, "test.cnb")
   679  
   680  						err = subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   681  							Name: packagePath,
   682  							Config: pubbldpkg.Config{
   683  								Platform:     dist.Platform{OS: "linux"},
   684  								Buildpack:    dist.BuildpackURI{URI: createBuildpack(packageDescriptor)},
   685  								Dependencies: []dist.ImageOrURI{{BuildpackURI: dist.BuildpackURI{URI: bpURL}}},
   686  							},
   687  							Publish:    false,
   688  							PullPolicy: image.PullAlways,
   689  							Format:     client.FormatFile,
   690  						})
   691  						h.AssertError(t, err, "packaging dependencies")
   692  					})
   693  				})
   694  			})
   695  
   696  			when("dependencies include packaged buildpack image and unpacked buildpack", func() {
   697  				var secondChildDescriptor dist.BuildpackDescriptor
   698  
   699  				it.Before(func() {
   700  					secondChildDescriptor = dist.BuildpackDescriptor{
   701  						WithAPI:    api.MustParse("0.2"),
   702  						WithInfo:   dist.ModuleInfo{ID: "bp.nested1", Version: "2.3.4"},
   703  						WithStacks: []dist.Stack{{ID: "some.stack.id"}},
   704  					}
   705  
   706  					packageDescriptor.WithOrder = append(packageDescriptor.Order(), dist.OrderEntry{Group: []dist.ModuleRef{{
   707  						ModuleInfo: dist.ModuleInfo{ID: secondChildDescriptor.Info().ID, Version: secondChildDescriptor.Info().Version},
   708  						Optional:   false,
   709  					}}})
   710  
   711  					nestedPackage = fakes.NewImage("nested/package-"+h.RandString(12), "", nil)
   712  					mockImageFactory.EXPECT().NewImage(nestedPackage.Name(), false, "linux").Return(nestedPackage, nil)
   713  
   714  					h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   715  						Name: nestedPackage.Name(),
   716  						Config: pubbldpkg.Config{
   717  							Platform:  dist.Platform{OS: "linux"},
   718  							Buildpack: dist.BuildpackURI{URI: createBuildpack(childDescriptor)},
   719  						},
   720  						Publish:    true,
   721  						PullPolicy: image.PullAlways,
   722  					}))
   723  
   724  					mockImageFetcher.EXPECT().Fetch(gomock.Any(), nestedPackage.Name(), image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(nestedPackage, nil)
   725  				})
   726  
   727  				it("should include both of them", func() {
   728  					packagePath := filepath.Join(tmpDir, "test.cnb")
   729  
   730  					h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   731  						Name: packagePath,
   732  						Config: pubbldpkg.Config{
   733  							Platform:  dist.Platform{OS: "linux"},
   734  							Buildpack: dist.BuildpackURI{URI: createBuildpack(packageDescriptor)},
   735  							Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: nestedPackage.Name()}},
   736  								{BuildpackURI: dist.BuildpackURI{URI: createBuildpack(secondChildDescriptor)}}},
   737  						},
   738  						Publish:    false,
   739  						PullPolicy: image.PullAlways,
   740  						Format:     client.FormatFile,
   741  					}))
   742  
   743  					assertPackageBPFileHasBuildpacks(t, packagePath, []dist.BuildpackDescriptor{packageDescriptor, childDescriptor, secondChildDescriptor})
   744  				})
   745  			})
   746  
   747  			when("dependencies include a packaged buildpack file", func() {
   748  				var (
   749  					dependencyPackagePath string
   750  				)
   751  				it.Before(func() {
   752  					dependencyPackagePath = filepath.Join(tmpDir, "dep.cnb")
   753  					dependencyPackageURI, err := paths.FilePathToURI(dependencyPackagePath, "")
   754  					h.AssertNil(t, err)
   755  
   756  					h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   757  						Name: dependencyPackagePath,
   758  						Config: pubbldpkg.Config{
   759  							Platform:  dist.Platform{OS: "linux"},
   760  							Buildpack: dist.BuildpackURI{URI: createBuildpack(childDescriptor)},
   761  						},
   762  						PullPolicy: image.PullAlways,
   763  						Format:     client.FormatFile,
   764  					}))
   765  
   766  					mockDownloader.EXPECT().Download(gomock.Any(), dependencyPackageURI).Return(blob.NewBlob(dependencyPackagePath), nil).AnyTimes()
   767  				})
   768  
   769  				it("should open file and correctly add buildpacks", func() {
   770  					packagePath := filepath.Join(tmpDir, "test.cnb")
   771  
   772  					h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   773  						Name: packagePath,
   774  						Config: pubbldpkg.Config{
   775  							Platform:     dist.Platform{OS: "linux"},
   776  							Buildpack:    dist.BuildpackURI{URI: createBuildpack(packageDescriptor)},
   777  							Dependencies: []dist.ImageOrURI{{BuildpackURI: dist.BuildpackURI{URI: dependencyPackagePath}}},
   778  						},
   779  						Publish:    false,
   780  						PullPolicy: image.PullAlways,
   781  						Format:     client.FormatFile,
   782  					}))
   783  
   784  					assertPackageBPFileHasBuildpacks(t, packagePath, []dist.BuildpackDescriptor{packageDescriptor, childDescriptor})
   785  				})
   786  			})
   787  
   788  			when("dependencies include a buildpack registry urn file", func() {
   789  				var (
   790  					tmpDir          string
   791  					registryFixture string
   792  					packHome        string
   793  				)
   794  				it.Before(func() {
   795  					var err error
   796  
   797  					childDescriptor = dist.BuildpackDescriptor{
   798  						WithAPI:    api.MustParse("0.2"),
   799  						WithInfo:   dist.ModuleInfo{ID: "example/foo", Version: "1.1.0"},
   800  						WithStacks: []dist.Stack{{ID: "some.stack.id"}},
   801  					}
   802  
   803  					packageDescriptor = dist.BuildpackDescriptor{
   804  						WithAPI:  api.MustParse("0.2"),
   805  						WithInfo: dist.ModuleInfo{ID: "bp.1", Version: "1.2.3"},
   806  						WithOrder: dist.Order{{
   807  							Group: []dist.ModuleRef{{
   808  								ModuleInfo: dist.ModuleInfo{ID: "example/foo", Version: "1.1.0"},
   809  								Optional:   false,
   810  							}},
   811  						}},
   812  					}
   813  
   814  					tmpDir, err = os.MkdirTemp("", "registry")
   815  					h.AssertNil(t, err)
   816  
   817  					packHome = filepath.Join(tmpDir, ".pack")
   818  					err = os.MkdirAll(packHome, 0755)
   819  					h.AssertNil(t, err)
   820  					os.Setenv("PACK_HOME", packHome)
   821  
   822  					registryFixture = h.CreateRegistryFixture(t, tmpDir, filepath.Join("testdata", "registry"))
   823  					h.AssertNotNil(t, registryFixture)
   824  
   825  					packageImage := fakes.NewImage("example.com/some/package@sha256:74eb48882e835d8767f62940d453eb96ed2737de3a16573881dcea7dea769df7", "", nil)
   826  					err = packageImage.AddLayerWithDiffID("testdata/empty-file", "sha256:xxx")
   827  					h.AssertNil(t, err)
   828  					err = packageImage.SetLabel("io.buildpacks.buildpackage.metadata", `{"id":"example/foo", "version":"1.1.0", "stacks":[{"id":"some.stack.id"}]}`)
   829  					h.AssertNil(t, err)
   830  					err = packageImage.SetLabel("io.buildpacks.buildpack.layers", `{"example/foo":{"1.1.0":{"api": "0.2", "layerDiffID":"sha256:xxx", "stacks":[{"id":"some.stack.id"}]}}}`)
   831  					h.AssertNil(t, err)
   832  					mockImageFetcher.EXPECT().Fetch(gomock.Any(), packageImage.Name(), image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(packageImage, nil)
   833  
   834  					packHome := filepath.Join(tmpDir, "packHome")
   835  					h.AssertNil(t, os.Setenv("PACK_HOME", packHome))
   836  					configPath := filepath.Join(packHome, "config.toml")
   837  					h.AssertNil(t, cfg.Write(cfg.Config{
   838  						Registries: []cfg.Registry{
   839  							{
   840  								Name: "some-registry",
   841  								Type: "github",
   842  								URL:  registryFixture,
   843  							},
   844  						},
   845  					}, configPath))
   846  				})
   847  
   848  				it.After(func() {
   849  					os.Unsetenv("PACK_HOME")
   850  					err := os.RemoveAll(tmpDir)
   851  					h.AssertNil(t, err)
   852  				})
   853  
   854  				it("should open file and correctly add buildpacks", func() {
   855  					packagePath := filepath.Join(tmpDir, "test.cnb")
   856  
   857  					h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   858  						Name: packagePath,
   859  						Config: pubbldpkg.Config{
   860  							Platform:     dist.Platform{OS: "linux"},
   861  							Buildpack:    dist.BuildpackURI{URI: createBuildpack(packageDescriptor)},
   862  							Dependencies: []dist.ImageOrURI{{BuildpackURI: dist.BuildpackURI{URI: "urn:cnb:registry:example/foo@1.1.0"}}},
   863  						},
   864  						Publish:    false,
   865  						PullPolicy: image.PullAlways,
   866  						Format:     client.FormatFile,
   867  						Registry:   "some-registry",
   868  					}))
   869  
   870  					assertPackageBPFileHasBuildpacks(t, packagePath, []dist.BuildpackDescriptor{packageDescriptor, childDescriptor})
   871  				})
   872  			})
   873  		})
   874  	})
   875  
   876  	when("unknown format is provided", func() {
   877  		it("should error", func() {
   878  			mockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: "linux"}, nil).AnyTimes()
   879  
   880  			err := subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
   881  				Name:   "some-buildpack",
   882  				Format: "invalid-format",
   883  				Config: pubbldpkg.Config{
   884  					Platform: dist.Platform{OS: "linux"},
   885  					Buildpack: dist.BuildpackURI{URI: createBuildpack(dist.BuildpackDescriptor{
   886  						WithAPI:    api.MustParse("0.2"),
   887  						WithInfo:   dist.ModuleInfo{ID: "bp.1", Version: "1.2.3"},
   888  						WithStacks: []dist.Stack{{ID: "some.stack.id"}},
   889  					})},
   890  				},
   891  				Publish:    false,
   892  				PullPolicy: image.PullAlways,
   893  			})
   894  			h.AssertError(t, err, "unknown format: 'invalid-format'")
   895  		})
   896  	})
   897  }
   898  
   899  func assertPackageBPFileHasBuildpacks(t *testing.T, path string, descriptors []dist.BuildpackDescriptor) {
   900  	packageBlob := blob.NewBlob(path)
   901  	mainBP, depBPs, err := buildpack.BuildpacksFromOCILayoutBlob(packageBlob)
   902  	h.AssertNil(t, err)
   903  	h.AssertBuildpacksHaveDescriptors(t, append([]buildpack.BuildModule{mainBP}, depBPs...), descriptors)
   904  }