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

     1  package buildpack_test
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"testing"
    10  
    11  	"github.com/buildpacks/imgutil/fakes"
    12  	"github.com/buildpacks/lifecycle/api"
    13  	"github.com/docker/docker/api/types/system"
    14  	"github.com/golang/mock/gomock"
    15  	"github.com/heroku/color"
    16  	"github.com/pkg/errors"
    17  	"github.com/sclevine/spec"
    18  	"github.com/sclevine/spec/report"
    19  
    20  	pubbldpkg "github.com/buildpacks/pack/buildpackage"
    21  	ifakes "github.com/buildpacks/pack/internal/fakes"
    22  	"github.com/buildpacks/pack/internal/paths"
    23  	"github.com/buildpacks/pack/pkg/blob"
    24  	"github.com/buildpacks/pack/pkg/buildpack"
    25  	"github.com/buildpacks/pack/pkg/client"
    26  	"github.com/buildpacks/pack/pkg/dist"
    27  	"github.com/buildpacks/pack/pkg/image"
    28  	"github.com/buildpacks/pack/pkg/logging"
    29  	"github.com/buildpacks/pack/pkg/testmocks"
    30  	h "github.com/buildpacks/pack/testhelpers"
    31  )
    32  
    33  func TestBuildpackDownloader(t *testing.T) {
    34  	color.Disable(true)
    35  	defer color.Disable(false)
    36  	spec.Run(t, "BuildpackDownloader", testBuildpackDownloader, spec.Report(report.Terminal{}))
    37  }
    38  
    39  func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) {
    40  	var (
    41  		mockController       *gomock.Controller
    42  		mockDownloader       *testmocks.MockBlobDownloader
    43  		mockImageFactory     *testmocks.MockImageFactory
    44  		mockImageFetcher     *testmocks.MockImageFetcher
    45  		mockRegistryResolver *testmocks.MockRegistryResolver
    46  		mockDockerClient     *testmocks.MockCommonAPIClient
    47  		buildpackDownloader  client.BuildpackDownloader
    48  		logger               logging.Logger
    49  		out                  bytes.Buffer
    50  		tmpDir               string
    51  	)
    52  
    53  	var createBuildpack = func(descriptor dist.BuildpackDescriptor) string {
    54  		bp, err := ifakes.NewFakeBuildpackBlob(&descriptor, 0644)
    55  		h.AssertNil(t, err)
    56  		url := fmt.Sprintf("https://example.com/bp.%s.tgz", h.RandString(12))
    57  		mockDownloader.EXPECT().Download(gomock.Any(), url).Return(bp, nil).AnyTimes()
    58  		return url
    59  	}
    60  
    61  	var createPackage = func(imageName string) *fakes.Image {
    62  		packageImage := fakes.NewImage(imageName, "", nil)
    63  		mockImageFactory.EXPECT().NewImage(packageImage.Name(), false, "linux").Return(packageImage, nil)
    64  
    65  		pack, err := client.NewClient(
    66  			client.WithLogger(logger),
    67  			client.WithDownloader(mockDownloader),
    68  			client.WithImageFactory(mockImageFactory),
    69  			client.WithFetcher(mockImageFetcher),
    70  			client.WithDockerClient(mockDockerClient),
    71  		)
    72  		h.AssertNil(t, err)
    73  
    74  		h.AssertNil(t, pack.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{
    75  			Name: packageImage.Name(),
    76  			Config: pubbldpkg.Config{
    77  				Platform: dist.Platform{OS: "linux"},
    78  				Buildpack: dist.BuildpackURI{URI: 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  			},
    84  			Publish: true,
    85  		}))
    86  
    87  		return packageImage
    88  	}
    89  
    90  	it.Before(func() {
    91  		logger = logging.NewLogWithWriters(&out, &out, logging.WithVerbose())
    92  		mockController = gomock.NewController(t)
    93  		mockDownloader = testmocks.NewMockBlobDownloader(mockController)
    94  		mockRegistryResolver = testmocks.NewMockRegistryResolver(mockController)
    95  		mockImageFetcher = testmocks.NewMockImageFetcher(mockController)
    96  		mockImageFactory = testmocks.NewMockImageFactory(mockController)
    97  		mockDockerClient = testmocks.NewMockCommonAPIClient(mockController)
    98  		mockDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/bp-one.tgz").Return(blob.NewBlob(filepath.Join("testdata", "buildpack")), nil).AnyTimes()
    99  		mockDownloader.EXPECT().Download(gomock.Any(), "some/buildpack/dir").Return(blob.NewBlob(filepath.Join("testdata", "buildpack")), nil).AnyTimes()
   100  
   101  		buildpackDownloader = buildpack.NewDownloader(logger, mockImageFetcher, mockDownloader, mockRegistryResolver)
   102  
   103  		mockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: "linux"}, nil).AnyTimes()
   104  
   105  		mockRegistryResolver.EXPECT().
   106  			Resolve("some-registry", "urn:cnb:registry:example/foo@1.1.0").
   107  			Return("example.com/some/package@sha256:74eb48882e835d8767f62940d453eb96ed2737de3a16573881dcea7dea769df7", nil).
   108  			AnyTimes()
   109  		mockRegistryResolver.EXPECT().
   110  			Resolve("some-registry", "example/foo@1.1.0").
   111  			Return("example.com/some/package@sha256:74eb48882e835d8767f62940d453eb96ed2737de3a16573881dcea7dea769df7", nil).
   112  			AnyTimes()
   113  
   114  		var err error
   115  		tmpDir, err = os.MkdirTemp("", "buildpack-downloader-test")
   116  		h.AssertNil(t, err)
   117  	})
   118  
   119  	it.After(func() {
   120  		mockController.Finish()
   121  		h.AssertNil(t, os.RemoveAll(tmpDir))
   122  	})
   123  
   124  	when("#Download", func() {
   125  		var (
   126  			packageImage    *fakes.Image
   127  			downloadOptions = buildpack.DownloadOptions{ImageOS: "linux"}
   128  		)
   129  
   130  		shouldFetchPackageImageWith := func(demon bool, pull image.PullPolicy, platform string) {
   131  			mockImageFetcher.EXPECT().Fetch(gomock.Any(), packageImage.Name(), image.FetchOptions{
   132  				Daemon:     demon,
   133  				PullPolicy: pull,
   134  				Platform:   platform,
   135  			}).Return(packageImage, nil)
   136  		}
   137  
   138  		when("package image lives in cnb registry", func() {
   139  			it.Before(func() {
   140  				packageImage = createPackage("example.com/some/package@sha256:74eb48882e835d8767f62940d453eb96ed2737de3a16573881dcea7dea769df7")
   141  			})
   142  
   143  			when("daemon=true and pull-policy=always", func() {
   144  				it("should pull and use local package image", func() {
   145  					downloadOptions = buildpack.DownloadOptions{
   146  						RegistryName: "some-registry",
   147  						ImageOS:      "linux",
   148  						Platform:     "linux/amd64",
   149  						Daemon:       true,
   150  						PullPolicy:   image.PullAlways,
   151  					}
   152  
   153  					shouldFetchPackageImageWith(true, image.PullAlways, "linux/amd64")
   154  					mainBP, _, err := buildpackDownloader.Download(context.TODO(), "urn:cnb:registry:example/foo@1.1.0", downloadOptions)
   155  					h.AssertNil(t, err)
   156  					h.AssertEq(t, mainBP.Descriptor().Info().ID, "example/foo")
   157  				})
   158  			})
   159  
   160  			when("ambigious URI provided", func() {
   161  				it("should find package in registry", func() {
   162  					downloadOptions = buildpack.DownloadOptions{
   163  						RegistryName: "some-registry",
   164  						ImageOS:      "linux",
   165  						Daemon:       true,
   166  						PullPolicy:   image.PullAlways,
   167  					}
   168  
   169  					shouldFetchPackageImageWith(true, image.PullAlways, "")
   170  					mainBP, _, err := buildpackDownloader.Download(context.TODO(), "example/foo@1.1.0", downloadOptions)
   171  					h.AssertNil(t, err)
   172  					h.AssertEq(t, mainBP.Descriptor().Info().ID, "example/foo")
   173  				})
   174  			})
   175  		})
   176  
   177  		when("package image lives in docker registry", func() {
   178  			it.Before(func() {
   179  				packageImage = createPackage("docker.io/some/package-" + h.RandString(12))
   180  			})
   181  
   182  			prepareFetcherWithMissingPackageImage := func() {
   183  				mockImageFetcher.EXPECT().Fetch(gomock.Any(), packageImage.Name(), gomock.Any()).Return(nil, image.ErrNotFound)
   184  			}
   185  
   186  			when("image key is provided", func() {
   187  				it("should succeed", func() {
   188  					packageImage = createPackage("some/package:tag")
   189  					downloadOptions = buildpack.DownloadOptions{
   190  						Daemon:     true,
   191  						PullPolicy: image.PullAlways,
   192  						ImageOS:    "linux",
   193  						Platform:   "linux/amd64",
   194  						ImageName:  "some/package:tag",
   195  					}
   196  
   197  					shouldFetchPackageImageWith(true, image.PullAlways, "linux/amd64")
   198  					mainBP, _, err := buildpackDownloader.Download(context.TODO(), "", downloadOptions)
   199  					h.AssertNil(t, err)
   200  					h.AssertEq(t, mainBP.Descriptor().Info().ID, "example/foo")
   201  				})
   202  			})
   203  
   204  			when("daemon=true and pull-policy=always", func() {
   205  				it("should pull and use local package image", func() {
   206  					downloadOptions = buildpack.DownloadOptions{
   207  						ImageOS:    "linux",
   208  						ImageName:  packageImage.Name(),
   209  						Daemon:     true,
   210  						PullPolicy: image.PullAlways,
   211  					}
   212  
   213  					shouldFetchPackageImageWith(true, image.PullAlways, "")
   214  					mainBP, _, err := buildpackDownloader.Download(context.TODO(), "", downloadOptions)
   215  					h.AssertNil(t, err)
   216  					h.AssertEq(t, mainBP.Descriptor().Info().ID, "example/foo")
   217  				})
   218  			})
   219  
   220  			when("daemon=false and pull-policy=always", func() {
   221  				it("should use remote package image", func() {
   222  					downloadOptions = buildpack.DownloadOptions{
   223  						ImageOS:    "linux",
   224  						ImageName:  packageImage.Name(),
   225  						Daemon:     false,
   226  						PullPolicy: image.PullAlways,
   227  					}
   228  
   229  					shouldFetchPackageImageWith(false, image.PullAlways, "")
   230  					mainBP, _, err := buildpackDownloader.Download(context.TODO(), "", downloadOptions)
   231  					h.AssertNil(t, err)
   232  					h.AssertEq(t, mainBP.Descriptor().Info().ID, "example/foo")
   233  				})
   234  			})
   235  
   236  			when("daemon=false and pull-policy=always", func() {
   237  				it("should use remote package URI", func() {
   238  					downloadOptions = buildpack.DownloadOptions{
   239  						ImageOS:    "linux",
   240  						Daemon:     false,
   241  						PullPolicy: image.PullAlways,
   242  					}
   243  					shouldFetchPackageImageWith(false, image.PullAlways, "")
   244  					mainBP, _, err := buildpackDownloader.Download(context.TODO(), packageImage.Name(), downloadOptions)
   245  					h.AssertNil(t, err)
   246  					h.AssertEq(t, mainBP.Descriptor().Info().ID, "example/foo")
   247  				})
   248  			})
   249  
   250  			when("publish=true and pull-policy=never", func() {
   251  				it("should push to registry and not pull package image", func() {
   252  					downloadOptions = buildpack.DownloadOptions{
   253  						ImageOS:    "linux",
   254  						ImageName:  packageImage.Name(),
   255  						Daemon:     false,
   256  						PullPolicy: image.PullNever,
   257  					}
   258  
   259  					shouldFetchPackageImageWith(false, image.PullNever, "")
   260  					mainBP, _, err := buildpackDownloader.Download(context.TODO(), "", downloadOptions)
   261  					h.AssertNil(t, err)
   262  					h.AssertEq(t, mainBP.Descriptor().Info().ID, "example/foo")
   263  				})
   264  			})
   265  
   266  			when("daemon=true pull-policy=never and there is no local package image", func() {
   267  				it("should fail without trying to retrieve package image from registry", func() {
   268  					downloadOptions = buildpack.DownloadOptions{
   269  						ImageOS:    "linux",
   270  						ImageName:  packageImage.Name(),
   271  						Daemon:     true,
   272  						PullPolicy: image.PullNever,
   273  					}
   274  					prepareFetcherWithMissingPackageImage()
   275  					_, _, err := buildpackDownloader.Download(context.TODO(), "", downloadOptions)
   276  					h.AssertError(t, err, "not found")
   277  				})
   278  			})
   279  		})
   280  
   281  		when("package lives on filesystem", func() {
   282  			it("should successfully retrieve package from absolute path", func() {
   283  				buildpackPath := filepath.Join("testdata", "buildpack")
   284  				buildpackURI, _ := paths.FilePathToURI(buildpackPath, "")
   285  				mockDownloader.EXPECT().Download(gomock.Any(), buildpackURI).Return(blob.NewBlob(buildpackPath), nil).AnyTimes()
   286  				mainBP, _, err := buildpackDownloader.Download(context.TODO(), buildpackURI, downloadOptions)
   287  				h.AssertNil(t, err)
   288  				h.AssertEq(t, mainBP.Descriptor().Info().ID, "bp.one")
   289  			})
   290  
   291  			it("should successfully retrieve package from relative path", func() {
   292  				buildpackPath := filepath.Join("testdata", "buildpack")
   293  				buildpackURI, _ := paths.FilePathToURI(buildpackPath, "")
   294  				mockDownloader.EXPECT().Download(gomock.Any(), buildpackURI).Return(blob.NewBlob(buildpackPath), nil).AnyTimes()
   295  				downloadOptions = buildpack.DownloadOptions{
   296  					ImageOS:         "linux",
   297  					RelativeBaseDir: "testdata",
   298  				}
   299  				mainBP, _, err := buildpackDownloader.Download(context.TODO(), "buildpack", downloadOptions)
   300  				h.AssertNil(t, err)
   301  				h.AssertEq(t, mainBP.Descriptor().Info().ID, "bp.one")
   302  			})
   303  
   304  			when("kind == extension", func() {
   305  				it("succeeds", func() {
   306  					extensionPath := filepath.Join("testdata", "extension")
   307  					extensionURI, _ := paths.FilePathToURI(extensionPath, "")
   308  					mockDownloader.EXPECT().Download(gomock.Any(), extensionURI).Return(blob.NewBlob(extensionPath), nil).AnyTimes()
   309  					downloadOptions = buildpack.DownloadOptions{
   310  						ImageOS:         "linux",
   311  						ModuleKind:      "extension",
   312  						RelativeBaseDir: "testdata",
   313  					}
   314  					mainExt, _, err := buildpackDownloader.Download(context.TODO(), "extension", downloadOptions)
   315  					h.AssertNil(t, err)
   316  					h.AssertEq(t, mainExt.Descriptor().Info().ID, "ext.one")
   317  				})
   318  			})
   319  
   320  			when("kind == packagedExtension", func() {
   321  				it("succeeds", func() {
   322  					packagedExtensionPath := filepath.Join("testdata", "tree-extension.cnb")
   323  					packagedExtensionURI, _ := paths.FilePathToURI(packagedExtensionPath, "")
   324  					mockDownloader.EXPECT().Download(gomock.Any(), packagedExtensionURI).Return(blob.NewBlob(packagedExtensionPath), nil).AnyTimes()
   325  					downloadOptions = buildpack.DownloadOptions{
   326  						ImageOS:         "linux",
   327  						ModuleKind:      "extension",
   328  						RelativeBaseDir: "testdata",
   329  						Daemon:          true,
   330  						PullPolicy:      image.PullAlways,
   331  					}
   332  					mainExt, _, _ := buildpackDownloader.Download(context.TODO(), "tree-extension.cnb", downloadOptions)
   333  					h.AssertEq(t, mainExt.Descriptor().Info().ID, "samples-tree")
   334  				})
   335  			})
   336  		})
   337  
   338  		when("package image is not a valid package", func() {
   339  			it("errors", func() {
   340  				notPackageImage := fakes.NewImage("docker.io/not/package", "", nil)
   341  
   342  				mockImageFetcher.EXPECT().Fetch(gomock.Any(), notPackageImage.Name(), gomock.Any()).Return(notPackageImage, nil)
   343  				h.AssertNil(t, notPackageImage.SetLabel("io.buildpacks.buildpack.layers", ""))
   344  
   345  				downloadOptions.ImageName = notPackageImage.Name()
   346  				_, _, err := buildpackDownloader.Download(context.TODO(), "", downloadOptions)
   347  				h.AssertError(t, err, "extracting buildpacks from 'docker.io/not/package': could not find label 'io.buildpacks.buildpackage.metadata'")
   348  			})
   349  		})
   350  
   351  		when("invalid buildpack URI", func() {
   352  			when("buildpack URI is from=builder:fake", func() {
   353  				it("errors", func() {
   354  					_, _, err := buildpackDownloader.Download(context.TODO(), "from=builder:fake", downloadOptions)
   355  					h.AssertError(t, err, "'from=builder:fake' is not a valid identifier")
   356  				})
   357  			})
   358  
   359  			when("buildpack URI is from=builder", func() {
   360  				it("errors", func() {
   361  					_, _, err := buildpackDownloader.Download(context.TODO(), "from=builder", downloadOptions)
   362  					h.AssertError(t, err,
   363  						"invalid locator: FromBuilderLocator")
   364  				})
   365  			})
   366  
   367  			when("can't resolve buildpack in registry", func() {
   368  				it("errors", func() {
   369  					mockRegistryResolver.EXPECT().
   370  						Resolve("://bad-url", "urn:cnb:registry:fake").
   371  						Return("", errors.New("bad mhkay")).
   372  						AnyTimes()
   373  
   374  					downloadOptions.RegistryName = "://bad-url"
   375  					_, _, err := buildpackDownloader.Download(context.TODO(), "urn:cnb:registry:fake", downloadOptions)
   376  					h.AssertError(t, err, "locating in registry")
   377  				})
   378  			})
   379  
   380  			when("can't download image from registry", func() {
   381  				it("errors", func() {
   382  					packageImage := fakes.NewImage("example.com/some/package@sha256:74eb48882e835d8767f62940d453eb96ed2737de3a16573881dcea7dea769df7", "", nil)
   383  					mockImageFetcher.EXPECT().Fetch(gomock.Any(), packageImage.Name(), image.FetchOptions{Daemon: false, PullPolicy: image.PullAlways}).Return(nil, errors.New("failed to pull"))
   384  
   385  					downloadOptions.RegistryName = "some-registry"
   386  					_, _, err := buildpackDownloader.Download(context.TODO(), "urn:cnb:registry:example/foo@1.1.0", downloadOptions)
   387  					h.AssertError(t, err,
   388  						"extracting from registry")
   389  				})
   390  			})
   391  
   392  			when("buildpack URI is an invalid locator", func() {
   393  				it("errors", func() {
   394  					_, _, err := buildpackDownloader.Download(context.TODO(), "nonsense string here", downloadOptions)
   395  					h.AssertError(t, err,
   396  						"invalid locator: InvalidLocator")
   397  				})
   398  			})
   399  		})
   400  	})
   401  }