package client_test import ( "bytes" "context" "fmt" "os" "path/filepath" "strings" "testing" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/buildpacks/imgutil/fakes" "github.com/buildpacks/lifecycle/api" "github.com/golang/mock/gomock" "github.com/heroku/color" mobysystem "github.com/moby/moby/api/types/system" dockerclient "github.com/moby/moby/client" "github.com/pkg/errors" "github.com/sclevine/spec" "github.com/sclevine/spec/report" pubbldr "github.com/buildpacks/pack/builder" pubbldpkg "github.com/buildpacks/pack/buildpackage" "github.com/buildpacks/pack/internal/builder" ifakes "github.com/buildpacks/pack/internal/fakes" "github.com/buildpacks/pack/internal/paths" "github.com/buildpacks/pack/internal/style" "github.com/buildpacks/pack/pkg/archive" "github.com/buildpacks/pack/pkg/blob" "github.com/buildpacks/pack/pkg/buildpack" "github.com/buildpacks/pack/pkg/client" "github.com/buildpacks/pack/pkg/dist" "github.com/buildpacks/pack/pkg/image" "github.com/buildpacks/pack/pkg/logging" "github.com/buildpacks/pack/pkg/testmocks" h "github.com/buildpacks/pack/testhelpers" ) func TestCreateBuilder(t *testing.T) { color.Disable(true) defer color.Disable(false) spec.Run(t, "create_builder", testCreateBuilder, spec.Parallel(), spec.Report(report.Terminal{})) } func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { when("#CreateBuilder", func() { var ( mockController *gomock.Controller mockDownloader *testmocks.MockBlobDownloader mockBuildpackDownloader *testmocks.MockBuildpackDownloader mockImageFactory *testmocks.MockImageFactory mockImageFetcher *testmocks.MockImageFetcher mockDockerClient *testmocks.MockAPIClient fakeBuildImage *fakes.Image fakeRunImage *fakes.Image fakeRunImageMirror *fakes.Image opts client.CreateBuilderOptions subject *client.Client logger logging.Logger out bytes.Buffer tmpDir string ) var prepareFetcherWithRunImages = func() { mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/run-image", gomock.Any()).Return(fakeRunImage, nil).AnyTimes() mockImageFetcher.EXPECT().Fetch(gomock.Any(), "localhost:5000/some/run-image", gomock.Any()).Return(fakeRunImageMirror, nil).AnyTimes() } var prepareFetcherWithBuildImage = func() { mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", gomock.Any()).Return(fakeBuildImage, nil) } var prepareExtensions = func() { // Extensions require Platform API >= 0.13 opts.Config.Lifecycle.URI = "file:///some-lifecycle-platform-0-13" opts.Config.Extensions = []pubbldr.ModuleConfig{ { ModuleInfo: dist.ModuleInfo{ID: "ext.one", Version: "1.2.3", Homepage: "http://one.extension"}, ImageOrURI: dist.ImageOrURI{ BuildpackURI: dist.BuildpackURI{ URI: "https://example.fake/ext-one.tgz", }, }, }, } opts.Config.OrderExtensions = []dist.OrderEntry{{ Group: []dist.ModuleRef{ {ModuleInfo: dist.ModuleInfo{ID: "ext.one", Version: "1.2.3"}, Optional: true}, }}, } } var createBuildpack = func(descriptor dist.BuildpackDescriptor) buildpack.BuildModule { buildpack, err := ifakes.NewFakeBuildpack(descriptor, 0644) h.AssertNil(t, err) return buildpack } var shouldCallBuildpackDownloaderWith = func(uri string, buildpackDownloadOptions buildpack.DownloadOptions) { buildpack := createBuildpack(dist.BuildpackDescriptor{ WithAPI: api.MustParse("0.3"), WithInfo: dist.ModuleInfo{ID: "example/foo", Version: "1.1.0"}, WithStacks: []dist.Stack{{ID: "some.stack.id"}}, }) mockBuildpackDownloader.EXPECT().Download(gomock.Any(), uri, gomock.Any()).Return(buildpack, nil, nil) } it.Before(func() { logger = logging.NewLogWithWriters(&out, &out, logging.WithVerbose()) mockController = gomock.NewController(t) mockDownloader = testmocks.NewMockBlobDownloader(mockController) mockImageFetcher = testmocks.NewMockImageFetcher(mockController) mockImageFactory = testmocks.NewMockImageFactory(mockController) mockDockerClient = testmocks.NewMockAPIClient(mockController) mockBuildpackDownloader = testmocks.NewMockBuildpackDownloader(mockController) fakeBuildImage = fakes.NewImage("some/build-image", "", nil) h.AssertNil(t, fakeBuildImage.SetLabel("io.buildpacks.stack.id", "some.stack.id")) h.AssertNil(t, fakeBuildImage.SetLabel("io.buildpacks.stack.mixins", `["mixinX", "build:mixinY"]`)) h.AssertNil(t, fakeBuildImage.SetEnv("CNB_USER_ID", "1234")) h.AssertNil(t, fakeBuildImage.SetEnv("CNB_GROUP_ID", "4321")) fakeRunImage = fakes.NewImage("some/run-image", "", nil) h.AssertNil(t, fakeRunImage.SetLabel("io.buildpacks.stack.id", "some.stack.id")) fakeRunImageMirror = fakes.NewImage("localhost:5000/some/run-image", "", nil) h.AssertNil(t, fakeRunImageMirror.SetLabel("io.buildpacks.stack.id", "some.stack.id")) exampleBuildpackBlob := blob.NewBlob(filepath.Join("testdata", "buildpack")) mockDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/bp-one.tgz").Return(exampleBuildpackBlob, nil).AnyTimes() exampleExtensionBlob := blob.NewBlob(filepath.Join("testdata", "extension")) mockDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/ext-one.tgz").Return(exampleExtensionBlob, nil).AnyTimes() mockDownloader.EXPECT().Download(gomock.Any(), "some/buildpack/dir").Return(blob.NewBlob(filepath.Join("testdata", "buildpack")), nil).AnyTimes() mockDownloader.EXPECT().Download(gomock.Any(), "file:///some-lifecycle").Return(blob.NewBlob(filepath.Join("testdata", "lifecycle", "platform-0.4")), nil).AnyTimes() mockDownloader.EXPECT().Download(gomock.Any(), "file:///some-lifecycle-platform-0-1").Return(blob.NewBlob(filepath.Join("testdata", "lifecycle", "platform-0.3")), nil).AnyTimes() mockDownloader.EXPECT().Download(gomock.Any(), "file:///some-lifecycle-platform-0-13").Return(blob.NewBlob(filepath.Join("testdata", "lifecycle", "platform-0.13")), nil).AnyTimes() bp, err := buildpack.FromBuildpackRootBlob(exampleBuildpackBlob, archive.DefaultTarWriterFactory(), nil) h.AssertNil(t, err) mockBuildpackDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/bp-one.tgz", gomock.Any()).Return(bp, nil, nil).AnyTimes() ext, err := buildpack.FromExtensionRootBlob(exampleExtensionBlob, archive.DefaultTarWriterFactory(), nil) h.AssertNil(t, err) mockBuildpackDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/ext-one.tgz", gomock.Any()).Return(ext, nil, nil).AnyTimes() subject, err = client.NewClient( client.WithLogger(logger), client.WithDownloader(mockDownloader), client.WithImageFactory(mockImageFactory), client.WithFetcher(mockImageFetcher), client.WithDockerClient(mockDockerClient), client.WithBuildpackDownloader(mockBuildpackDownloader), ) h.AssertNil(t, err) mockDockerClient.EXPECT().Info(context.TODO(), gomock.Any()).Return(dockerclient.SystemInfoResult{Info: mobysystem.Info{OSType: "linux"}}, nil).AnyTimes() opts = client.CreateBuilderOptions{ RelativeBaseDir: "/", BuilderName: "some/builder", Config: pubbldr.Config{ Description: "Some description", Buildpacks: []pubbldr.ModuleConfig{ { ModuleInfo: dist.ModuleInfo{ID: "bp.one", Version: "1.2.3", Homepage: "http://one.buildpack"}, ImageOrURI: dist.ImageOrURI{ BuildpackURI: dist.BuildpackURI{ URI: "https://example.fake/bp-one.tgz", }, }, }, }, Order: []dist.OrderEntry{{ Group: []dist.ModuleRef{ {ModuleInfo: dist.ModuleInfo{ID: "bp.one", Version: "1.2.3"}, Optional: false}, }}, }, Stack: pubbldr.StackConfig{ ID: "some.stack.id", }, Run: pubbldr.RunConfig{ Images: []pubbldr.RunImageConfig{{ Image: "some/run-image", Mirrors: []string{"localhost:5000/some/run-image"}, }}, }, Build: pubbldr.BuildConfig{ Image: "some/build-image", }, Lifecycle: pubbldr.LifecycleConfig{URI: "file:///some-lifecycle"}, }, Publish: false, PullPolicy: image.PullAlways, } tmpDir, err = os.MkdirTemp("", "create-builder-test") h.AssertNil(t, err) }) it.After(func() { mockController.Finish() h.AssertNil(t, os.RemoveAll(tmpDir)) }) var successfullyCreateBuilder = func() *builder.Builder { t.Helper() err := subject.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) h.AssertEq(t, fakeBuildImage.IsSaved(), true) bldr, err := builder.FromImage(fakeBuildImage) h.AssertNil(t, err) return bldr } when("validating the builder config", func() { it("should not fail when the stack ID is empty", func() { opts.Config.Stack.ID = "" prepareFetcherWithBuildImage() prepareFetcherWithRunImages() err := subject.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) }) it("should fail when the stack ID from the builder config does not match the stack ID from the build image", func() { h.AssertNil(t, fakeBuildImage.SetLabel("io.buildpacks.stack.id", "other.stack.id")) prepareFetcherWithBuildImage() prepareFetcherWithRunImages() err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "stack 'some.stack.id' from builder config is incompatible with stack 'other.stack.id' from build image") }) it("should not fail when the stack is empty", func() { opts.Config.Stack.ID = "" opts.Config.Stack.BuildImage = "" opts.Config.Stack.RunImage = "" prepareFetcherWithBuildImage() prepareFetcherWithRunImages() err := subject.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) }) it("should fail when the run images and stack are empty", func() { opts.Config.Stack.BuildImage = "" opts.Config.Stack.RunImage = "" opts.Config.Run = pubbldr.RunConfig{} err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "run.images are required") }) it("should fail when the run images image and stack are empty", func() { opts.Config.Stack.BuildImage = "" opts.Config.Stack.RunImage = "" opts.Config.Run = pubbldr.RunConfig{ Images: []pubbldr.RunImageConfig{{}}, } err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "run.images.image is required") }) it("should fail if stack and run image are different", func() { opts.Config.Stack.RunImage = "some-other-stack-run-image" err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "run.images and stack.run-image do not match") }) it("should fail if stack and build image are different", func() { opts.Config.Stack.BuildImage = "some-other-stack-build-image" err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "build.image and stack.build-image do not match") }) it("should fail when lifecycle version is not a semver", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() opts.Config.Lifecycle.URI = "" opts.Config.Lifecycle.Version = "not-semver" err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "'lifecycle.version' must be a valid semver") }) it("should fail when both lifecycle version and uri are present", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() opts.Config.Lifecycle.URI = "file://some-lifecycle" opts.Config.Lifecycle.Version = "something" err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "'lifecycle' can only declare 'version' or 'uri', not both") }) it("should fail when buildpack ID does not match downloaded buildpack", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() opts.Config.Buildpacks[0].ID = "does.not.match" err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "buildpack from URI 'https://example.fake/bp-one.tgz' has ID 'bp.one' which does not match ID 'does.not.match' from builder config") }) it("should fail when buildpack version does not match downloaded buildpack", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() opts.Config.Buildpacks[0].Version = "0.0.0" err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "buildpack from URI 'https://example.fake/bp-one.tgz' has version '1.2.3' which does not match version '0.0.0' from builder config") }) it("should fail when extension ID does not match downloaded extension", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() prepareExtensions() opts.Config.Extensions[0].ID = "does.not.match" err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "extension from URI 'https://example.fake/ext-one.tgz' has ID 'ext.one' which does not match ID 'does.not.match' from builder config") }) it("should fail when extension version does not match downloaded extension", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() prepareExtensions() opts.Config.Extensions[0].Version = "0.0.0" err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "extension from URI 'https://example.fake/ext-one.tgz' has version '1.2.3' which does not match version '0.0.0' from builder config") }) }) when("validating the run image config", func() { it("should fail when the stack ID from the builder config does not match the stack ID from the run image", func() { prepareFetcherWithRunImages() h.AssertNil(t, fakeRunImage.SetLabel("io.buildpacks.stack.id", "other.stack.id")) err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "stack 'some.stack.id' from builder config is incompatible with stack 'other.stack.id' from run image 'some/run-image'") }) it("should fail when the stack ID from the builder config does not match the stack ID from the run image mirrors", func() { prepareFetcherWithRunImages() h.AssertNil(t, fakeRunImageMirror.SetLabel("io.buildpacks.stack.id", "other.stack.id")) err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "stack 'some.stack.id' from builder config is incompatible with stack 'other.stack.id' from run image 'localhost:5000/some/run-image'") }) it("should warn when the run image cannot be found", func() { mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(fakeBuildImage, nil) mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/run-image", image.FetchOptions{Daemon: false, PullPolicy: image.PullAlways}).Return(nil, errors.Wrap(image.ErrNotFound, "yikes")) mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/run-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(nil, errors.Wrap(image.ErrNotFound, "yikes")) mockImageFetcher.EXPECT().Fetch(gomock.Any(), "localhost:5000/some/run-image", image.FetchOptions{Daemon: false, PullPolicy: image.PullAlways}).Return(nil, errors.Wrap(image.ErrNotFound, "yikes")) mockImageFetcher.EXPECT().Fetch(gomock.Any(), "localhost:5000/some/run-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(nil, errors.Wrap(image.ErrNotFound, "yikes")) err := subject.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) h.AssertContains(t, out.String(), "Warning: run image 'some/run-image' is not accessible") }) it("should fail when not publish and the run image cannot be fetched", func() { mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/run-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(nil, errors.New("yikes")) err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "failed to fetch image: yikes") }) it("should fail when publish and the run image cannot be fetched", func() { mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/run-image", image.FetchOptions{Daemon: false, PullPolicy: image.PullAlways}).Return(nil, errors.New("yikes")) opts.Publish = true err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "failed to fetch image: yikes") }) it("should fail when the run image isn't a valid image", func() { fakeImage := fakeBadImageStruct{} mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/run-image", gomock.Any()).Return(fakeImage, nil).AnyTimes() mockImageFetcher.EXPECT().Fetch(gomock.Any(), "localhost:5000/some/run-image", gomock.Any()).Return(fakeImage, nil).AnyTimes() err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "failed to label image") }) when("publish is true", func() { it("should only try to validate the remote run image", func() { mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", image.FetchOptions{Daemon: true}).Times(0) mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/run-image", image.FetchOptions{Daemon: true}).Times(0) mockImageFetcher.EXPECT().Fetch(gomock.Any(), "localhost:5000/some/run-image", image.FetchOptions{Daemon: true}).Times(0) mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", image.FetchOptions{Daemon: false}).Return(fakeBuildImage, nil) mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/run-image", image.FetchOptions{Daemon: false}).Return(fakeRunImage, nil) mockImageFetcher.EXPECT().Fetch(gomock.Any(), "localhost:5000/some/run-image", image.FetchOptions{Daemon: false}).Return(fakeRunImageMirror, nil) opts.Publish = true err := subject.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) }) }) }) when("creating the base builder", func() { when("build image not found", func() { it("should fail", func() { prepareFetcherWithRunImages() mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(nil, image.ErrNotFound) err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "fetch build image: not found") }) }) when("build image isn't a valid image", func() { it("should fail", func() { fakeImage := fakeBadImageStruct{} prepareFetcherWithRunImages() mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(fakeImage, nil) err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "failed to create builder: invalid build-image") }) }) when("windows containers", func() { when("experimental enabled", func() { it("succeeds", func() { opts.Config.Extensions = nil // TODO: downloading extensions doesn't work yet; to be implemented in https://github.com/buildpacks/pack/issues/1489 opts.Config.OrderExtensions = nil // TODO: downloading extensions doesn't work yet; to be implemented in https://github.com/buildpacks/pack/issues/1489 packClientWithExperimental, err := client.NewClient( client.WithLogger(logger), client.WithDownloader(mockDownloader), client.WithImageFactory(mockImageFactory), client.WithFetcher(mockImageFetcher), client.WithExperimental(true), ) h.AssertNil(t, err) prepareFetcherWithRunImages() h.AssertNil(t, fakeBuildImage.SetOS("windows")) mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(fakeBuildImage, nil) err = packClientWithExperimental.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) }) }) when("experimental disabled", func() { it("fails", func() { prepareFetcherWithRunImages() h.AssertNil(t, fakeBuildImage.SetOS("windows")) mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", gomock.Any()).Return(fakeBuildImage, nil) err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "failed to create builder: Windows containers support is currently experimental.") }) }) }) when("error downloading lifecycle", func() { it("should fail", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() opts.Config.Lifecycle.URI = "fake" uri, err := paths.FilePathToURI(opts.Config.Lifecycle.URI, opts.RelativeBaseDir) h.AssertNil(t, err) mockDownloader.EXPECT().Download(gomock.Any(), uri).Return(nil, errors.New("error here")).AnyTimes() err = subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "downloading lifecycle") }) }) when("lifecycle isn't a valid lifecycle", func() { it("should fail", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() opts.Config.Lifecycle.URI = "fake" uri, err := paths.FilePathToURI(opts.Config.Lifecycle.URI, opts.RelativeBaseDir) h.AssertNil(t, err) mockDownloader.EXPECT().Download(gomock.Any(), uri).Return(blob.NewBlob(filepath.Join("testdata", "empty-file")), nil).AnyTimes() err = subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "invalid lifecycle") }) }) }) when("validating lifecycle Platform API for extensions", func() { when("lifecycle supports Platform API >= 0.13", func() { it("should allow extensions without experimental flag", func() { // Uses default lifecycle which has Platform API 0.13 prepareFetcherWithBuildImage() prepareFetcherWithRunImages() opts.Config.Lifecycle.URI = "file:///some-lifecycle-platform-0-13" err := subject.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) }) }) when("lifecycle supports Platform API < 0.13", func() { when("experimental flag is not set", func() { it("should fail when builder has extensions", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() prepareExtensions() // Override to use lifecycle with Platform API 0.3 (< 0.13) for this test opts.Config.Lifecycle.URI = "file:///some-lifecycle" err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "support for image extensions with Platform API < 0.13 is currently experimental") }) }) when("experimental flag is set", func() { it("should succeed when builder has extensions", func() { packClientWithExperimental, err := client.NewClient( client.WithLogger(logger), client.WithDownloader(mockDownloader), client.WithImageFactory(mockImageFactory), client.WithFetcher(mockImageFetcher), client.WithDockerClient(mockDockerClient), client.WithBuildpackDownloader(mockBuildpackDownloader), client.WithExperimental(true), ) h.AssertNil(t, err) prepareFetcherWithBuildImage() prepareFetcherWithRunImages() prepareExtensions() // Remove buildpacks to avoid API compatibility issues opts.Config.Buildpacks = nil opts.Config.Order = nil // Override to use lifecycle with Platform API 0.3 (< 0.13) for this test opts.Config.Lifecycle.URI = "file:///some-lifecycle" err = packClientWithExperimental.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) }) }) }) when("builder has no extensions", func() { it("should succeed regardless of Platform API version", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() // Remove extensions from config opts.Config.Extensions = nil opts.Config.OrderExtensions = nil // Use lifecycle with Platform API 0.3 (< 0.13) opts.Config.Lifecycle.URI = "file:///some-lifecycle" err := subject.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) }) }) }) when("only lifecycle version is provided", func() { it("should download from predetermined uri", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() opts.Config.Lifecycle.URI = "" opts.Config.Lifecycle.Version = "3.4.5" mockDownloader.EXPECT().Download( gomock.Any(), "https://github.com/buildpacks/lifecycle/releases/download/v3.4.5/lifecycle-v3.4.5+linux.x86-64.tgz", ).Return( blob.NewBlob(filepath.Join("testdata", "lifecycle", "platform-0.4")), nil, ) err := subject.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) }) it("should download from predetermined uri for arm64", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() opts.Config.Lifecycle.URI = "" opts.Config.Lifecycle.Version = "3.4.5" h.AssertNil(t, fakeBuildImage.SetArchitecture("arm64")) mockDownloader.EXPECT().Download( gomock.Any(), "https://github.com/buildpacks/lifecycle/releases/download/v3.4.5/lifecycle-v3.4.5+linux.arm64.tgz", ).Return( blob.NewBlob(filepath.Join("testdata", "lifecycle", "platform-0.4")), nil, ) err := subject.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) }) when("windows", func() { it("should download from predetermined uri", func() { opts.Config.Extensions = nil // TODO: downloading extensions doesn't work yet; to be implemented in https://github.com/buildpacks/pack/issues/1489 opts.Config.OrderExtensions = nil // TODO: downloading extensions doesn't work yet; to be implemented in https://github.com/buildpacks/pack/issues/1489 packClientWithExperimental, err := client.NewClient( client.WithLogger(logger), client.WithDownloader(mockDownloader), client.WithImageFactory(mockImageFactory), client.WithFetcher(mockImageFetcher), client.WithExperimental(true), ) h.AssertNil(t, err) prepareFetcherWithBuildImage() prepareFetcherWithRunImages() opts.Config.Lifecycle.URI = "" opts.Config.Lifecycle.Version = "3.4.5" h.AssertNil(t, fakeBuildImage.SetOS("windows")) mockDownloader.EXPECT().Download( gomock.Any(), "https://github.com/buildpacks/lifecycle/releases/download/v3.4.5/lifecycle-v3.4.5+windows.x86-64.tgz", ).Return( blob.NewBlob(filepath.Join("testdata", "lifecycle", "platform-0.4")), nil, ) err = packClientWithExperimental.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) }) }) }) when("no lifecycle version or URI is provided", func() { it("should download default lifecycle", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() opts.Config.Lifecycle.URI = "" opts.Config.Lifecycle.Version = "" mockDownloader.EXPECT().Download( gomock.Any(), fmt.Sprintf( "https://github.com/buildpacks/lifecycle/releases/download/v%s/lifecycle-v%s+linux.x86-64.tgz", builder.DefaultLifecycleVersion, builder.DefaultLifecycleVersion, ), ).Return( blob.NewBlob(filepath.Join("testdata", "lifecycle", "platform-0.4")), nil, ) err := subject.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) }) it("should download default lifecycle on arm64", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() opts.Config.Lifecycle.URI = "" opts.Config.Lifecycle.Version = "" h.AssertNil(t, fakeBuildImage.SetArchitecture("arm64")) mockDownloader.EXPECT().Download( gomock.Any(), fmt.Sprintf( "https://github.com/buildpacks/lifecycle/releases/download/v%s/lifecycle-v%s+linux.arm64.tgz", builder.DefaultLifecycleVersion, builder.DefaultLifecycleVersion, ), ).Return( blob.NewBlob(filepath.Join("testdata", "lifecycle", "platform-0.4")), nil, ) err := subject.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) }) when("windows", func() { it("should download default lifecycle", func() { opts.Config.Extensions = nil // TODO: downloading extensions doesn't work yet; to be implemented in https://github.com/buildpacks/pack/issues/1489 opts.Config.OrderExtensions = nil // TODO: downloading extensions doesn't work yet; to be implemented in https://github.com/buildpacks/pack/issues/1489 packClientWithExperimental, err := client.NewClient( client.WithLogger(logger), client.WithDownloader(mockDownloader), client.WithImageFactory(mockImageFactory), client.WithFetcher(mockImageFetcher), client.WithExperimental(true), ) h.AssertNil(t, err) prepareFetcherWithBuildImage() prepareFetcherWithRunImages() opts.Config.Lifecycle.URI = "" opts.Config.Lifecycle.Version = "" h.AssertNil(t, fakeBuildImage.SetOS("windows")) mockDownloader.EXPECT().Download( gomock.Any(), fmt.Sprintf( "https://github.com/buildpacks/lifecycle/releases/download/v%s/lifecycle-v%s+windows.x86-64.tgz", builder.DefaultLifecycleVersion, builder.DefaultLifecycleVersion, ), ).Return( blob.NewBlob(filepath.Join("testdata", "lifecycle", "platform-0.4")), nil, ) err = packClientWithExperimental.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) }) }) }) when("lifecycle URI is a docker image", func() { var lifecycleImageName = "buildpacksio/lifecycle:latest" setupFakeLifecycleImage := func() *h.FakeWithUnderlyingImage { // Write the tar content to a file in tmpDir lifecycleLayerPath := filepath.Join("testdata", "lifecycle", "lifecycle.tar") lifecycleTag, err := name.NewTag(lifecycleImageName) h.AssertNil(t, err) v1LifecycleImage, err := tarball.ImageFromPath(lifecycleLayerPath, &lifecycleTag) h.AssertNil(t, err) return h.NewFakeWithUnderlyingV1Image(lifecycleImageName, nil, v1LifecycleImage) } it("should download lifecycle from docker registry", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() fakeLifecycleImage := setupFakeLifecycleImage() opts.Config.Lifecycle.URI = "docker://" + lifecycleImageName opts.Config.Lifecycle.Version = "" opts.RelativeBaseDir = tmpDir // Expect the image fetcher to fetch the lifecycle image mockImageFetcher.EXPECT().Fetch(gomock.Any(), lifecycleImageName, image.FetchOptions{Daemon: false}).Return(fakeLifecycleImage, nil) // Create the expected lifecycle.tar file that will be referenced lifecyclePath := filepath.Join(tmpDir, "lifecycle.tar") // The downloader will be called with the extracted lifecycle tar mockDownloader.EXPECT().Download(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, uri string) (blob.Blob, error) { // The URI should be a file:// URI pointing to lifecycle.tar h.AssertTrue(t, strings.Contains(uri, "lifecycle.tar")) // Write a minimal lifecycle tar for the test f, err := os.Create(lifecyclePath) h.AssertNil(t, err) defer f.Close() // Copy the test lifecycle content testLifecycle := blob.NewBlob(filepath.Join("testdata", "lifecycle", "platform-0.4")) return testLifecycle, nil }) err := subject.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) }) it("should handle docker URI without docker:// prefix", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() fakeLifecycleImage := setupFakeLifecycleImage() opts.Config.Lifecycle.URI = "docker:/" + lifecycleImageName opts.Config.Lifecycle.Version = "" opts.RelativeBaseDir = tmpDir // Expect the image fetcher to fetch the lifecycle image mockImageFetcher.EXPECT().Fetch(gomock.Any(), lifecycleImageName, image.FetchOptions{Daemon: false}).Return(fakeLifecycleImage, nil) // The downloader will be called with the extracted lifecycle tar mockDownloader.EXPECT().Download(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, uri string) (blob.Blob, error) { testLifecycle := blob.NewBlob(filepath.Join("testdata", "lifecycle", "platform-0.4")) return testLifecycle, nil }) err := subject.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) }) when("fetching lifecycle image fails", func() { it("should return an error", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() opts.Config.Lifecycle.URI = "docker://" + lifecycleImageName opts.Config.Lifecycle.Version = "" opts.RelativeBaseDir = tmpDir // Expect the image fetcher to fail mockImageFetcher.EXPECT().Fetch(gomock.Any(), lifecycleImageName, image.FetchOptions{Daemon: false}).Return(nil, errors.New("failed to fetch image")) err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "Could not parse uri from lifecycle image") }) }) when("lifecycle image has no layers", func() { it("should return an error", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() opts.Config.Lifecycle.URI = "docker://" + lifecycleImageName opts.Config.Lifecycle.Version = "" opts.RelativeBaseDir = tmpDir // Create an image with no layers emptyLifecycleImage := fakes.NewImage(lifecycleImageName, "", nil) mockImageFetcher.EXPECT().Fetch(gomock.Any(), lifecycleImageName, image.FetchOptions{Daemon: false}).Return(emptyLifecycleImage, nil) err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "Could not parse uri from lifecycle image") }) }) when("both lifecycle URI and version are provided", func() { it("should return an error", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() opts.Config.Lifecycle.URI = "docker://" + lifecycleImageName opts.Config.Lifecycle.Version = "1.2.3" err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "'lifecycle' can only declare 'version' or 'uri', not both") }) }) }) when("buildpack mixins are not satisfied", func() { it("should return an error", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() h.AssertNil(t, fakeBuildImage.SetLabel("io.buildpacks.stack.mixins", "")) err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "validating buildpacks: buildpack 'bp.one@1.2.3' requires missing mixin(s): build:mixinY, mixinX") }) }) when("creation succeeds", func() { it("should set basic metadata", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() bldr := successfullyCreateBuilder() h.AssertEq(t, bldr.Name(), "some/builder") h.AssertEq(t, bldr.Description(), "Some description") h.AssertEq(t, bldr.UID(), 1234) h.AssertEq(t, bldr.GID(), 4321) h.AssertEq(t, bldr.StackID, "some.stack.id") }) it("should set buildpack and order metadata", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() bldr := successfullyCreateBuilder() bpInfo := dist.ModuleInfo{ ID: "bp.one", Version: "1.2.3", Homepage: "http://one.buildpack", } h.AssertEq(t, bldr.Buildpacks(), []dist.ModuleInfo{bpInfo}) bpInfo.Homepage = "" h.AssertEq(t, bldr.Order(), dist.Order{{ Group: []dist.ModuleRef{{ ModuleInfo: bpInfo, Optional: false, }}, }}) }) it("should set extensions and order-extensions metadata", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() prepareExtensions() bldr := successfullyCreateBuilder() extInfo := dist.ModuleInfo{ ID: "ext.one", Version: "1.2.3", Homepage: "http://one.extension", } h.AssertEq(t, bldr.Extensions(), []dist.ModuleInfo{extInfo}) extInfo.Homepage = "" h.AssertEq(t, bldr.OrderExtensions(), dist.Order{{ Group: []dist.ModuleRef{{ ModuleInfo: extInfo, Optional: false, // extensions are always optional }}, }}) }) it("should embed the lifecycle", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() successfullyCreateBuilder() layerTar, err := fakeBuildImage.FindLayerWithPath("/cnb/lifecycle") h.AssertNil(t, err) h.AssertTarHasFile(t, layerTar, "/cnb/lifecycle/detector") h.AssertTarHasFile(t, layerTar, "/cnb/lifecycle/restorer") h.AssertTarHasFile(t, layerTar, "/cnb/lifecycle/analyzer") h.AssertTarHasFile(t, layerTar, "/cnb/lifecycle/builder") h.AssertTarHasFile(t, layerTar, "/cnb/lifecycle/exporter") h.AssertTarHasFile(t, layerTar, "/cnb/lifecycle/launcher") }) it("should set lifecycle descriptor", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() bldr := successfullyCreateBuilder() h.AssertEq(t, bldr.LifecycleDescriptor().Info.Version.String(), "0.0.0") //nolint:staticcheck h.AssertEq(t, bldr.LifecycleDescriptor().API.BuildpackVersion.String(), "0.2") //nolint:staticcheck h.AssertEq(t, bldr.LifecycleDescriptor().API.PlatformVersion.String(), "0.2") h.AssertEq(t, bldr.LifecycleDescriptor().APIs.Buildpack.Deprecated.AsStrings(), []string{"0.2", "0.3"}) h.AssertEq(t, bldr.LifecycleDescriptor().APIs.Buildpack.Supported.AsStrings(), []string{"0.2", "0.3", "0.4", "0.9"}) h.AssertEq(t, bldr.LifecycleDescriptor().APIs.Platform.Deprecated.AsStrings(), []string{"0.2"}) h.AssertEq(t, bldr.LifecycleDescriptor().APIs.Platform.Supported.AsStrings(), []string{"0.3", "0.4"}) }) it("should warn when deprecated Buildpack API version is used", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() prepareExtensions() bldr := successfullyCreateBuilder() h.AssertEq(t, bldr.LifecycleDescriptor().APIs.Buildpack.Deprecated.AsStrings(), []string{"0.2", "0.3"}) h.AssertContains(t, out.String(), fmt.Sprintf("Buildpack %s is using deprecated Buildpacks API version %s", style.Symbol("bp.one@1.2.3"), style.Symbol("0.3"))) h.AssertContains(t, out.String(), fmt.Sprintf("Extension %s is using deprecated Buildpacks API version %s", style.Symbol("ext.one@1.2.3"), style.Symbol("0.3"))) }) it("shouldn't warn when Buildpack API version used isn't deprecated", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() prepareExtensions() opts.Config.Buildpacks[0].URI = "https://example.fake/bp-one-with-api-4.tgz" opts.Config.Extensions[0].URI = "https://example.fake/ext-one-with-api-9.tgz" buildpackBlob := blob.NewBlob(filepath.Join("testdata", "buildpack-api-0.4")) bp, err := buildpack.FromBuildpackRootBlob(buildpackBlob, archive.DefaultTarWriterFactory(), nil) h.AssertNil(t, err) mockBuildpackDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/bp-one-with-api-4.tgz", gomock.Any()).Return(bp, nil, nil) extensionBlob := blob.NewBlob(filepath.Join("testdata", "extension-api-0.9")) extension, err := buildpack.FromExtensionRootBlob(extensionBlob, archive.DefaultTarWriterFactory(), nil) h.AssertNil(t, err) mockBuildpackDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/ext-one-with-api-9.tgz", gomock.Any()).Return(extension, nil, nil) bldr := successfullyCreateBuilder() h.AssertEq(t, bldr.LifecycleDescriptor().APIs.Buildpack.Deprecated.AsStrings(), []string{"0.2", "0.3"}) h.AssertNotContains(t, out.String(), "is using deprecated Buildpacks API version") }) it("should set labels", func() { opts.Labels = map[string]string{"test.label.one": "1", "test.label.two": "2"} prepareFetcherWithBuildImage() prepareFetcherWithRunImages() err := subject.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) imageLabels, err := fakeBuildImage.Labels() h.AssertNil(t, err) h.AssertEq(t, imageLabels["test.label.one"], "1") h.AssertEq(t, imageLabels["test.label.two"], "2") }) when("Buildpack dependencies are provided", func() { var ( bp1v1 buildpack.BuildModule bp1v2 buildpack.BuildModule bp2v1 buildpack.BuildModule bp2v2 buildpack.BuildModule fakeLayerImage *h.FakeAddedLayerImage err error ) it.Before(func() { fakeLayerImage = &h.FakeAddedLayerImage{Image: fakeBuildImage} }) var prepareBuildpackDependencies = func() []buildpack.BuildModule { bp1v1Blob := blob.NewBlob(filepath.Join("testdata", "buildpack-non-deterministic", "buildpack-1-version-1")) bp1v2Blob := blob.NewBlob(filepath.Join("testdata", "buildpack-non-deterministic", "buildpack-1-version-2")) bp2v1Blob := blob.NewBlob(filepath.Join("testdata", "buildpack-non-deterministic", "buildpack-2-version-1")) bp2v2Blob := blob.NewBlob(filepath.Join("testdata", "buildpack-non-deterministic", "buildpack-2-version-2")) bp1v1, err = buildpack.FromBuildpackRootBlob(bp1v1Blob, archive.DefaultTarWriterFactory(), nil) h.AssertNil(t, err) bp1v2, err = buildpack.FromBuildpackRootBlob(bp1v2Blob, archive.DefaultTarWriterFactory(), nil) h.AssertNil(t, err) bp2v1, err = buildpack.FromBuildpackRootBlob(bp2v1Blob, archive.DefaultTarWriterFactory(), nil) h.AssertNil(t, err) bp2v2, err = buildpack.FromBuildpackRootBlob(bp2v2Blob, archive.DefaultTarWriterFactory(), nil) h.AssertNil(t, err) return []buildpack.BuildModule{bp2v2, bp2v1, bp1v1, bp1v2} } var successfullyCreateDeterministicBuilder = func() { t.Helper() err := subject.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) h.AssertEq(t, fakeLayerImage.IsSaved(), true) } it("should add dependencies buildpacks layers order by ID and version", func() { mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", gomock.Any()).Return(fakeLayerImage, nil) prepareFetcherWithRunImages() prepareExtensions() opts.Config.Buildpacks[0].URI = "https://example.fake/bp-one-with-api-4.tgz" opts.Config.Extensions[0].URI = "https://example.fake/ext-one-with-api-9.tgz" bpDependencies := prepareBuildpackDependencies() buildpackBlob := blob.NewBlob(filepath.Join("testdata", "buildpack-api-0.4")) bp, err := buildpack.FromBuildpackRootBlob(buildpackBlob, archive.DefaultTarWriterFactory(), nil) h.AssertNil(t, err) mockBuildpackDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/bp-one-with-api-4.tgz", gomock.Any()).DoAndReturn( func(ctx context.Context, buildpackURI string, opts buildpack.DownloadOptions) (buildpack.BuildModule, []buildpack.BuildModule, error) { // test options h.AssertEq(t, opts.Target.ValuesAsPlatform(), "linux/amd64") return bp, bpDependencies, nil }) extensionBlob := blob.NewBlob(filepath.Join("testdata", "extension-api-0.9")) extension, err := buildpack.FromExtensionRootBlob(extensionBlob, archive.DefaultTarWriterFactory(), nil) h.AssertNil(t, err) mockBuildpackDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/ext-one-with-api-9.tgz", gomock.Any()).DoAndReturn( func(ctx context.Context, buildpackURI string, opts buildpack.DownloadOptions) (buildpack.BuildModule, []buildpack.BuildModule, error) { // test options h.AssertEq(t, opts.Target.ValuesAsPlatform(), "linux/amd64") return extension, nil, nil }) successfullyCreateDeterministicBuilder() layers := fakeLayerImage.AddedLayersOrder() // Main buildpack + 4 dependencies + 1 extension h.AssertEq(t, len(layers), 6) // [0] bp.one.1.2.3.tar - main buildpack h.AssertTrue(t, strings.Contains(layers[1], h.LayerFileName(bp1v1))) h.AssertTrue(t, strings.Contains(layers[2], h.LayerFileName(bp1v2))) h.AssertTrue(t, strings.Contains(layers[3], h.LayerFileName(bp2v1))) h.AssertTrue(t, strings.Contains(layers[4], h.LayerFileName(bp2v2))) // [5] ext.one.1.2.3.tar - extension }) }) }) it("supports directory buildpacks", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() opts.RelativeBaseDir = "" directoryPath := "testdata/buildpack" opts.Config.Buildpacks[0].URI = directoryPath buildpackBlob := blob.NewBlob(directoryPath) buildpack, err := buildpack.FromBuildpackRootBlob(buildpackBlob, archive.DefaultTarWriterFactory(), nil) h.AssertNil(t, err) mockBuildpackDownloader.EXPECT().Download(gomock.Any(), directoryPath, gomock.Any()).Return(buildpack, nil, nil) err = subject.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) }) it("supports directory extensions", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() prepareExtensions() opts.RelativeBaseDir = "" directoryPath := "testdata/extension" opts.Config.Extensions[0].URI = directoryPath extensionBlob := blob.NewBlob(directoryPath) extension, err := buildpack.FromExtensionRootBlob(extensionBlob, archive.DefaultTarWriterFactory(), nil) h.AssertNil(t, err) mockBuildpackDownloader.EXPECT().Download(gomock.Any(), directoryPath, gomock.Any()).Return(extension, nil, nil) err = subject.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) }) when("package file", func() { it.Before(func() { fileURI := func(path string) (original, uri string) { absPath, err := paths.FilePathToURI(path, "") h.AssertNil(t, err) return path, absPath } cnbFile, _ := fileURI(filepath.Join(tmpDir, "bp_one1.cnb")) buildpackPath, buildpackPathURI := fileURI(filepath.Join("testdata", "buildpack")) mockDownloader.EXPECT().Download(gomock.Any(), buildpackPathURI).Return(blob.NewBlob(buildpackPath), nil) h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{ Name: cnbFile, Config: pubbldpkg.Config{ Platform: dist.Platform{OS: "linux"}, Buildpack: dist.BuildpackURI{URI: buildpackPath}, }, Format: "file", })) buildpack, _, err := buildpack.BuildpacksFromOCILayoutBlob(blob.NewBlob(cnbFile)) h.AssertNil(t, err) mockBuildpackDownloader.EXPECT().Download(gomock.Any(), cnbFile, gomock.Any()).Return(buildpack, nil, nil).AnyTimes() opts.Config.Buildpacks = []pubbldr.ModuleConfig{{ ImageOrURI: dist.ImageOrURI{BuildpackURI: dist.BuildpackURI{URI: cnbFile}}, }} }) it("package file is valid", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() bldr := successfullyCreateBuilder() bpInfo := dist.ModuleInfo{ ID: "bp.one", Version: "1.2.3", Homepage: "http://one.buildpack", } h.AssertEq(t, bldr.Buildpacks(), []dist.ModuleInfo{bpInfo}) bpInfo.Homepage = "" h.AssertEq(t, bldr.Order(), dist.Order{{ Group: []dist.ModuleRef{{ ModuleInfo: bpInfo, Optional: false, }}, }}) }) }) when("packages", func() { when("package image lives in cnb registry", func() { when("publish=false and pull-policy=always", func() { it("should call BuildpackDownloader with the proper argumentss", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() opts.BuilderName = "some/builder" opts.Publish = false opts.PullPolicy = image.PullAlways opts.Registry = "some-registry" opts.Config.Buildpacks = append( opts.Config.Buildpacks, pubbldr.ModuleConfig{ ImageOrURI: dist.ImageOrURI{ BuildpackURI: dist.BuildpackURI{ URI: "urn:cnb:registry:example/foo@1.1.0", }, }, }, ) shouldCallBuildpackDownloaderWith("urn:cnb:registry:example/foo@1.1.0", buildpack.DownloadOptions{Daemon: true, PullPolicy: image.PullAlways, RegistryName: "some-"}) h.AssertNil(t, subject.CreateBuilder(context.TODO(), opts)) }) }) }) }) when("flatten option is set", func() { /* 1 * / \ * 2 3 * / \ * 4 5 * / \ * 6 7 */ var ( fakeLayerImage *h.FakeAddedLayerImage err error ) var successfullyCreateFlattenBuilder = func() { t.Helper() err := subject.CreateBuilder(context.TODO(), opts) h.AssertNil(t, err) h.AssertEq(t, fakeLayerImage.IsSaved(), true) } it.Before(func() { fakeLayerImage = &h.FakeAddedLayerImage{Image: fakeBuildImage} mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", gomock.Any()).Return(fakeLayerImage, nil) var depBPs []buildpack.BuildModule blob1 := blob.NewBlob(filepath.Join("testdata", "buildpack-flatten", "buildpack-1")) for i := 2; i <= 7; i++ { b := blob.NewBlob(filepath.Join("testdata", "buildpack-flatten", fmt.Sprintf("buildpack-%d", i))) bp, err := buildpack.FromBuildpackRootBlob(b, archive.DefaultTarWriterFactory(), nil) h.AssertNil(t, err) depBPs = append(depBPs, bp) } mockDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/flatten-bp-1.tgz").Return(blob1, nil).AnyTimes() bp, err := buildpack.FromBuildpackRootBlob(blob1, archive.DefaultTarWriterFactory(), nil) h.AssertNil(t, err) mockBuildpackDownloader.EXPECT().Download(gomock.Any(), "https://example.fake/flatten-bp-1.tgz", gomock.Any()).Return(bp, depBPs, nil).AnyTimes() opts = client.CreateBuilderOptions{ RelativeBaseDir: "/", BuilderName: "some/builder", Config: pubbldr.Config{ Description: "Some description", Buildpacks: []pubbldr.ModuleConfig{ { ModuleInfo: dist.ModuleInfo{ID: "flatten/bp-1", Version: "1", Homepage: "http://buildpack-1"}, ImageOrURI: dist.ImageOrURI{ BuildpackURI: dist.BuildpackURI{ URI: "https://example.fake/flatten-bp-1.tgz", }, }, }, }, Order: []dist.OrderEntry{{ Group: []dist.ModuleRef{ {ModuleInfo: dist.ModuleInfo{ID: "flatten/bp-2", Version: "2"}, Optional: false}, {ModuleInfo: dist.ModuleInfo{ID: "flatten/bp-4", Version: "4"}, Optional: false}, {ModuleInfo: dist.ModuleInfo{ID: "flatten/bp-6", Version: "6"}, Optional: false}, {ModuleInfo: dist.ModuleInfo{ID: "flatten/bp-7", Version: "7"}, Optional: false}, }}, }, Stack: pubbldr.StackConfig{ ID: "some.stack.id", }, Run: pubbldr.RunConfig{ Images: []pubbldr.RunImageConfig{{ Image: "some/run-image", Mirrors: []string{"localhost:5000/some/run-image"}, }}, }, Build: pubbldr.BuildConfig{ Image: "some/build-image", }, Lifecycle: pubbldr.LifecycleConfig{URI: "file:///some-lifecycle"}, }, Publish: false, PullPolicy: image.PullAlways, } }) when("flatten all", func() { it("creates 1 layer for all buildpacks", func() { prepareFetcherWithRunImages() opts.Flatten, err = buildpack.ParseFlattenBuildModules([]string{"flatten/bp-1@1,flatten/bp-2@2,flatten/bp-4@4,flatten/bp-6@6,flatten/bp-7@7,flatten/bp-3@3,flatten/bp-5@5"}) h.AssertNil(t, err) successfullyCreateFlattenBuilder() layers := fakeLayerImage.AddedLayersOrder() h.AssertEq(t, len(layers), 1) }) }) when("only some modules are flattened", func() { it("creates 1 layer for buildpacks [1,2,3,4,5,6] and 1 layer for buildpack [7]", func() { prepareFetcherWithRunImages() opts.Flatten, err = buildpack.ParseFlattenBuildModules([]string{"flatten/bp-1@1,flatten/bp-2@2,flatten/bp-4@4,flatten/bp-6@6,flatten/bp-3@3,flatten/bp-5@5"}) h.AssertNil(t, err) successfullyCreateFlattenBuilder() layers := fakeLayerImage.AddedLayersOrder() h.AssertEq(t, len(layers), 2) }) it("creates 1 layer for buildpacks [1,2,3] and 1 layer for [4,5,6] and 1 layer for [7]", func() { prepareFetcherWithRunImages() opts.Flatten, err = buildpack.ParseFlattenBuildModules([]string{"flatten/bp-1@1,flatten/bp-2@2,flatten/bp-3@3", "flatten/bp-4@4,flatten/bp-6@6,flatten/bp-5@5"}) h.AssertNil(t, err) successfullyCreateFlattenBuilder() layers := fakeLayerImage.AddedLayersOrder() h.AssertEq(t, len(layers), 3) }) }) }) when("daemon target selection for multi-platform builders", func() { when("publish is false", func() { when("daemon is linux/amd64", func() { it.Before(func() { mockDockerClient.EXPECT().ServerVersion(gomock.Any(), gomock.Any()).Return(dockerclient.ServerVersionResult{ Os: "linux", Arch: "amd64", }, nil).AnyTimes() }) when("multiple targets are provided", func() { it("selects the matching OS and architecture", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() opts.Targets = []dist.Target{ {OS: "linux", Arch: "arm64"}, {OS: "linux", Arch: "amd64"}, // should match {OS: "windows", Arch: "amd64"}, } h.AssertNil(t, subject.CreateBuilder(context.TODO(), opts)) // Verify that only one image was created (for the matching target) h.AssertEq(t, fakeBuildImage.IsSaved(), true) }) }) when("no exact architecture match exists", func() { it("returns error", func() { opts.Targets = []dist.Target{ {OS: "linux", Arch: "arm64"}, {OS: "linux", Arch: "arm"}, } err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "could not find a target that matches daemon os=linux and architecture=amd64") }) }) when("target with empty architecture exists", func() { it("selects the OS-only match", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() opts.Targets = []dist.Target{ {OS: "linux", Arch: "arm64"}, {OS: "linux", Arch: ""}, // should match {OS: "windows", Arch: "amd64"}, } h.AssertNil(t, subject.CreateBuilder(context.TODO(), opts)) // Verify that the builder was created h.AssertEq(t, fakeBuildImage.IsSaved(), true) }) }) }) when("daemon is linux/arm64", func() { it.Before(func() { mockDockerClient.EXPECT().ServerVersion(gomock.Any(), gomock.Any()).Return(dockerclient.ServerVersionResult{ Os: "linux", Arch: "arm64", }, nil).AnyTimes() }) when("targets are ordered with amd64 first", func() { it("selects arm64 even when amd64 appears first", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() opts.Targets = []dist.Target{ {OS: "linux", Arch: "amd64"}, // appears first {OS: "linux", Arch: "arm64"}, // should match {OS: "windows", Arch: "arm64"}, } h.AssertNil(t, subject.CreateBuilder(context.TODO(), opts)) // Verify that the builder was created h.AssertEq(t, fakeBuildImage.IsSaved(), true) }) }) when("only amd64 targets available", func() { it("returns error", func() { opts.Targets = []dist.Target{ {OS: "linux", Arch: "amd64"}, {OS: "windows", Arch: "amd64"}, } err := subject.CreateBuilder(context.TODO(), opts) h.AssertError(t, err, "could not find a target that matches daemon os=linux and architecture=arm64") }) }) }) when("empty targets list", func() { it("creates builder without calling daemonTarget", func() { prepareFetcherWithBuildImage() prepareFetcherWithRunImages() // Empty targets should use the default behavior opts.Targets = []dist.Target{} // ServerVersion should NOT be called for empty targets mockDockerClient.EXPECT().ServerVersion(gomock.Any(), gomock.Any()).Times(0) h.AssertNil(t, subject.CreateBuilder(context.TODO(), opts)) // Verify that the builder was created h.AssertEq(t, fakeBuildImage.IsSaved(), true) }) }) }) }) }) } type fakeBadImageStruct struct { *fakes.Image } func (i fakeBadImageStruct) Name() string { return "fake image" } func (i fakeBadImageStruct) Label(str string) (string, error) { return "", errors.New("error here") }