github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/pkg/client/inspect_buildpack_test.go (about) 1 package client_test 2 3 import ( 4 "archive/tar" 5 "bytes" 6 "fmt" 7 "os" 8 "path/filepath" 9 "runtime" 10 "testing" 11 12 "github.com/buildpacks/imgutil/fakes" 13 "github.com/buildpacks/lifecycle/api" 14 "github.com/golang/mock/gomock" 15 "github.com/google/go-containerregistry/pkg/v1/empty" 16 "github.com/google/go-containerregistry/pkg/v1/layout" 17 "github.com/google/go-containerregistry/pkg/v1/mutate" 18 "github.com/heroku/color" 19 "github.com/pkg/errors" 20 "github.com/sclevine/spec" 21 "github.com/sclevine/spec/report" 22 23 cfg "github.com/buildpacks/pack/internal/config" 24 "github.com/buildpacks/pack/pkg/archive" 25 "github.com/buildpacks/pack/pkg/blob" 26 "github.com/buildpacks/pack/pkg/buildpack" 27 "github.com/buildpacks/pack/pkg/client" 28 "github.com/buildpacks/pack/pkg/dist" 29 "github.com/buildpacks/pack/pkg/image" 30 "github.com/buildpacks/pack/pkg/logging" 31 "github.com/buildpacks/pack/pkg/testmocks" 32 h "github.com/buildpacks/pack/testhelpers" 33 ) 34 35 const buildpackageMetadataTag = `{ 36 "id": "some/top-buildpack", 37 "version": "0.0.1", 38 "name": "top", 39 "homepage": "top-buildpack-homepage", 40 "stacks": [ 41 { 42 "id": "io.buildpacks.stacks.first-stack" 43 }, 44 { 45 "id": "io.buildpacks.stacks.second-stack" 46 } 47 ] 48 }` 49 50 const buildpackLayersTag = `{ 51 "some/first-inner-buildpack":{ 52 "1.0.0":{ 53 "api":"0.2", 54 "order":[ 55 { 56 "group":[ 57 { 58 "id":"some/first-inner-buildpack", 59 "version":"1.0.0" 60 }, 61 { 62 "id":"some/second-inner-buildpack", 63 "version":"3.0.0" 64 } 65 ] 66 }, 67 { 68 "group":[ 69 { 70 "id":"some/second-inner-buildpack", 71 "version":"3.0.0" 72 } 73 ] 74 } 75 ], 76 "stacks":[ 77 { 78 "id":"io.buildpacks.stacks.first-stack" 79 }, 80 { 81 "id":"io.buildpacks.stacks.second-stack" 82 } 83 ], 84 "layerDiffID":"sha256:first-inner-buildpack-diff-id", 85 "homepage":"first-inner-buildpack-homepage" 86 } 87 }, 88 "some/second-inner-buildpack":{ 89 "2.0.0":{ 90 "api":"0.2", 91 "stacks":[ 92 { 93 "id":"io.buildpacks.stacks.first-stack" 94 }, 95 { 96 "id":"io.buildpacks.stacks.second-stack" 97 } 98 ], 99 "layerDiffID":"sha256:second-inner-buildpack-diff-id", 100 "homepage":"second-inner-buildpack-homepage" 101 }, 102 "3.0.0":{ 103 "api":"0.2", 104 "stacks":[ 105 { 106 "id":"io.buildpacks.stacks.first-stack" 107 }, 108 { 109 "id":"io.buildpacks.stacks.second-stack" 110 } 111 ], 112 "layerDiffID":"sha256:third-inner-buildpack-diff-id", 113 "homepage":"third-inner-buildpack-homepage" 114 } 115 }, 116 "some/top-buildpack":{ 117 "0.0.1":{ 118 "api":"0.2", 119 "order":[ 120 { 121 "group":[ 122 { 123 "id":"some/first-inner-buildpack", 124 "version":"1.0.0" 125 }, 126 { 127 "id":"some/second-inner-buildpack", 128 "version":"2.0.0" 129 } 130 ] 131 }, 132 { 133 "group":[ 134 { 135 "id":"some/first-inner-buildpack", 136 "version":"1.0.0" 137 } 138 ] 139 } 140 ], 141 "layerDiffID":"sha256:top-buildpack-diff-id", 142 "homepage":"top-buildpack-homepage", 143 "name": "top" 144 } 145 } 146 }` 147 148 func TestInspectBuildpack(t *testing.T) { 149 color.Disable(true) 150 defer color.Disable(false) 151 spec.Run(t, "InspectBuilder", testInspectBuildpack, spec.Sequential(), spec.Report(report.Terminal{})) 152 } 153 154 func testInspectBuildpack(t *testing.T, when spec.G, it spec.S) { 155 var ( 156 subject *client.Client 157 mockImageFetcher *testmocks.MockImageFetcher 158 mockController *gomock.Controller 159 out bytes.Buffer 160 buildpackImage *fakes.Image 161 apiVersion *api.Version 162 expectedInfo *client.BuildpackInfo 163 mockDownloader *testmocks.MockBlobDownloader 164 165 tmpDir string 166 buildpackPath string 167 ) 168 169 it.Before(func() { 170 mockController = gomock.NewController(t) 171 mockImageFetcher = testmocks.NewMockImageFetcher(mockController) 172 mockDownloader = testmocks.NewMockBlobDownloader(mockController) 173 174 subject = &client.Client{} 175 client.WithLogger(logging.NewLogWithWriters(&out, &out))(subject) 176 client.WithFetcher(mockImageFetcher)(subject) 177 client.WithDownloader(mockDownloader)(subject) 178 179 buildpackImage = fakes.NewImage("some/buildpack", "", nil) 180 h.AssertNil(t, buildpackImage.SetLabel(buildpack.MetadataLabel, buildpackageMetadataTag)) 181 h.AssertNil(t, buildpackImage.SetLabel(dist.BuildpackLayersLabel, buildpackLayersTag)) 182 183 var err error 184 apiVersion, err = api.NewVersion("0.2") 185 h.AssertNil(t, err) 186 187 tmpDir, err = os.MkdirTemp("", "inspectBuildpack") 188 h.AssertNil(t, err) 189 190 buildpackPath = filepath.Join(tmpDir, "buildpackTarFile.tar") 191 192 expectedInfo = &client.BuildpackInfo{ 193 BuildpackMetadata: buildpack.Metadata{ 194 ModuleInfo: dist.ModuleInfo{ 195 ID: "some/top-buildpack", 196 Version: "0.0.1", 197 Name: "top", 198 Homepage: "top-buildpack-homepage", 199 }, 200 Stacks: []dist.Stack{ 201 {ID: "io.buildpacks.stacks.first-stack"}, 202 {ID: "io.buildpacks.stacks.second-stack"}, 203 }, 204 }, 205 Buildpacks: []dist.ModuleInfo{ 206 { 207 ID: "some/first-inner-buildpack", 208 Version: "1.0.0", 209 Homepage: "first-inner-buildpack-homepage", 210 }, 211 { 212 ID: "some/second-inner-buildpack", 213 Version: "2.0.0", 214 Homepage: "second-inner-buildpack-homepage", 215 }, 216 { 217 ID: "some/second-inner-buildpack", 218 Version: "3.0.0", 219 Homepage: "third-inner-buildpack-homepage", 220 }, 221 { 222 ID: "some/top-buildpack", 223 Version: "0.0.1", 224 Name: "top", 225 Homepage: "top-buildpack-homepage", 226 }, 227 }, 228 Order: dist.Order{ 229 { 230 Group: []dist.ModuleRef{ 231 { 232 ModuleInfo: dist.ModuleInfo{ 233 ID: "some/top-buildpack", 234 Version: "0.0.1", 235 Name: "top", 236 Homepage: "top-buildpack-homepage", 237 }, 238 Optional: false, 239 }, 240 }, 241 }, 242 }, 243 BuildpackLayers: dist.ModuleLayers{ 244 "some/first-inner-buildpack": { 245 "1.0.0": { 246 API: apiVersion, 247 Stacks: []dist.Stack{ 248 {ID: "io.buildpacks.stacks.first-stack"}, 249 {ID: "io.buildpacks.stacks.second-stack"}, 250 }, 251 Order: dist.Order{ 252 { 253 Group: []dist.ModuleRef{ 254 { 255 ModuleInfo: dist.ModuleInfo{ 256 ID: "some/first-inner-buildpack", 257 Version: "1.0.0", 258 }, 259 Optional: false, 260 }, 261 { 262 ModuleInfo: dist.ModuleInfo{ 263 ID: "some/second-inner-buildpack", 264 Version: "3.0.0", 265 }, 266 Optional: false, 267 }, 268 }, 269 }, 270 { 271 Group: []dist.ModuleRef{ 272 { 273 ModuleInfo: dist.ModuleInfo{ 274 ID: "some/second-inner-buildpack", 275 Version: "3.0.0", 276 }, 277 Optional: false, 278 }, 279 }, 280 }, 281 }, 282 LayerDiffID: "sha256:first-inner-buildpack-diff-id", 283 Homepage: "first-inner-buildpack-homepage", 284 }, 285 }, 286 "some/second-inner-buildpack": { 287 "2.0.0": { 288 API: apiVersion, 289 Stacks: []dist.Stack{ 290 {ID: "io.buildpacks.stacks.first-stack"}, 291 {ID: "io.buildpacks.stacks.second-stack"}, 292 }, 293 LayerDiffID: "sha256:second-inner-buildpack-diff-id", 294 Homepage: "second-inner-buildpack-homepage", 295 }, 296 "3.0.0": { 297 API: apiVersion, 298 Stacks: []dist.Stack{ 299 {ID: "io.buildpacks.stacks.first-stack"}, 300 {ID: "io.buildpacks.stacks.second-stack"}, 301 }, 302 LayerDiffID: "sha256:third-inner-buildpack-diff-id", 303 Homepage: "third-inner-buildpack-homepage", 304 }, 305 }, 306 "some/top-buildpack": { 307 "0.0.1": { 308 API: apiVersion, 309 Order: dist.Order{ 310 { 311 Group: []dist.ModuleRef{ 312 { 313 ModuleInfo: dist.ModuleInfo{ 314 ID: "some/first-inner-buildpack", 315 Version: "1.0.0", 316 }, 317 Optional: false, 318 }, 319 { 320 ModuleInfo: dist.ModuleInfo{ 321 ID: "some/second-inner-buildpack", 322 Version: "2.0.0", 323 }, 324 Optional: false, 325 }, 326 }, 327 }, 328 { 329 Group: []dist.ModuleRef{ 330 { 331 ModuleInfo: dist.ModuleInfo{ 332 ID: "some/first-inner-buildpack", 333 Version: "1.0.0", 334 }, 335 Optional: false, 336 }, 337 }, 338 }, 339 }, 340 LayerDiffID: "sha256:top-buildpack-diff-id", 341 Homepage: "top-buildpack-homepage", 342 Name: "top", 343 }, 344 }, 345 }, 346 } 347 }) 348 349 it.After(func() { 350 mockController.Finish() 351 err := os.RemoveAll(tmpDir) 352 if runtime.GOOS != "windows" { 353 h.AssertNil(t, err) 354 } 355 }) 356 357 when("inspect-buildpack", func() { 358 when("inspecting a registry buildpack", func() { 359 var registryFixture string 360 var configPath string 361 it.Before(func() { 362 expectedInfo.Location = buildpack.RegistryLocator 363 364 registryFixture = h.CreateRegistryFixture(t, tmpDir, filepath.Join("testdata", "registry")) 365 packHome := filepath.Join(tmpDir, "packHome") 366 h.AssertNil(t, os.Setenv("PACK_HOME", packHome)) 367 368 configPath = filepath.Join(packHome, "config.toml") 369 h.AssertNil(t, cfg.Write(cfg.Config{ 370 Registries: []cfg.Registry{ 371 { 372 Name: "some-registry", 373 Type: "github", 374 URL: registryFixture, 375 }, 376 }, 377 }, configPath)) 378 379 mockImageFetcher.EXPECT().Fetch( 380 gomock.Any(), 381 "example.com/some/package@sha256:8c27fe111c11b722081701dfed3bd55e039b9ce92865473cf4cdfa918071c566", 382 image.FetchOptions{Daemon: false, PullPolicy: image.PullNever}).Return(buildpackImage, nil) 383 }) 384 385 it.After(func() { 386 h.AssertNil(t, os.Unsetenv("PACK_HOME")) 387 }) 388 389 it("succeeds", func() { 390 registryBuildpack := "urn:cnb:registry:example/java" 391 inspectOptions := client.InspectBuildpackOptions{ 392 BuildpackName: registryBuildpack, 393 Registry: "some-registry", 394 } 395 info, err := subject.InspectBuildpack(inspectOptions) 396 h.AssertNil(t, err) 397 398 h.AssertEq(t, info, expectedInfo) 399 }) 400 401 // TODO add test case when buildpack is flattened 402 }) 403 404 when("inspecting local buildpack archive", func() { 405 it.Before(func() { 406 expectedInfo.Location = buildpack.URILocator 407 408 assert := h.NewAssertionManager(t) 409 writeBuildpackArchive(buildpackPath, tmpDir, assert) 410 }) 411 412 it("succeeds", func() { 413 mockDownloader.EXPECT().Download(gomock.Any(), buildpackPath).Return(blob.NewBlob(buildpackPath), nil) 414 inspectOptions := client.InspectBuildpackOptions{ 415 BuildpackName: buildpackPath, 416 Daemon: false, 417 } 418 info, err := subject.InspectBuildpack(inspectOptions) 419 h.AssertNil(t, err) 420 421 h.AssertEq(t, info, expectedInfo) 422 }) 423 424 // TODO add test case when buildpack is flattened 425 }) 426 427 when("inspecting an image", func() { 428 for _, useDaemon := range []bool{true, false} { 429 useDaemon := useDaemon 430 when(fmt.Sprintf("daemon is %t", useDaemon), func() { 431 it.Before(func() { 432 expectedInfo.Location = buildpack.PackageLocator 433 if useDaemon { 434 mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/buildpack", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).Return(buildpackImage, nil) 435 } else { 436 mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/buildpack", image.FetchOptions{Daemon: false, PullPolicy: image.PullNever}).Return(buildpackImage, nil) 437 } 438 }) 439 440 it("succeeds", func() { 441 inspectOptions := client.InspectBuildpackOptions{ 442 BuildpackName: "docker://some/buildpack", 443 Daemon: useDaemon, 444 } 445 info, err := subject.InspectBuildpack(inspectOptions) 446 h.AssertNil(t, err) 447 448 h.AssertEq(t, info, expectedInfo) 449 }) 450 }) 451 } 452 }) 453 }) 454 when("failure cases", func() { 455 when("invalid buildpack name", func() { 456 it("returns an error", func() { 457 invalidBuildpackName := "" 458 inspectOptions := client.InspectBuildpackOptions{ 459 BuildpackName: invalidBuildpackName, 460 } 461 _, err := subject.InspectBuildpack(inspectOptions) 462 463 h.AssertError(t, err, "unable to handle locator ") 464 h.AssertFalse(t, errors.Is(err, image.ErrNotFound)) 465 }) 466 }) 467 when("buildpack image", func() { 468 when("unable to fetch buildpack image", func() { 469 it.Before(func() { 470 mockImageFetcher.EXPECT().Fetch(gomock.Any(), "missing/buildpack", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).Return(nil, errors.Wrapf(image.ErrNotFound, "big bad error")) 471 }) 472 it("returns an ErrNotFound error", func() { 473 inspectOptions := client.InspectBuildpackOptions{ 474 BuildpackName: "docker://missing/buildpack", 475 Daemon: true, 476 } 477 _, err := subject.InspectBuildpack(inspectOptions) 478 h.AssertTrue(t, errors.Is(err, image.ErrNotFound)) 479 }) 480 }) 481 482 when("image does not have buildpackage metadata", func() { 483 it.Before(func() { 484 fakeImage := fakes.NewImage("empty", "", nil) 485 h.AssertNil(t, fakeImage.SetLabel(dist.BuildpackLayersLabel, ":::")) 486 mockImageFetcher.EXPECT().Fetch(gomock.Any(), "missing-metadata/buildpack", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).Return(fakeImage, nil) 487 }) 488 it("returns an error", func() { 489 inspectOptions := client.InspectBuildpackOptions{ 490 BuildpackName: "docker://missing-metadata/buildpack", 491 Daemon: true, 492 } 493 _, err := subject.InspectBuildpack(inspectOptions) 494 495 h.AssertError(t, err, fmt.Sprintf("unable to get image label %s", dist.BuildpackLayersLabel)) 496 h.AssertFalse(t, errors.Is(err, image.ErrNotFound)) 497 }) 498 }) 499 }) 500 when("buildpack archive", func() { 501 when("archive is not a buildpack", func() { 502 it.Before(func() { 503 invalidBuildpackPath := filepath.Join(tmpDir, "fake-buildpack-path") 504 h.AssertNil(t, os.WriteFile(invalidBuildpackPath, []byte("not a buildpack"), os.ModePerm)) 505 506 mockDownloader.EXPECT().Download(gomock.Any(), "https://invalid/buildpack").Return(blob.NewBlob(invalidBuildpackPath), nil) 507 }) 508 it("returns an error", func() { 509 inspectOptions := client.InspectBuildpackOptions{ 510 BuildpackName: "https://invalid/buildpack", 511 Daemon: true, 512 } 513 514 _, err := subject.InspectBuildpack(inspectOptions) 515 h.AssertNotNil(t, err) 516 h.AssertFalse(t, errors.Is(err, image.ErrNotFound)) 517 h.AssertError(t, err, "unable to fetch config from buildpack blob:") 518 }) 519 }) 520 when("unable to download buildpack archive", func() { 521 it.Before(func() { 522 mockDownloader.EXPECT().Download(gomock.Any(), "https://missing/buildpack").Return(nil, errors.New("unable to download archive")) 523 }) 524 it("returns a untyped error", func() { 525 inspectOptions := client.InspectBuildpackOptions{ 526 BuildpackName: "https://missing/buildpack", 527 Daemon: true, 528 } 529 530 _, err := subject.InspectBuildpack(inspectOptions) 531 h.AssertNotNil(t, err) 532 h.AssertFalse(t, errors.Is(err, image.ErrNotFound)) 533 h.AssertError(t, err, "unable to download archive") 534 }) 535 }) 536 }) 537 538 when("buildpack on registry", func() { 539 when("unable to get registry", func() { 540 it("returns an error", func() { 541 registryBuildpack := "urn:cnb:registry:example/foo" 542 inspectOptions := client.InspectBuildpackOptions{ 543 BuildpackName: registryBuildpack, 544 Daemon: true, 545 Registry: ":::", 546 } 547 548 _, err := subject.InspectBuildpack(inspectOptions) 549 550 h.AssertError(t, err, "invalid registry :::") 551 h.AssertFalse(t, errors.Is(err, image.ErrNotFound)) 552 }) 553 }) 554 when("buildpack is not on registry", func() { 555 var registryFixture string 556 var configPath string 557 558 it.Before(func() { 559 registryFixture = h.CreateRegistryFixture(t, tmpDir, filepath.Join("testdata", "registry")) 560 packHome := filepath.Join(tmpDir, "packHome") 561 h.AssertNil(t, os.Setenv("PACK_HOME", packHome)) 562 configPath = filepath.Join(packHome, "config.toml") 563 h.AssertNil(t, cfg.Write(cfg.Config{ 564 Registries: []cfg.Registry{ 565 { 566 Name: "some-registry", 567 Type: "github", 568 URL: registryFixture, 569 }, 570 }, 571 }, configPath)) 572 }) 573 it("returns an error", func() { 574 registryBuildpack := "urn:cnb:registry:example/not-present" 575 inspectOptions := client.InspectBuildpackOptions{ 576 BuildpackName: registryBuildpack, 577 Daemon: true, 578 Registry: "some-registry", 579 } 580 581 _, err := subject.InspectBuildpack(inspectOptions) 582 583 h.AssertError(t, err, "unable to find 'urn:cnb:registry:example/not-present' in registry:") 584 }) 585 }) 586 when("unable to fetch buildpack from registry", func() { 587 var registryFixture string 588 var configPath string 589 590 it.Before(func() { 591 registryFixture = h.CreateRegistryFixture(t, tmpDir, filepath.Join("testdata", "registry")) 592 packHome := filepath.Join(tmpDir, "packHome") 593 h.AssertNil(t, os.Setenv("PACK_HOME", packHome)) 594 595 configPath = filepath.Join(packHome, "config.toml") 596 h.AssertNil(t, cfg.Write(cfg.Config{ 597 Registries: []cfg.Registry{ 598 { 599 Name: "some-registry", 600 Type: "github", 601 URL: registryFixture, 602 }, 603 }, 604 }, configPath)) 605 mockImageFetcher.EXPECT().Fetch( 606 gomock.Any(), 607 "example.com/some/package@sha256:2560f05307e8de9d830f144d09556e19dd1eb7d928aee900ed02208ae9727e7a", 608 image.FetchOptions{Daemon: false, PullPolicy: image.PullNever}).Return(nil, image.ErrNotFound) 609 }) 610 it("returns an untyped error", func() { 611 registryBuildpack := "urn:cnb:registry:example/foo" 612 inspectOptions := client.InspectBuildpackOptions{ 613 BuildpackName: registryBuildpack, 614 Daemon: true, 615 Registry: "some-registry", 616 } 617 618 _, err := subject.InspectBuildpack(inspectOptions) 619 h.AssertNotNil(t, err) 620 h.AssertFalse(t, errors.Is(err, image.ErrNotFound)) 621 h.AssertError(t, err, "error pulling registry specified image") 622 }) 623 }) 624 }) 625 }) 626 } 627 628 // write an OCI image using GGCR lib 629 func writeBuildpackArchive(buildpackPath, tmpDir string, assert h.AssertionManager) { 630 layoutDir := filepath.Join(tmpDir, "layout") 631 imgIndex := empty.Index 632 img := empty.Image 633 c, err := img.ConfigFile() 634 assert.Nil(err) 635 636 c.Config.Labels = map[string]string{} 637 c.Config.Labels[buildpack.MetadataLabel] = buildpackageMetadataTag 638 c.Config.Labels[dist.BuildpackLayersLabel] = buildpackLayersTag 639 img, err = mutate.Config(img, c.Config) 640 assert.Nil(err) 641 642 p, err := layout.Write(layoutDir, imgIndex) 643 assert.Nil(err) 644 645 assert.Nil(p.AppendImage(img)) 646 assert.Nil(err) 647 648 buildpackWriter, err := os.Create(buildpackPath) 649 assert.Nil(err) 650 defer buildpackWriter.Close() 651 652 tw := tar.NewWriter(buildpackWriter) 653 defer tw.Close() 654 655 assert.Nil(archive.WriteDirToTar(tw, layoutDir, "/", 0, 0, 0755, true, false, nil)) 656 }