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 f409fcc875..c88b752704 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 class BuildRequest { 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 class BuildRequest { 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 class BuildRequest { 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 class BuildRequest { 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 class BuildRequest { 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 class BuildRequest { 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 class BuildRequest { 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 class BuildRequest { 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 class BuildRequest { 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 class BuildRequest { 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 class BuildRequest { 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 class BuildRequest { 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 class BuildRequest { 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 class BuildRequest { 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 class BuildRequest { 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 class BuildRequest { 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 class BuildRequest { 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 class BuildRequest { 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 4d12105764..d12c27dab8 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 @@ package org.springframework.boot.buildpack.platform.build; 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 @@ class Lifecycle implements Closeable { 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 @@ class Lifecycle implements Closeable { 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 @@ class Lifecycle implements Closeable { 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 @@ class Lifecycle implements Closeable { 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 @@ class Lifecycle implements Closeable { 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 @@ class Lifecycle implements Closeable { @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 55cc096d08..464d218a50 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 @@ class BuildRequestTests { .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 40a2a80caa..64f54b450e 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 @@ class LifecycleTests { 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 @@ class LifecycleTests { 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 2b7814d909..7259fc11af 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 7bd3d9a24c..0f611d5d05 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 ef6ffc910e..3474dc37d4 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 5bca082e10..875239d07f 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 008889f519..e492703c6f 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 6b2af0c45a..04ebf45c74 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 abstract class BootBuildImage extends DefaultTask { 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 abstract class BootBuildImage extends DefaultTask { @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 @@ public abstract class BootBuildImage extends DefaultTask { } 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 84a2506cd7..1b1a653168 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 @@ class PackagingDocumentationTests { 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 b1c8c80335..4ffb3a011f 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 c4bc44c6e5..abf2c26ad4 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 059a9c2f88..67b23f539b 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 2cf4941fec..a67c45a0ed 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 349d1519e9..7f09ff8292 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 2b92c6dcb8..5a3d3ec76e 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 2d5cf6b247..699c450c63 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 @@ public class Image { 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 ed5a8e5d8e..a829b25dbf 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 @@ class ImageTests { 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 @@ class ImageTests { 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();