From 4433fcd1f2c6d9c9a912eb89b2b44ce2d3f7b512 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Wed, 20 Sep 2023 13:30:20 -0500 Subject: [PATCH] Add support for build workspace option when building images Closes gh-37478 --- .../platform/build/BuildRequest.java | 74 ++++++++++++------ .../buildpack/platform/build/Lifecycle.java | 78 +++++++++++++------ .../platform/build/BuildRequestTests.java | 16 ++++ .../platform/build/LifecycleTests.java | 6 +- .../lifecycle-creator-cache-bind-mounts.json | 4 +- .../lifecycle-creator-cache-volumes.json | 4 +- .../docs/asciidoc/packaging-oci-image.adoc | 15 +++- .../boot-build-image-bind-caches.gradle | 6 ++ .../boot-build-image-bind-caches.gradle.kts | 6 ++ .../gradle/tasks/bundling/BootBuildImage.java | 25 ++++++ .../docs/PackagingDocumentationTests.java | 3 +- ...tionTests-buildsImageWithBindCaches.gradle | 5 ++ ...onTests-buildsImageWithVolumeCaches.gradle | 5 ++ .../docs/asciidoc/packaging-oci-image.adoc | 14 +++- .../packaging-oci-image/bind-caches-pom.xml | 5 ++ .../projects/build-image-bind-caches/pom.xml | 5 ++ .../build-image-volume-caches/pom.xml | 5 ++ .../org/springframework/boot/maven/Image.java | 5 ++ .../boot/maven/ImageTests.java | 16 ++++ 19 files changed, 239 insertions(+), 58 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java index f409fcc875ff..c88b7527040a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java @@ -77,6 +77,8 @@ public class BuildRequest { private final List tags; + private final Cache buildWorkspace; + private final Cache buildCache; private final Cache launchCache; @@ -102,6 +104,7 @@ public class BuildRequest { this.bindings = Collections.emptyList(); this.network = null; this.tags = Collections.emptyList(); + this.buildWorkspace = null; this.buildCache = null; this.launchCache = null; this.createdDate = null; @@ -111,8 +114,8 @@ public class BuildRequest { BuildRequest(ImageReference name, Function applicationContent, ImageReference builder, ImageReference runImage, Creator creator, Map env, boolean cleanCache, boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List buildpacks, - List bindings, String network, List tags, Cache buildCache, Cache launchCache, - Instant createdDate, String applicationDirectory) { + List bindings, String network, List tags, Cache buildWorkspace, Cache buildCache, + Cache launchCache, Instant createdDate, String applicationDirectory) { this.name = name; this.applicationContent = applicationContent; this.builder = builder; @@ -127,6 +130,7 @@ public class BuildRequest { this.bindings = bindings; this.network = network; this.tags = tags; + this.buildWorkspace = buildWorkspace; this.buildCache = buildCache; this.launchCache = launchCache; this.createdDate = createdDate; @@ -142,8 +146,8 @@ public BuildRequest withBuilder(ImageReference builder) { Assert.notNull(builder, "Builder must not be null"); return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, - this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.launchCache, - this.createdDate, this.applicationDirectory); + this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, + this.launchCache, this.createdDate, this.applicationDirectory); } /** @@ -154,8 +158,8 @@ public BuildRequest withBuilder(ImageReference builder) { public BuildRequest withRunImage(ImageReference runImageName) { return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(), this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, - this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.launchCache, - this.createdDate, this.applicationDirectory); + this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, + this.launchCache, this.createdDate, this.applicationDirectory); } /** @@ -167,7 +171,7 @@ public BuildRequest withCreator(Creator creator) { Assert.notNull(creator, "Creator must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, + this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); } @@ -184,8 +188,8 @@ public BuildRequest withEnv(String name, String value) { env.put(name, value); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, - this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.launchCache, - this.createdDate, this.applicationDirectory); + this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, + this.launchCache, this.createdDate, this.applicationDirectory); } /** @@ -199,8 +203,8 @@ public BuildRequest withEnv(Map env) { updatedEnv.putAll(env); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy, - this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, - this.launchCache, this.createdDate, this.applicationDirectory); + this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, + this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); } /** @@ -211,7 +215,7 @@ public BuildRequest withEnv(Map env) { public BuildRequest withCleanCache(boolean cleanCache) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, + this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); } @@ -223,7 +227,7 @@ public BuildRequest withCleanCache(boolean cleanCache) { public BuildRequest withVerboseLogging(boolean verboseLogging) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, + this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); } @@ -235,7 +239,7 @@ public BuildRequest withVerboseLogging(boolean verboseLogging) { public BuildRequest withPullPolicy(PullPolicy pullPolicy) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, + this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); } @@ -247,7 +251,7 @@ public BuildRequest withPullPolicy(PullPolicy pullPolicy) { public BuildRequest withPublish(boolean publish) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, + this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); } @@ -272,7 +276,7 @@ public BuildRequest withBuildpacks(List buildpacks) { Assert.notNull(buildpacks, "Buildpacks must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks, this.bindings, - this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, + this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); } @@ -297,7 +301,7 @@ public BuildRequest withBindings(List bindings) { Assert.notNull(bindings, "Bindings must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, bindings, - this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, + this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); } @@ -310,7 +314,8 @@ public BuildRequest withBindings(List bindings) { public BuildRequest withNetwork(String network) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - network, this.tags, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); + network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, + this.applicationDirectory); } /** @@ -332,7 +337,21 @@ public BuildRequest withTags(List tags) { Assert.notNull(tags, "Tags must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, tags, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); + this.network, tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, + this.applicationDirectory); + } + + /** + * Return a new {@link BuildRequest} with an updated build workspace. + * @param buildWorkspace the build workspace + * @return an updated build request + */ + public BuildRequest withBuildWorkspace(Cache buildWorkspace) { + Assert.notNull(buildWorkspace, "BuildWorkspace must not be null"); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, + this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, + this.network, this.tags, buildWorkspace, this.buildCache, this.launchCache, this.createdDate, + this.applicationDirectory); } /** @@ -344,7 +363,8 @@ public BuildRequest withBuildCache(Cache buildCache) { Assert.notNull(buildCache, "BuildCache must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, buildCache, this.launchCache, this.createdDate, this.applicationDirectory); + this.network, this.tags, this.buildWorkspace, buildCache, this.launchCache, this.createdDate, + this.applicationDirectory); } /** @@ -356,7 +376,8 @@ public BuildRequest withLaunchCache(Cache launchCache) { Assert.notNull(launchCache, "LaunchCache must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildCache, launchCache, this.createdDate, this.applicationDirectory); + this.network, this.tags, this.buildWorkspace, this.buildCache, launchCache, this.createdDate, + this.applicationDirectory); } /** @@ -368,8 +389,8 @@ public BuildRequest withCreatedDate(String createdDate) { Assert.notNull(createdDate, "CreatedDate must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildCache, this.launchCache, parseCreatedDate(createdDate), - this.applicationDirectory); + this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, + parseCreatedDate(createdDate), this.applicationDirectory); } private Instant parseCreatedDate(String createdDate) { @@ -393,7 +414,8 @@ public BuildRequest withApplicationDirectory(String applicationDirectory) { Assert.notNull(applicationDirectory, "ApplicationDirectory must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, applicationDirectory); + this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, + applicationDirectory); } /** @@ -513,6 +535,10 @@ public List getTags() { return this.tags; } + public Cache getBuildWorkspace() { + return this.buildWorkspace; + } + /** * Return the custom build cache that should be used by the lifecycle. * @return the build cache 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 4d12105764a3..d12c27dab84e 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 @@ -18,7 +18,9 @@ import java.io.Closeable; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import java.util.function.Consumer; import com.sun.jna.Platform; @@ -70,9 +72,9 @@ class Lifecycle implements Closeable { private final ApiVersion platformVersion; - private final VolumeName layersVolume; + private final Cache layers; - private final VolumeName applicationVolume; + private final Cache application; private final Cache buildCache; @@ -101,17 +103,13 @@ class Lifecycle implements Closeable { this.builder = builder; this.lifecycleVersion = LifecycleVersion.parse(builder.getBuilderMetadata().getLifecycle().getVersion()); this.platformVersion = getPlatformVersion(builder.getBuilderMetadata().getLifecycle()); - this.layersVolume = createRandomVolumeName("pack-layers-"); - this.applicationVolume = createRandomVolumeName("pack-app-"); + this.layers = getLayersBindingSource(request); + this.application = getApplicationBindingSource(request); this.buildCache = getBuildCache(request); this.launchCache = getLaunchCache(request); this.applicationDirectory = getApplicationDirectory(request); } - protected VolumeName createRandomVolumeName(String prefix) { - return VolumeName.random(prefix); - } - private Cache getBuildCache(BuildRequest request) { if (request.getBuildCache() != null) { return request.getBuildCache(); @@ -130,11 +128,6 @@ private String getApplicationDirectory(BuildRequest request) { return (request.getApplicationDirectory() != null) ? request.getApplicationDirectory() : Directory.APPLICATION; } - private Cache createVolumeCache(BuildRequest request, String suffix) { - return Cache.volume( - VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", "." + suffix, 6)); - } - private ApiVersion getPlatformVersion(BuilderMetadata.Lifecycle lifecycle) { if (lifecycle.getApis().getPlatform() != null) { String[] supportedVersions = lifecycle.getApis().getPlatform(); @@ -153,12 +146,7 @@ void execute() throws IOException { this.executed = true; this.log.executingLifecycle(this.request, this.lifecycleVersion, this.buildCache); if (this.request.isCleanCache()) { - if (this.buildCache.getVolume() != null) { - deleteVolume(this.buildCache.getVolume().getVolumeName()); - } - if (this.buildCache.getBind() != null) { - deleteBind(this.buildCache.getBind().getSource()); - } + deleteCache(this.buildCache); } run(createPhase()); this.log.executedLifecycle(this.request); @@ -183,8 +171,8 @@ private Phase createPhase() { phase.withArgs("-process-type=web"); } phase.withArgs(this.request.getName()); - phase.withBinding(Binding.from(this.layersVolume, Directory.LAYERS)); - phase.withBinding(Binding.from(this.applicationVolume, this.applicationDirectory)); + phase.withBinding(Binding.from(getCacheBindingSource(this.layers), Directory.LAYERS)); + phase.withBinding(Binding.from(getCacheBindingSource(this.application), this.applicationDirectory)); phase.withBinding(Binding.from(getCacheBindingSource(this.buildCache), Directory.CACHE)); phase.withBinding(Binding.from(getCacheBindingSource(this.launchCache), Directory.LAUNCH_CACHE)); if (this.request.getBindings() != null) { @@ -200,10 +188,42 @@ private Phase createPhase() { return phase; } + private Cache getLayersBindingSource(BuildRequest request) { + if (request.getBuildWorkspace() != null) { + return getBuildWorkspaceBindingSource(request.getBuildWorkspace(), "layers"); + } + return createVolumeCache("pack-layers-"); + } + + private Cache getApplicationBindingSource(BuildRequest request) { + if (request.getBuildWorkspace() != null) { + return getBuildWorkspaceBindingSource(request.getBuildWorkspace(), "app"); + } + return createVolumeCache("pack-app-"); + } + + private Cache getBuildWorkspaceBindingSource(Cache buildWorkspace, String suffix) { + return (buildWorkspace.getVolume() != null) ? Cache.volume(buildWorkspace.getVolume().getName() + "-" + suffix) + : Cache.bind(buildWorkspace.getBind().getSource() + "-" + suffix); + } + private String getCacheBindingSource(Cache cache) { return (cache.getVolume() != null) ? cache.getVolume().getName() : cache.getBind().getSource(); } + private Cache createVolumeCache(String prefix) { + return Cache.volume(createRandomVolumeName(prefix)); + } + + private Cache createVolumeCache(BuildRequest request, String suffix) { + return Cache.volume( + VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", "." + suffix, 6)); + } + + protected VolumeName createRandomVolumeName(String prefix) { + return VolumeName.random(prefix); + } + private void configureDaemonAccess(Phase phase) { if (this.dockerHost != null) { if (this.dockerHost.isRemote()) { @@ -255,6 +275,9 @@ private ContainerReference createContainer(ContainerConfig config) throws IOExce return this.docker.container().create(config); } try { + if (this.application.getBind() != null) { + Files.createDirectories(Path.of(this.application.getBind().getSource())); + } TarArchive applicationContent = this.request.getApplicationContent(this.builder.getBuildOwner()); return this.docker.container() .create(config, ContainerContent.of(applicationContent, this.applicationDirectory)); @@ -266,8 +289,17 @@ private ContainerReference createContainer(ContainerConfig config) throws IOExce @Override public void close() throws IOException { - deleteVolume(this.layersVolume); - deleteVolume(this.applicationVolume); + deleteCache(this.layers); + deleteCache(this.application); + } + + private void deleteCache(Cache cache) throws IOException { + if (cache.getVolume() != null) { + deleteVolume(cache.getVolume().getVolumeName()); + } + if (cache.getBind() != null) { + deleteBind(cache.getBind().getSource()); + } } private void deleteVolume(VolumeName name) throws IOException { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java index 55cc096d083d..464d218a50f7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java @@ -233,6 +233,22 @@ void withTagsWhenTagsIsNullThrowsException() throws IOException { .withMessage("Tags must not be null"); } + @Test + void withBuildWorkspaceVolumeAddsWorkspace() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + BuildRequest withWorkspace = request.withBuildWorkspace(Cache.volume("build-workspace")); + assertThat(request.getBuildWorkspace()).isNull(); + assertThat(withWorkspace.getBuildWorkspace()).isEqualTo(Cache.volume("build-workspace")); + } + + @Test + void withBuildWorkspaceBindAddsWorkspace() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + BuildRequest withWorkspace = request.withBuildWorkspace(Cache.bind("/tmp/build-workspace")); + assertThat(request.getBuildWorkspace()).isNull(); + assertThat(withWorkspace.getBuildWorkspace()).isEqualTo(Cache.bind("/tmp/build-workspace")); + } + @Test void withBuildVolumeCacheAddsCache() throws IOException { BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java index 40a2a80caa89..64f54b450e49 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java @@ -211,7 +211,8 @@ void executeWithCacheVolumeNamesExecutesPhases() throws Exception { given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); - BuildRequest request = getTestRequest().withBuildCache(Cache.volume("build-volume")) + BuildRequest request = getTestRequest().withBuildWorkspace(Cache.volume("work-volume")) + .withBuildCache(Cache.volume("build-volume")) .withLaunchCache(Cache.volume("launch-volume")); createLifecycle(request).execute(); assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-cache-volumes.json")); @@ -223,7 +224,8 @@ void executeWithCacheBindMountsExecutesPhases() throws Exception { given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); - BuildRequest request = getTestRequest().withBuildCache(Cache.bind("/tmp/build-cache")) + BuildRequest request = getTestRequest().withBuildWorkspace(Cache.bind("/tmp/work")) + .withBuildCache(Cache.bind("/tmp/build-cache")) .withLaunchCache(Cache.bind("/tmp/launch-cache")); createLifecycle(request).execute(); assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-cache-bind-mounts.json")); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-cache-bind-mounts.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-cache-bind-mounts.json index 2b7814d909c8..7259fc11af77 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-cache-bind-mounts.json +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-cache-bind-mounts.json @@ -27,8 +27,8 @@ "HostConfig": { "Binds": [ "/var/run/docker.sock:/var/run/docker.sock", - "pack-layers-aaaaaaaaaa:/layers", - "pack-app-aaaaaaaaaa:/workspace", + "/tmp/work-layers:/layers", + "/tmp/work-app:/workspace", "/tmp/build-cache:/cache", "/tmp/launch-cache:/launch-cache" ], diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-cache-volumes.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-cache-volumes.json index 7bd3d9a24ca0..0f611d5d059c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-cache-volumes.json +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-cache-volumes.json @@ -27,8 +27,8 @@ "HostConfig": { "Binds": [ "/var/run/docker.sock:/var/run/docker.sock", - "pack-layers-aaaaaaaaaa:/layers", - "pack-app-aaaaaaaaaa:/workspace", + "work-volume-layers:/layers", + "work-volume-app:/workspace", "build-volume:/cache", "launch-volume:/launch-cache" ], diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc index ef6ffc910eb5..3474dc37d4b6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -193,14 +193,22 @@ The value supplied will be passed unvalidated to Docker when creating the builde The values provided to the `tags` option should be full image references in the form of `[image name]:[tag]` or `[repository]/[image name]:[tag]`. | +| `buildWorkspace` +| +| A temporary workspace that will be used by the builder and buildpacks to store files during image building. +The value can be a named volume or a bind mount location. +| A named volume in the Docker daemon, with a name derived from the image name. + | `buildCache` | | A cache containing layers created by buildpacks and used by the image building process. +The value can be a named volume or a bind mount location. | A named volume in the Docker daemon, with a name derived from the image name. | `launchCache` | | A cache containing layers created by buildpacks and used by the image launching process. +The value can be a named volume or a bind mount location. | A named volume in the Docker daemon, with a name derived from the image name. | `createdDate` @@ -420,7 +428,7 @@ The publish option can be specified on the command line as well, as shown in thi ---- [[build-image.examples.caches]] -=== Builder Cache Configuration +=== Builder Cache and Workspace Configuration The CNB builder caches layers that are used when building and launching an image. By default, these caches are stored as named volumes in the Docker daemon with names that are derived from the full name of the target image. @@ -440,7 +448,10 @@ include::../gradle/packaging/boot-build-image-caches.gradle[tags=caches] include::../gradle/packaging/boot-build-image-caches.gradle.kts[tags=caches] ---- -The caches can be configured to use bind mounts instead of named volumes, as shown in the following example: +Builders and buildpacks need a location to store temporary files during image building. +By default, this temporary build workspace is stored in a named volume. + +The caches and the build workspace can be configured to use bind mounts instead of named volumes, as shown in the following example: [source,groovy,indent=0,subs="verbatim,attributes",role="primary"] .Groovy diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle index 5bca082e10fd..875239d07f80 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle @@ -9,6 +9,11 @@ tasks.named("bootJar") { // tag::caches[] tasks.named("bootBuildImage") { + buildWorkspace { + bind { + source = "/tmp/cache-${rootProject.name}.work" + } + } buildCache { bind { source = "/tmp/cache-${rootProject.name}.build" @@ -24,6 +29,7 @@ tasks.named("bootBuildImage") { tasks.register("bootBuildImageCaches") { doFirst { + bootBuildImage.buildWorkspace.asCache().with { print "buildWorkspace=$source" } bootBuildImage.buildCache.asCache().with { println "buildCache=$source" } bootBuildImage.launchCache.asCache().with { println "launchCache=$source" } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle.kts index 008889f51961..e492703c6f96 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle.kts +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle.kts @@ -7,6 +7,11 @@ plugins { // tag::caches[] tasks.named("bootBuildImage") { + buildWorkspace { + bind { + source.set("/tmp/cache-${rootProject.name}.work") + } + } buildCache { bind { source.set("/tmp/cache-${rootProject.name}.build") @@ -22,6 +27,7 @@ tasks.named("bootBuildImage") { tasks.register("bootBuildImageCaches") { doFirst { + println("buildWorkspace=" + tasks.getByName("bootBuildImage").buildWorkspace.asCache().bind.source) println("buildCache=" + tasks.getByName("bootBuildImage").buildCache.asCache().bind.source) println("launchCache=" + tasks.getByName("bootBuildImage").launchCache.asCache().bind.source) } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java index 6b2af0c45a5e..04ebf45c74d2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java @@ -69,6 +69,8 @@ public abstract class BootBuildImage extends DefaultTask { private final String projectName; + private final CacheSpec buildWorkspace; + private final CacheSpec buildCache; private final CacheSpec launchCache; @@ -91,6 +93,7 @@ public BootBuildImage() { getCleanCache().convention(false); getVerboseLogging().convention(false); getPublish().convention(false); + this.buildWorkspace = getProject().getObjects().newInstance(CacheSpec.class); this.buildCache = getProject().getObjects().newInstance(CacheSpec.class); this.launchCache = getProject().getObjects().newInstance(CacheSpec.class); this.docker = getProject().getObjects().newInstance(DockerSpec.class); @@ -222,6 +225,25 @@ public void setPullPolicy(String pullPolicy) { @Option(option = "network", description = "Connect detect and build containers to network") public abstract Property getNetwork(); + /** + * Returns the build temporary workspace that will be used when building the image. + * @return the cache + */ + @Nested + @Optional + public CacheSpec getBuildWorkspace() { + return this.buildWorkspace; + } + + /** + * Customizes the {@link CacheSpec} for the build temporary workspace using the given + * {@code action}. + * @param action the action + */ + public void buildWorkspace(Action action) { + action.execute(this.buildWorkspace); + } + /** * Returns the build cache that will be used when building the image. * @return the cache @@ -400,6 +422,9 @@ private BuildRequest customizeTags(BuildRequest request) { } private BuildRequest customizeCaches(BuildRequest request) { + if (this.buildWorkspace.asCache() != null) { + request = request.withBuildWorkspace((this.buildWorkspace.asCache())); + } if (this.buildCache.asCache() != null) { request = request.withBuildCache(this.buildCache.asCache()); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java index 84a2506cd715..1b1a6531682c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java @@ -343,7 +343,8 @@ void bootBuildImageWithCaches() { void bootBuildImageWithBindCaches() { BuildResult result = this.gradleBuild.script("src/docs/gradle/packaging/boot-build-image-bind-caches") .build("bootBuildImageCaches"); - assertThat(result.getOutput()).containsPattern("buildCache=/tmp/cache-gradle-[\\d]+.build") + assertThat(result.getOutput()).containsPattern("buildWorkspace=/tmp/cache-gradle-[\\d]+.work") + .containsPattern("buildCache=/tmp/cache-gradle-[\\d]+.build") .containsPattern("launchCache=/tmp/cache-gradle-[\\d]+.launch"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBindCaches.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBindCaches.gradle index b1c8c803350a..4ffb3a011faa 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBindCaches.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBindCaches.gradle @@ -11,6 +11,11 @@ java { bootBuildImage { builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2" pullPolicy = "IF_NOT_PRESENT" + buildWorkspace { + bind { + source = System.getProperty('java.io.tmpdir') + "/junit-image-pack-${rootProject.name}-work" + } + } buildCache { bind { source = System.getProperty('java.io.tmpdir') + "/junit-image-cache-${rootProject.name}-build" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithVolumeCaches.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithVolumeCaches.gradle index c4bc44c6e505..abf2c26ad447 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithVolumeCaches.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithVolumeCaches.gradle @@ -11,6 +11,11 @@ java { bootBuildImage { builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2" pullPolicy = "IF_NOT_PRESENT" + buildWorkspace { + volume { + name = "pack-${rootProject.name}.work" + } + } buildCache { volume { name = "cache-${rootProject.name}.build" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc index 059a9c2f882d..67b23f539b46 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -202,12 +202,19 @@ The value supplied will be passed unvalidated to Docker when creating the builde The values provided to the `tags` option should be full image references in the form of `[image name]:[tag]` or `[repository]/[image name]:[tag]`. | +| `buildWorkspace` +| A temporary workspace that will be used by the builder and buildpacks to store files during image building. +The value can be a named volume or a bind mount location. +| A named volume in the Docker daemon, with a name derived from the image name. + | `buildCache` | A cache containing layers created by buildpacks and used by the image building process. +The value can be a named volume or a bind mount location. | A named volume in the Docker daemon, with a name derived from the image name. | `launchCache` | A cache containing layers created by buildpacks and used by the image launching process. +The value can be a named volume or a bind mount location. | A named volume in the Docker daemon, with a name derived from the image name. | `createdDate` + @@ -403,7 +410,7 @@ include::../maven/packaging-oci-image/docker-pom-authentication-command-line.xml ---- [[build-image.examples.caches]] -=== Builder Cache Configuration +=== Builder Cache and Workspace Configuration The CNB builder caches layers that are used when building and launching an image. By default, these caches are stored as named volumes in the Docker daemon with names that are derived from the full name of the target image. @@ -416,7 +423,10 @@ The cache volumes can be configured to use alternative names to give more contro include::../maven/packaging-oci-image/caches-pom.xml[tags=caches] ---- -The caches can be configured to use bind mounts instead of named volumes, as shown in the following example: +Builders and buildpacks need a location to store temporary files during image building. +By default, this temporary build workspace is stored in a named volume. + +The caches and the build workspace can be configured to use bind mounts instead of named volumes, as shown in the following example: [source,xml,indent=0,subs="verbatim,attributes",tabsize=4] ---- diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/bind-caches-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/bind-caches-pom.xml index 2cf4941fecbe..a67c45a0ed5d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/bind-caches-pom.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/bind-caches-pom.xml @@ -8,6 +8,11 @@ spring-boot-maven-plugin + + + /tmp/cache-${project.artifactId}.work + + /tmp/cache-${project.artifactId}.build diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bind-caches/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bind-caches/pom.xml index 349d1519e9e1..7f09ff829236 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bind-caches/pom.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bind-caches/pom.xml @@ -24,6 +24,11 @@ projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2 + + + ${java.io.tmpdir}/junit-image-cache-${test-build-id}-work + + ${java.io.tmpdir}/junit-image-cache-${test-build-id}-build diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-volume-caches/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-volume-caches/pom.xml index 2b92c6dcb825..5a3d3ec76e86 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-volume-caches/pom.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-volume-caches/pom.xml @@ -24,6 +24,11 @@ projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2 + + + cache-${test-build-id}.work + + cache-${test-build-id}.build diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java index 2d5cf6b24728..699c450c6333 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java @@ -69,6 +69,8 @@ public class Image { List tags; + CacheInfo buildWorkspace; + CacheInfo buildCache; CacheInfo launchCache; @@ -243,6 +245,9 @@ private BuildRequest customize(BuildRequest request) { if (!CollectionUtils.isEmpty(this.tags)) { request = request.withTags(this.tags.stream().map(ImageReference::of).toList()); } + if (this.buildWorkspace != null) { + request = request.withBuildWorkspace(this.buildWorkspace.asCache()); + } if (this.buildCache != null) { request = request.withBuildCache(this.buildCache.asCache()); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java index ed5a8e5d8ede..a829b25dbfb2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java @@ -170,6 +170,14 @@ void getBuildRequestWhenHasTagsUsesTags() { ImageReference.of("example.com/my-app:0.0.1-SNAPSHOT"), ImageReference.of("example.com/my-app:latest")); } + @Test + void getBuildRequestWhenHasBuildWorkspaceVolumeUsesWorkspace() { + Image image = new Image(); + image.buildWorkspace = CacheInfo.fromVolume(new VolumeCacheInfo("build-work-vol")); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getBuildWorkspace()).isEqualTo(Cache.volume("build-work-vol")); + } + @Test void getBuildRequestWhenHasBuildCacheVolumeUsesCache() { Image image = new Image(); @@ -186,6 +194,14 @@ void getBuildRequestWhenHasLaunchCacheVolumeUsesCache() { assertThat(request.getLaunchCache()).isEqualTo(Cache.volume("launch-cache-vol")); } + @Test + void getBuildRequestWhenHasBuildWorkspaceBindUsesWorkspace() { + Image image = new Image(); + image.buildWorkspace = CacheInfo.fromBind(new BindCacheInfo("build-work-dir")); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getBuildWorkspace()).isEqualTo(Cache.bind("build-work-dir")); + } + @Test void getBuildRequestWhenHasBuildCacheBindUsesCache() { Image image = new Image();