diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/AbstractBuildLog.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/AbstractBuildLog.java index 7154d4ad43b..b870d30f9f6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/AbstractBuildLog.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/AbstractBuildLog.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -68,6 +68,12 @@ public abstract class AbstractBuildLog implements BuildLog { log(" > Using build cache volume '" + buildCacheVolume + "'"); } + @Override + public void executingLifecycle(BuildRequest request, LifecycleVersion version, Cache buildCache) { + log(" > Executing lifecycle version " + version); + log(" > Using build cache " + buildCache); + } + @Override public Consumer runningPhase(BuildRequest request, String name) { log(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildLog.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildLog.java index 0acbbabd224..e84054773f4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildLog.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildLog.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -79,6 +79,14 @@ public interface BuildLog { */ void executingLifecycle(BuildRequest request, LifecycleVersion version, VolumeName buildCacheVolume); + /** + * Log that the lifecycle is executing. + * @param request the build request + * @param version the lifecycle version + * @param buildCache the build cache in use + */ + void executingLifecycle(BuildRequest request, LifecycleVersion version, Cache buildCache); + /** * Log that a specific phase is running. * @param request the build request diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java index 9f3087f94c3..704a3418d39 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -18,6 +18,7 @@ package org.springframework.boot.buildpack.platform.build; import java.util.Objects; +import org.springframework.boot.buildpack.platform.docker.type.VolumeName; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -37,7 +38,22 @@ public class Cache { /** * A cache stored as a volume in the Docker daemon. */ - VOLUME; + VOLUME("volume"), + + /** + * A cache stored as a bind mount. + */ + BIND("bind mount"); + + private final String description; + + Format(String description) { + this.description = description; + } + + public String getDescription() { + return this.description; + } } @@ -55,16 +71,44 @@ public class Cache { return (this.format.equals(Format.VOLUME)) ? (Volume) this : null; } + /** + * Return the details of the cache if it is a bind cache. + * @return the cache, or {@code null} if it is not a bind cache + */ + public Bind getBind() { + return (this.format.equals(Format.BIND)) ? (Bind) this : null; + } + /** * Create a new {@code Cache} that uses a volume with the provided name. * @param name the cache volume name * @return a new cache instance */ public static Cache volume(String name) { + Assert.notNull(name, "Name must not be null"); + return new Volume(VolumeName.of(name)); + } + + /** + * Create a new {@code Cache} that uses a volume with the provided name. + * @param name the cache volume name + * @return a new cache instance + */ + public static Cache volume(VolumeName name) { Assert.notNull(name, "Name must not be null"); return new Volume(name); } + /** + * Create a new {@code Cache} that uses a bind mount with the provided source. + * @param source the cache bind mount source + * @return a new cache instance + */ + public static Cache bind(String source) { + Assert.notNull(source, "Source must not be null"); + return new Bind(source); + } + @Override public boolean equals(Object obj) { if (this == obj) { @@ -87,14 +131,18 @@ public class Cache { */ public static class Volume extends Cache { - private final String name; + private final VolumeName name; - Volume(String name) { + Volume(VolumeName name) { super(Format.VOLUME); this.name = name; } public String getName() { + return this.name.toString(); + } + + public VolumeName getVolumeName() { return this.name; } @@ -120,6 +168,56 @@ public class Cache { return result; } + @Override + public String toString() { + return this.format.getDescription() + " '" + this.name + "'"; + } + + } + + /** + * Details of a cache stored in a bind mount. + */ + public static class Bind extends Cache { + + private final String source; + + Bind(String source) { + super(Format.BIND); + this.source = source; + } + + public String getSource() { + return this.source; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + if (!super.equals(obj)) { + return false; + } + Bind other = (Bind) obj; + return Objects.equals(this.source, other.source); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.source); + return result; + } + + @Override + public String toString() { + return this.format.getDescription() + " '" + this.source + "'"; + } + } } 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 a9bf57caee1..4d12105764a 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,6 +18,7 @@ package org.springframework.boot.buildpack.platform.build; import java.io.Closeable; import java.io.IOException; +import java.nio.file.Path; import java.util.function.Consumer; import com.sun.jna.Platform; @@ -34,6 +35,7 @@ import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.docker.type.VolumeName; import org.springframework.boot.buildpack.platform.io.TarArchive; import org.springframework.util.Assert; +import org.springframework.util.FileSystemUtils; /** * A buildpack lifecycle used to run the build {@link Phase phases} needed to package an @@ -72,9 +74,9 @@ class Lifecycle implements Closeable { private final VolumeName applicationVolume; - private final VolumeName buildCacheVolume; + private final Cache buildCache; - private final VolumeName launchCacheVolume; + private final Cache launchCache; private final String applicationDirectory; @@ -101,8 +103,8 @@ class Lifecycle implements Closeable { this.platformVersion = getPlatformVersion(builder.getBuilderMetadata().getLifecycle()); this.layersVolume = createRandomVolumeName("pack-layers-"); this.applicationVolume = createRandomVolumeName("pack-app-"); - this.buildCacheVolume = getBuildCacheVolumeName(request); - this.launchCacheVolume = getLaunchCacheVolumeName(request); + this.buildCache = getBuildCache(request); + this.launchCache = getLaunchCache(request); this.applicationDirectory = getApplicationDirectory(request); } @@ -110,33 +112,27 @@ class Lifecycle implements Closeable { return VolumeName.random(prefix); } - private VolumeName getBuildCacheVolumeName(BuildRequest request) { + private Cache getBuildCache(BuildRequest request) { if (request.getBuildCache() != null) { - return getVolumeName(request.getBuildCache()); + return request.getBuildCache(); } - return createCacheVolumeName(request, "build"); + return createVolumeCache(request, "build"); } - private VolumeName getLaunchCacheVolumeName(BuildRequest request) { + private Cache getLaunchCache(BuildRequest request) { if (request.getLaunchCache() != null) { - return getVolumeName(request.getLaunchCache()); + return request.getLaunchCache(); } - return createCacheVolumeName(request, "launch"); - } - - private VolumeName getVolumeName(Cache cache) { - if (cache.getVolume() != null) { - return VolumeName.of(cache.getVolume().getName()); - } - return null; + return createVolumeCache(request, "launch"); } private String getApplicationDirectory(BuildRequest request) { return (request.getApplicationDirectory() != null) ? request.getApplicationDirectory() : Directory.APPLICATION; } - private VolumeName createCacheVolumeName(BuildRequest request, String suffix) { - return VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", "." + suffix, 6); + 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) { @@ -155,9 +151,14 @@ class Lifecycle implements Closeable { void execute() throws IOException { Assert.state(!this.executed, "Lifecycle has already been executed"); this.executed = true; - this.log.executingLifecycle(this.request, this.lifecycleVersion, this.buildCacheVolume); + this.log.executingLifecycle(this.request, this.lifecycleVersion, this.buildCache); if (this.request.isCleanCache()) { - deleteVolume(this.buildCacheVolume); + if (this.buildCache.getVolume() != null) { + deleteVolume(this.buildCache.getVolume().getVolumeName()); + } + if (this.buildCache.getBind() != null) { + deleteBind(this.buildCache.getBind().getSource()); + } } run(createPhase()); this.log.executedLifecycle(this.request); @@ -184,8 +185,8 @@ class Lifecycle implements Closeable { 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(this.buildCacheVolume, Directory.CACHE)); - phase.withBinding(Binding.from(this.launchCacheVolume, Directory.LAUNCH_CACHE)); + phase.withBinding(Binding.from(getCacheBindingSource(this.buildCache), Directory.CACHE)); + phase.withBinding(Binding.from(getCacheBindingSource(this.launchCache), Directory.LAUNCH_CACHE)); if (this.request.getBindings() != null) { this.request.getBindings().forEach(phase::withBinding); } @@ -199,6 +200,10 @@ class Lifecycle implements Closeable { return phase; } + private String getCacheBindingSource(Cache cache) { + return (cache.getVolume() != null) ? cache.getVolume().getName() : cache.getBind().getSource(); + } + private void configureDaemonAccess(Phase phase) { if (this.dockerHost != null) { if (this.dockerHost.isRemote()) { @@ -269,6 +274,15 @@ class Lifecycle implements Closeable { this.docker.volume().delete(name, true); } + private void deleteBind(String source) { + try { + FileSystemUtils.deleteRecursively(Path.of(source)); + } + catch (IOException ex) { + throw new IllegalStateException("Error cleaning bind mount directory '" + source + "'", ex); + } + } + /** * Common directories used by the various phases. */ 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 1ded1fa5261..20eb2944503 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 @@ -239,6 +239,14 @@ class BuildRequestTests { assertThat(withCache.getBuildCache()).isEqualTo(Cache.volume("build-volume")); } + @Test + void withBuildBindCacheAddsCache() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + BuildRequest withCache = request.withBuildCache(Cache.bind("/tmp/build-cache")); + assertThat(request.getBuildCache()).isNull(); + assertThat(withCache.getBuildCache()).isEqualTo(Cache.bind("/tmp/build-cache")); + } + @Test void withBuildVolumeCacheWhenCacheIsNullThrowsException() throws IOException { BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); @@ -254,6 +262,14 @@ class BuildRequestTests { assertThat(withCache.getLaunchCache()).isEqualTo(Cache.volume("launch-volume")); } + @Test + void withLaunchBindCacheAddsCache() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + BuildRequest withCache = request.withLaunchCache(Cache.bind("/tmp/launch-cache")); + assertThat(request.getLaunchCache()).isNull(); + assertThat(withCache.getLaunchCache()).isEqualTo(Cache.bind("/tmp/launch-cache")); + } + @Test void withLaunchVolumeCacheWhenCacheIsNullThrowsException() 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 d8b03407a3d..40a2a80caa8 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 @@ -218,6 +218,18 @@ class LifecycleTests { assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); } + @Test + 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")) + .withLaunchCache(Cache.bind("/tmp/launch-cache")); + createLifecycle(request).execute(); + assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-cache-bind-mounts.json")); + assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + } + @Test void executeWithCreatedDateExecutesPhases() throws Exception { given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLogTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLogTests.java index 74cefced82a..1e25ed10a99 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLogTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLogTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -67,7 +67,7 @@ class PrintStreamBuildLogTests { Consumer pullRunImageConsumer = log.pullingImage(runImageReference, ImageType.RUNNER); pullRunImageConsumer.accept(new TotalProgressEvent(100)); log.pulledImage(runImage, ImageType.RUNNER); - log.executingLifecycle(request, LifecycleVersion.parse("0.5"), VolumeName.of("pack-abc.cache")); + log.executingLifecycle(request, LifecycleVersion.parse("0.5"), Cache.volume(VolumeName.of("pack-abc.cache"))); Consumer phase1Consumer = log.runningPhase(request, "alphabet"); phase1Consumer.accept(mockLogEvent("one")); phase1Consumer.accept(mockLogEvent("two")); 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 new file mode 100644 index 00000000000..2b7814d909c --- /dev/null +++ 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 @@ -0,0 +1,39 @@ +{ + "User": "root", + "Image": "pack.local/ephemeral-builder", + "Cmd": [ + "/cnb/lifecycle/creator", + "-app", + "/workspace", + "-platform", + "/platform", + "-run-image", + "docker.io/cloudfoundry/run:latest", + "-layers", + "/layers", + "-cache-dir", + "/cache", + "-launch-cache", + "/launch-cache", + "-daemon", + "docker.io/library/my-application:latest" + ], + "Env": [ + "CNB_PLATFORM_API=0.8" + ], + "Labels": { + "author": "spring-boot" + }, + "HostConfig": { + "Binds": [ + "/var/run/docker.sock:/var/run/docker.sock", + "pack-layers-aaaaaaaaaa:/layers", + "pack-app-aaaaaaaaaa:/workspace", + "/tmp/build-cache:/cache", + "/tmp/launch-cache:/launch-cache" + ], + "SecurityOpt" : [ + "label=disable" + ] + } +} \ No newline at end of file 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 501d4f12d4f..ef6ffc910eb 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 @@ -440,6 +440,20 @@ 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: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-build-image-bind-caches.gradle[tags=caches] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-build-image-bind-caches.gradle.kts[tags=caches] +---- + [[build-image.examples.docker]] === Docker Configuration 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 new file mode 100644 index 00000000000..5bca082e10f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle @@ -0,0 +1,30 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +tasks.named("bootJar") { + mainClass = 'com.example.ExampleApplication' +} + +// tag::caches[] +tasks.named("bootBuildImage") { + buildCache { + bind { + source = "/tmp/cache-${rootProject.name}.build" + } + } + launchCache { + bind { + source = "/tmp/cache-${rootProject.name}.launch" + } + } +} +// end::caches[] + +tasks.register("bootBuildImageCaches") { + doFirst { + 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 new file mode 100644 index 00000000000..008889f5196 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle.kts @@ -0,0 +1,28 @@ +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::caches[] +tasks.named("bootBuildImage") { + buildCache { + bind { + source.set("/tmp/cache-${rootProject.name}.build") + } + } + launchCache { + bind { + source.set("/tmp/cache-${rootProject.name}.launch") + } + } +} +// end::caches[] + +tasks.register("bootBuildImageCaches") { + doFirst { + 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/CacheSpec.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java index d33d6a96496..235a3665f14 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 the original author or authors. + * Copyright 2021-2023 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. @@ -60,6 +60,19 @@ public class CacheSpec { this.cache = Cache.volume(spec.getName().get()); } + /** + * Configures a bind cache using the given {@code action}. + * @param action the action + */ + public void bind(Action action) { + if (this.cache != null) { + throw new GradleException("Each image building cache can be configured only once"); + } + BindCacheSpec spec = this.objectFactory.newInstance(BindCacheSpec.class); + action.execute(spec); + this.cache = Cache.bind(spec.getSource().get()); + } + /** * Configuration for an image building cache stored in a Docker volume. */ @@ -74,4 +87,18 @@ public class CacheSpec { } + /** + * Configuration for an image building cache stored in a bind mount. + */ + public abstract static class BindCacheSpec { + + /** + * Returns the source of the cache. + * @return the cache source + */ + @Input + public abstract Property getSource(); + + } + } 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 562a3f5b139..84a2506cd71 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 @@ -339,6 +339,14 @@ class PackagingDocumentationTests { .containsPattern("launchCache=cache-gradle-[\\d]+.launch"); } + @TestTemplate + 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") + .containsPattern("launchCache=/tmp/cache-gradle-[\\d]+.launch"); + } + protected void jarFile(File file) throws IOException { try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(file))) { jar.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF")); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java index 2a2238c4a9e..8e3f23eb2c6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java @@ -50,6 +50,7 @@ import org.springframework.boot.gradle.junit.GradleCompatibility; import org.springframework.boot.testsupport.gradle.testkit.GradleBuild; import org.springframework.boot.testsupport.junit.DisabledOnOs; import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable; +import org.springframework.util.FileSystemUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -297,6 +298,26 @@ class BootBuildImageIntegrationTests { deleteVolumes("cache-" + projectName + ".build", "cache-" + projectName + ".launch"); } + @TestTemplate + void buildsImageWithBindCaches() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.build("bootBuildImage"); + String projectName = this.gradleBuild.getProjectDir().getName(); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("docker.io/library/" + projectName); + assertThat(result.getOutput()).contains("---> Test Info buildpack building"); + assertThat(result.getOutput()).contains("---> Test Info buildpack done"); + removeImages(projectName); + String tempDir = System.getProperty("java.io.tmpdir"); + Path buildCachePath = Paths.get(tempDir, "junit-image-cache-" + projectName + "-build"); + Path launchCachePath = Paths.get(tempDir, "junit-image-cache-" + projectName + "-launch"); + assertThat(buildCachePath).exists().isDirectory(); + assertThat(launchCachePath).exists().isDirectory(); + FileSystemUtils.deleteRecursively(buildCachePath); + FileSystemUtils.deleteRecursively(launchCachePath); + } + @TestTemplate void buildsImageWithCreatedDate() throws IOException { writeMainClass(); 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 new file mode 100644 index 00000000000..b1c8c803350 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBindCaches.gradle @@ -0,0 +1,24 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +java { + sourceCompatibility = '1.8' + targetCompatibility = '1.8' +} + +bootBuildImage { + builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2" + pullPolicy = "IF_NOT_PRESENT" + buildCache { + bind { + source = System.getProperty('java.io.tmpdir') + "/junit-image-cache-${rootProject.name}-build" + } + } + launchCache { + bind { + source = System.getProperty('java.io.tmpdir') + "/junit-image-cache-${rootProject.name}-launch" + } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWhenCachesAreConfiguredTwice.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWhenCachesAreConfiguredTwice.gradle index 7b3c343aab7..40440edccfc 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWhenCachesAreConfiguredTwice.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWhenCachesAreConfiguredTwice.gradle @@ -12,10 +12,10 @@ bootBuildImage { builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2" buildCache { volume { - name = "build-cache-volume1" + name = "build-cache-volume" } - volume { - name = "build-cache-volum2" + bind { + name = "/tmp/build-cache-bind" } } } 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 0abcd2303d4..07b1f1aef6f 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 @@ -420,6 +420,13 @@ 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: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging-oci-image/bind-caches-pom.xml[tags=caches] +---- + [[build-image.examples.docker]] === Docker Configuration 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 new file mode 100644 index 00000000000..2cf4941fecb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/bind-caches-pom.xml @@ -0,0 +1,27 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + /tmp/cache-${project.artifactId}.build + + + + + /tmp/cache-${project.artifactId}.launch + + + + + + + + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java index b80cdc0bc44..8ea336c6a1d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java @@ -37,6 +37,7 @@ import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.docker.type.VolumeName; import org.springframework.boot.testsupport.junit.DisabledOnOs; import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable; +import org.springframework.util.FileSystemUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -385,19 +386,41 @@ class BuildImageTests extends AbstractArchiveIntegrationTests { @TestTemplate void whenBuildImageIsInvokedWithVolumeCaches(MavenBuild mavenBuild) { String testBuildId = randomString(); - mavenBuild.project("build-image-caches") + mavenBuild.project("build-image-volume-caches") .goals("package") .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT") .systemProperty("test-build-id", testBuildId) .execute((project) -> { assertThat(buildLog(project)).contains("Building image") - .contains("docker.io/library/build-image-caches:0.0.1.BUILD-SNAPSHOT") + .contains("docker.io/library/build-image-volume-caches:0.0.1.BUILD-SNAPSHOT") .contains("Successfully built image"); - removeImage("build-image-caches", "0.0.1.BUILD-SNAPSHOT"); + removeImage("build-image-volume-caches", "0.0.1.BUILD-SNAPSHOT"); deleteVolumes("cache-" + testBuildId + ".build", "cache-" + testBuildId + ".launch"); }); } + @TestTemplate + void whenBuildImageIsInvokedWithBindCaches(MavenBuild mavenBuild) { + String testBuildId = randomString(); + mavenBuild.project("build-image-bind-caches") + .goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT") + .systemProperty("test-build-id", testBuildId) + .execute((project) -> { + assertThat(buildLog(project)).contains("Building image") + .contains("docker.io/library/build-image-bind-caches:0.0.1.BUILD-SNAPSHOT") + .contains("Successfully built image"); + removeImage("build-image-bind-caches", "0.0.1.BUILD-SNAPSHOT"); + String tempDir = System.getProperty("java.io.tmpdir"); + Path buildCachePath = Paths.get(tempDir, "junit-image-cache-" + testBuildId + "-build"); + Path launchCachePath = Paths.get(tempDir, "junit-image-cache-" + testBuildId + "-launch"); + assertThat(buildCachePath).exists().isDirectory(); + assertThat(launchCachePath).exists().isDirectory(); + FileSystemUtils.deleteRecursively(buildCachePath); + FileSystemUtils.deleteRecursively(launchCachePath); + }); + } + @TestTemplate void whenBuildImageIsInvokedWithCreatedDate(MavenBuild mavenBuild) { String testBuildId = randomString(); 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 new file mode 100644 index 00000000000..349d1519e9e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bind-caches/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-bind-caches + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image-no-fork + + + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2 + + + ${java.io.tmpdir}/junit-image-cache-${test-build-id}-build + + + + + ${java.io.tmpdir}/junit-image-cache-${test-build-id}-launch + + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-caches/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bind-caches/src/main/java/org/test/SampleApplication.java similarity index 93% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-caches/src/main/java/org/test/SampleApplication.java rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bind-caches/src/main/java/org/test/SampleApplication.java index e964724deac..03544b74e46 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-caches/src/main/java/org/test/SampleApplication.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bind-caches/src/main/java/org/test/SampleApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-caches/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-volume-caches/pom.xml similarity index 96% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-caches/pom.xml rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-volume-caches/pom.xml index f95eb39f874..2b92c6dcb82 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-caches/pom.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-volume-caches/pom.xml @@ -3,7 +3,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.boot.maven.it - build-image-caches + build-image-volume-caches 0.0.1.BUILD-SNAPSHOT UTF-8 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-volume-caches/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-volume-caches/src/main/java/org/test/SampleApplication.java new file mode 100644 index 00000000000..03544b74e46 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-volume-caches/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2023 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java index 491deabe28e..a64c0387073 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -32,8 +32,8 @@ public class CacheInfo { public CacheInfo() { } - CacheInfo(VolumeCacheInfo volumeCacheInfo) { - this.cache = Cache.volume(volumeCacheInfo.getName()); + private CacheInfo(Cache cache) { + this.cache = cache; } public void setVolume(VolumeCacheInfo info) { @@ -41,10 +41,23 @@ public class CacheInfo { this.cache = Cache.volume(info.getName()); } + public void setBind(BindCacheInfo info) { + Assert.state(this.cache == null, "Each image building cache can be configured only once"); + this.cache = Cache.bind(info.getSource()); + } + Cache asCache() { return this.cache; } + static CacheInfo fromVolume(VolumeCacheInfo cacheInfo) { + return new CacheInfo(Cache.volume(cacheInfo.getName())); + } + + static CacheInfo fromBind(BindCacheInfo cacheInfo) { + return new CacheInfo(Cache.bind(cacheInfo.getSource())); + } + /** * Encapsulates configuration of an image building cache stored in a volume. */ @@ -69,4 +82,28 @@ public class CacheInfo { } + /** + * Encapsulates configuration of an image building cache stored in a bind mount. + */ + public static class BindCacheInfo { + + private String source; + + public BindCacheInfo() { + } + + BindCacheInfo(String name) { + this.source = name; + } + + public String getSource() { + return this.source; + } + + void setSource(String source) { + this.source = source; + } + + } + } 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 86625106bdb..ed5a8e5d8ed 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 @@ -34,6 +34,7 @@ import org.springframework.boot.buildpack.platform.docker.type.Binding; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.io.Owner; import org.springframework.boot.buildpack.platform.io.TarArchive; +import org.springframework.boot.maven.CacheInfo.BindCacheInfo; import org.springframework.boot.maven.CacheInfo.VolumeCacheInfo; import static org.assertj.core.api.Assertions.assertThat; @@ -170,21 +171,37 @@ class ImageTests { } @Test - void getBuildRequestWhenHasBuildVolumeCacheUsesCache() { + void getBuildRequestWhenHasBuildCacheVolumeUsesCache() { Image image = new Image(); - image.buildCache = new CacheInfo(new VolumeCacheInfo("build-cache-vol")); + image.buildCache = CacheInfo.fromVolume(new VolumeCacheInfo("build-cache-vol")); BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); assertThat(request.getBuildCache()).isEqualTo(Cache.volume("build-cache-vol")); } @Test - void getBuildRequestWhenHasLaunchVolumeCacheUsesCache() { + void getBuildRequestWhenHasLaunchCacheVolumeUsesCache() { Image image = new Image(); - image.launchCache = new CacheInfo(new VolumeCacheInfo("launch-cache-vol")); + image.launchCache = CacheInfo.fromVolume(new VolumeCacheInfo("launch-cache-vol")); BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); assertThat(request.getLaunchCache()).isEqualTo(Cache.volume("launch-cache-vol")); } + @Test + void getBuildRequestWhenHasBuildCacheBindUsesCache() { + Image image = new Image(); + image.buildCache = CacheInfo.fromBind(new BindCacheInfo("build-cache-dir")); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getBuildCache()).isEqualTo(Cache.bind("build-cache-dir")); + } + + @Test + void getBuildRequestWhenHasLaunchCacheBindUsesCache() { + Image image = new Image(); + image.launchCache = CacheInfo.fromBind(new BindCacheInfo("launch-cache-dir")); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getLaunchCache()).isEqualTo(Cache.bind("launch-cache-dir")); + } + @Test void getBuildRequestWhenHasCreatedDateUsesCreatedDate() { Image image = new Image();