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 }