pack/pkg/client/create_builder_test.go

1466 lines
55 KiB
Go

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")
}