diff --git a/internal/build/docker.go b/internal/build/docker.go index 6fd7d54fe..64db04d5d 100644 --- a/internal/build/docker.go +++ b/internal/build/docker.go @@ -23,6 +23,8 @@ type DockerClient interface { ContainerInspect(ctx context.Context, container string) (types.ContainerJSON, error) ContainerRemove(ctx context.Context, container string, options containertypes.RemoveOptions) error CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error + NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) + NetworkRemove(ctx context.Context, network string) error } var _ DockerClient = dockerClient.CommonAPIClient(nil) diff --git a/internal/build/lifecycle_execution.go b/internal/build/lifecycle_execution.go index 2f017dd70..bac6a53b9 100644 --- a/internal/build/lifecycle_execution.go +++ b/internal/build/lifecycle_execution.go @@ -12,6 +12,7 @@ import ( "github.com/buildpacks/lifecycle/api" "github.com/buildpacks/lifecycle/auth" "github.com/buildpacks/lifecycle/platform/files" + "github.com/docker/docker/api/types" "github.com/google/go-containerregistry/pkg/name" "github.com/pkg/errors" "golang.org/x/sync/errgroup" @@ -165,6 +166,7 @@ func (l *LifecycleExecution) PrevImageName() string { func (l *LifecycleExecution) Run(ctx context.Context, phaseFactoryCreator PhaseFactoryCreator) error { phaseFactory := phaseFactoryCreator(l) + var buildCache Cache if l.opts.CacheImage != "" || (l.opts.Cache.Build.Format == cache.CacheImage) { cacheImageName := l.opts.CacheImage @@ -196,6 +198,29 @@ func (l *LifecycleExecution) Run(ctx context.Context, phaseFactoryCreator PhaseF launchCache := cache.NewVolumeCache(l.opts.Image, l.opts.Cache.Launch, "launch", l.docker) + if l.opts.Network == "" { + // start an ephemeral bridge network + driver := "bridge" + if l.os == "windows" { + driver = "nat" + } + networkName := fmt.Sprintf("pack.local/network/%x", randString(10)) + resp, err := l.docker.NetworkCreate(ctx, networkName, types.NetworkCreate{ + Driver: driver, + }) + if err != nil { + return fmt.Errorf("failed to create ephemeral %s network: %w", driver, err) + } + defer func() { + _ = l.docker.NetworkRemove(ctx, networkName) + }() + l.logger.Debugf("Created ephemeral bridge network %s with ID %s", networkName, resp.ID) + if resp.Warning != "" { + l.logger.Warn(resp.Warning) + } + l.opts.Network = networkName + } + if !l.opts.UseCreator { if l.platformAPI.LessThan("0.7") { l.logger.Info(style.Step("DETECTING")) diff --git a/internal/build/lifecycle_execution_test.go b/internal/build/lifecycle_execution_test.go index 6aa126a25..4ab807712 100644 --- a/internal/build/lifecycle_execution_test.go +++ b/internal/build/lifecycle_execution_test.go @@ -16,6 +16,7 @@ import ( ifakes "github.com/buildpacks/imgutil/fakes" "github.com/buildpacks/lifecycle/api" "github.com/buildpacks/lifecycle/platform/files" + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/google/go-containerregistry/pkg/authn" @@ -275,7 +276,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { fakeBuilder *fakes.FakeBuilder outBuf bytes.Buffer logger *logging.LogWithWriters - docker *client.Client + docker *fakeDockerClient fakeTermui *fakes.FakeTermui ) @@ -289,7 +290,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { fakeBuilder, err = fakes.NewFakeBuilder(fakes.WithSupportedPlatformAPIs([]*api.Version{api.MustParse("0.3")})) h.AssertNil(t, err) logger = logging.NewLogWithWriters(&outBuf, &outBuf) - docker, err = client.NewClientWithOpts(client.FromEnv, client.WithVersion("1.38")) + docker = &fakeDockerClient{} h.AssertNil(t, err) fakePhaseFactory = fakes.NewFakePhaseFactory() }) @@ -780,6 +781,46 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { }) }) + when("network is not provided", func() { + it("creates an ephemeral bridge network", func() { + beforeNetworks := func() int { + networks, err := docker.NetworkList(context.Background(), types.NetworkListOptions{}) + h.AssertNil(t, err) + return len(networks) + }() + + opts := build.LifecycleOptions{ + Image: imageName, + Builder: fakeBuilder, + Termui: fakeTermui, + } + + lifecycle, err := build.NewLifecycleExecution(logger, docker, "some-temp-dir", opts) + h.AssertNil(t, err) + + err = lifecycle.Run(context.Background(), func(execution *build.LifecycleExecution) build.PhaseFactory { + return fakePhaseFactory + }) + h.AssertNil(t, err) + + for _, entry := range fakePhaseFactory.NewCalledWithProvider { + h.AssertContains(t, string(entry.HostConfig().NetworkMode), "pack.local/network/") + h.AssertEq(t, entry.HostConfig().NetworkMode.IsDefault(), false) + h.AssertEq(t, entry.HostConfig().NetworkMode.IsHost(), false) + h.AssertEq(t, entry.HostConfig().NetworkMode.IsNone(), false) + h.AssertEq(t, entry.HostConfig().NetworkMode.IsPrivate(), true) + h.AssertEq(t, entry.HostConfig().NetworkMode.IsUserDefined(), true) + } + + afterNetworks := func() int { + networks, err := docker.NetworkList(context.Background(), types.NetworkListOptions{}) + h.AssertNil(t, err) + return len(networks) + }() + h.AssertEq(t, beforeNetworks, afterNetworks) + }) + }) + when("Error cases", func() { when("passed invalid", func() { it("fails for cache-image", func() { @@ -2657,6 +2698,26 @@ func (f *fakeImageFetcher) fetchRunImage(name string) error { return nil } +type fakeDockerClient struct { + nNetworks int + build.DockerClient +} + +func (f *fakeDockerClient) NetworkList(ctx context.Context, opts types.NetworkListOptions) ([]types.NetworkResource, error) { + ret := make([]types.NetworkResource, f.nNetworks) + return ret, nil +} + +func (f *fakeDockerClient) NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) { + f.nNetworks++ + return types.NetworkCreateResponse{}, nil +} + +func (f *fakeDockerClient) NetworkRemove(ctx context.Context, network string) error { + f.nNetworks-- + return nil +} + func newTestLifecycleExecErr(t *testing.T, logVerbose bool, tmpDir string, ops ...func(*build.LifecycleOptions)) (*build.LifecycleExecution, error) { docker, err := client.NewClientWithOpts(client.FromEnv, client.WithVersion("1.38")) h.AssertNil(t, err) diff --git a/pkg/client/docker.go b/pkg/client/docker.go index 11b0fe1c0..d5a020f47 100644 --- a/pkg/client/docker.go +++ b/pkg/client/docker.go @@ -32,4 +32,6 @@ type DockerClient interface { ContainerWait(ctx context.Context, container string, condition containertypes.WaitCondition) (<-chan containertypes.WaitResponse, <-chan error) ContainerAttach(ctx context.Context, container string, options containertypes.AttachOptions) (types.HijackedResponse, error) ContainerStart(ctx context.Context, container string, options containertypes.StartOptions) error + NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) + NetworkRemove(ctx context.Context, network string) error }