diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java index ebbca2b0534..39218e18f4b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java @@ -30,6 +30,7 @@ import org.springframework.boot.buildpack.platform.docker.configuration.Resolved import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException; import org.springframework.boot.buildpack.platform.docker.type.Binding; import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageArchive; import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.io.IOBiConsumer; @@ -114,16 +115,10 @@ public class Builder { Buildpacks buildpacks = getBuildpacks(request, imageFetcher, builderMetadata, buildpackLayersMetadata); EphemeralBuilder ephemeralBuilder = new EphemeralBuilder(buildOwner, builderImage, request.getName(), builderMetadata, request.getCreator(), request.getEnv(), buildpacks); - this.docker.image().load(ephemeralBuilder.getArchive(), UpdateListener.none()); - try { - executeLifecycle(request, ephemeralBuilder); - tagImage(request.getName(), request.getTags()); - if (request.isPublish()) { - pushImages(request.getName(), request.getTags()); - } - } - finally { - this.docker.image().remove(ephemeralBuilder.getName(), true); + executeLifecycle(request, ephemeralBuilder); + tagImage(request.getName(), request.getTags()); + if (request.isPublish()) { + pushImages(request.getName(), request.getTags()); } } @@ -169,13 +164,25 @@ public class Builder { } private void executeLifecycle(BuildRequest request, EphemeralBuilder builder) throws IOException { - ResolvedDockerHost dockerHost = null; - if (this.dockerConfiguration != null && this.dockerConfiguration.isBindHostToBuilder()) { - dockerHost = ResolvedDockerHost.from(this.dockerConfiguration.getHost()); + try (Lifecycle lifecycle = new Lifecycle(this.log, this.docker, getDockerHost(), request, builder)) { + executeLifecycle(builder, lifecycle); } - try (Lifecycle lifecycle = new Lifecycle(this.log, this.docker, dockerHost, request, builder)) { + } + + private void executeLifecycle(EphemeralBuilder builder, Lifecycle lifecycle) throws IOException { + ImageArchive archive = builder.getArchive(lifecycle.getApplicationDirectory()); + this.docker.image().load(archive, UpdateListener.none()); + try { lifecycle.execute(); } + finally { + this.docker.image().remove(builder.getName(), true); + } + } + + private ResolvedDockerHost getDockerHost() { + boolean bindHostToBuilder = this.dockerConfiguration != null && this.dockerConfiguration.isBindHostToBuilder(); + return (bindHostToBuilder) ? ResolvedDockerHost.from(this.dockerConfiguration.getHost()) : null; } private void tagImage(ImageReference sourceReference, List tags) throws IOException { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilder.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilder.java index 3ac1ee7a785..9471a4b6985 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilder.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,11 +21,14 @@ import java.util.Map; import org.springframework.boot.buildpack.platform.docker.type.Image; import org.springframework.boot.buildpack.platform.docker.type.ImageArchive; +import org.springframework.boot.buildpack.platform.docker.type.ImageArchive.Update; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.docker.type.Layer; import org.springframework.boot.buildpack.platform.io.Content; +import org.springframework.boot.buildpack.platform.io.IOConsumer; import org.springframework.boot.buildpack.platform.io.Owner; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; /** * A short-lived builder that is created for each {@link Lifecycle} run. @@ -37,13 +40,17 @@ class EphemeralBuilder { static final String BUILDER_FOR_LABEL_NAME = "org.springframework.boot.builderFor"; + private ImageReference name; + private final BuildOwner buildOwner; + private final Creator creator; + private final BuilderMetadata builderMetadata; - private final ImageArchive archive; + private final Image builderImage; - private final Creator creator; + private final IOConsumer archiveUpdate; /** * Create a new {@link EphemeralBuilder} instance. @@ -54,26 +61,25 @@ class EphemeralBuilder { * @param creator the builder creator * @param env the builder env * @param buildpacks an optional set of buildpacks to apply - * @throws IOException on IO error */ EphemeralBuilder(BuildOwner buildOwner, Image builderImage, ImageReference targetImage, - BuilderMetadata builderMetadata, Creator creator, Map env, Buildpacks buildpacks) - throws IOException { - ImageReference name = ImageReference.random("pack.local/builder/").inTaggedForm(); + BuilderMetadata builderMetadata, Creator creator, Map env, Buildpacks buildpacks) { + this.name = ImageReference.random("pack.local/builder/").inTaggedForm(); this.buildOwner = buildOwner; this.creator = creator; this.builderMetadata = builderMetadata.copy(this::updateMetadata); - this.archive = ImageArchive.from(builderImage, (update) -> { + this.builderImage = builderImage; + this.archiveUpdate = (update) -> { update.withUpdatedConfig(this.builderMetadata::attachTo); update.withUpdatedConfig((config) -> config.withLabel(BUILDER_FOR_LABEL_NAME, targetImage.toString())); - update.withTag(name); + update.withTag(this.name); if (!CollectionUtils.isEmpty(env)) { update.withNewLayer(getEnvLayer(env)); } if (buildpacks != null) { buildpacks.apply(update::withNewLayer); } - }); + }; } private void updateMetadata(BuilderMetadata.Update update) { @@ -95,7 +101,7 @@ class EphemeralBuilder { * @return the ephemeral builder name */ ImageReference getName() { - return this.archive.getTag(); + return this.name; } /** @@ -116,15 +122,26 @@ class EphemeralBuilder { /** * Return the contents of ephemeral builder for passing to Docker. + * @param applicationDirectory the application directory * @return the ephemeral builder archive + * @throws IOException on IO error */ - ImageArchive getArchive() { - return this.archive; + ImageArchive getArchive(String applicationDirectory) throws IOException { + return ImageArchive.from(this.builderImage, (update) -> { + this.archiveUpdate.accept(update); + if (StringUtils.hasLength(applicationDirectory)) { + update.withNewLayer(applicationDirectoryLayer(applicationDirectory)); + } + }); + } + + private Layer applicationDirectoryLayer(String applicationDirectory) throws IOException { + return Layer.of((layout) -> layout.directory(applicationDirectory, this.buildOwner)); } @Override public String toString() { - return this.archive.getTag().toString(); + return this.name.toString(); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java index c2457da089e..36cb9af1f7a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -117,6 +117,10 @@ class Lifecycle implements Closeable { this.securityOptions = getSecurityOptions(request); } + String getApplicationDirectory() { + return this.applicationDirectory; + } + private Cache getBuildCache(BuildRequest request) { if (request.getBuildCache() != null) { return request.getBuildCache(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilderTests.java index ef877fd3ee0..75ab37ee21b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilderTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -88,7 +88,7 @@ class EphemeralBuilderTests extends AbstractJsonTests { } @Test - void getNameHasRandomName() throws Exception { + void getNameHasRandomName() { EphemeralBuilder b1 = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata, this.creator, this.env, this.buildpacks); EphemeralBuilder b2 = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata, @@ -101,7 +101,7 @@ class EphemeralBuilderTests extends AbstractJsonTests { void getArchiveHasCreatedByConfig() throws Exception { EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata, this.creator, this.env, this.buildpacks); - ImageConfig config = builder.getArchive().getImageConfig(); + ImageConfig config = builder.getArchive(null).getImageConfig(); BuilderMetadata ephemeralMetadata = BuilderMetadata.fromImageConfig(config); assertThat(ephemeralMetadata.getCreatedBy().getName()).isEqualTo("Spring Boot"); assertThat(ephemeralMetadata.getCreatedBy().getVersion()).isEqualTo("dev"); @@ -111,7 +111,7 @@ class EphemeralBuilderTests extends AbstractJsonTests { void getArchiveHasTag() throws Exception { EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata, this.creator, this.env, this.buildpacks); - ImageReference tag = builder.getArchive().getTag(); + ImageReference tag = builder.getArchive(null).getTag(); assertThat(tag.toString()).startsWith("pack.local/builder/").endsWith(":latest"); } @@ -119,7 +119,7 @@ class EphemeralBuilderTests extends AbstractJsonTests { void getArchiveHasFixedCreatedDate() throws Exception { EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata, this.creator, this.env, this.buildpacks); - Instant createInstant = builder.getArchive().getCreateDate(); + Instant createInstant = builder.getArchive(null).getCreateDate(); OffsetDateTime createDateTime = OffsetDateTime.ofInstant(createInstant, ZoneId.of("UTC")); assertThat(createDateTime.getYear()).isEqualTo(1980); assertThat(createDateTime.getMonthValue()).isOne(); @@ -133,7 +133,7 @@ class EphemeralBuilderTests extends AbstractJsonTests { void getArchiveContainsEnvLayer() throws Exception { EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata, this.creator, this.env, this.buildpacks); - File directory = unpack(getLayer(builder.getArchive(), EXISTING_IMAGE_LAYER_COUNT), "env"); + File directory = unpack(getLayer(builder.getArchive(null), EXISTING_IMAGE_LAYER_COUNT), "env"); assertThat(new File(directory, "platform/env/spring")).usingCharset(StandardCharsets.UTF_8).hasContent("boot"); assertThat(new File(directory, "platform/env/empty")).usingCharset(StandardCharsets.UTF_8).hasContent(""); } @@ -142,7 +142,7 @@ class EphemeralBuilderTests extends AbstractJsonTests { void getArchiveHasBuilderForLabel() throws Exception { EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata, this.creator, this.env, this.buildpacks); - ImageConfig config = builder.getArchive().getImageConfig(); + ImageConfig config = builder.getArchive(null).getImageConfig(); assertThat(config.getLabels()) .contains(entry(EphemeralBuilder.BUILDER_FOR_LABEL_NAME, this.targetImage.toString())); } @@ -162,13 +162,21 @@ class EphemeralBuilderTests extends AbstractJsonTests { "/cnb/buildpacks/example_buildpack2/0.0.2/buildpack.toml"); assertBuildpackLayerContent(builder, EXISTING_IMAGE_LAYER_COUNT + 2, "/cnb/buildpacks/example_buildpack3/0.0.3/buildpack.toml"); - File orderDirectory = unpack(getLayer(builder.getArchive(), EXISTING_IMAGE_LAYER_COUNT + 3), "order"); + File orderDirectory = unpack(getLayer(builder.getArchive(null), EXISTING_IMAGE_LAYER_COUNT + 3), "order"); assertThat(new File(orderDirectory, "cnb/order.toml")).usingCharset(StandardCharsets.UTF_8) .hasContent(content("order.toml")); } + @Test + void getArchiveHasApplicationDirectoryLayer() throws Exception { + EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata, + this.creator, this.env, this.buildpacks); + File directory = unpack(getLayer(builder.getArchive("/myapp"), EXISTING_IMAGE_LAYER_COUNT + 1), "appdir"); + assertThat(new File(directory, "myapp")).isDirectory(); + } + private void assertBuildpackLayerContent(EphemeralBuilder builder, int index, String s) throws Exception { - File buildpackDirectory = unpack(getLayer(builder.getArchive(), index), "buildpack"); + File buildpackDirectory = unpack(getLayer(builder.getArchive(null), index), "buildpack"); assertThat(new File(buildpackDirectory, s)).usingCharset(StandardCharsets.UTF_8).hasContent("[test]"); }