From 5dafdb455f294f7a4c7facf80ab3831f5fae5a3b Mon Sep 17 00:00:00 2001 From: Tim Ysewyn Date: Tue, 30 Jun 2026 07:20:31 -0500 Subject: [PATCH] Add support for image-based build caches Added a new image cache option that uses the lifecycle's `-cache-image` argument for the creator, analyzer, restorer and exporter phases, so the build cache can be stored in an image and shared between hosts via an image registry. Exposed the option in the Maven (``) and Gradle (`buildCache { image { } }`) plugins. Reject image caches configured for the launch cache, which the lifecycle does not support. Signed-off-by: Tim Ysewyn --- .../boot-build-image-image-caches.gradle | 24 +++++++ .../boot-build-image-image-caches.gradle.kts | 22 ++++++ .../pages/packaging-oci-image.adoc | 18 +++++ .../boot/gradle/tasks/bundling/CacheSpec.java | 27 +++++++ .../docs/PackagingDocumentationTests.java | 7 ++ .../packaging-oci-image/image-caches-pom.xml | 22 ++++++ .../maven-plugin/pages/build-image.adoc | 7 ++ .../springframework/boot/maven/CacheInfo.java | 37 ++++++++++ .../boot/maven/ImageTests.java | 19 +++++ .../platform/build/BuildRequest.java | 1 + .../boot/buildpack/platform/build/Cache.java | 70 ++++++++++++++++++- .../buildpack/platform/build/Lifecycle.java | 21 +++++- .../boot/buildpack/platform/build/Phase.java | 4 ++ .../platform/build/BuildRequestTests.java | 8 +++ .../platform/build/LifecycleTests.java | 21 ++++++ .../buildpack/platform/build/PhaseTests.java | 11 +++ .../build/lifecycle-analyzer-image-cache.json | 33 +++++++++ .../build/lifecycle-creator-image-cache.json | 38 ++++++++++ .../build/lifecycle-exporter-image-cache.json | 34 +++++++++ .../build/lifecycle-restorer-image-cache.json | 27 +++++++ 20 files changed, 447 insertions(+), 4 deletions(-) create mode 100644 build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle create mode 100644 build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle.kts create mode 100644 build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging-oci-image/image-caches-pom.xml create mode 100644 buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-analyzer-image-cache.json create mode 100644 buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-image-cache.json create mode 100644 buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-exporter-image-cache.json create mode 100644 buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-restorer-image-cache.json diff --git a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle new file mode 100644 index 000000000000..14ffe5e14446 --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle @@ -0,0 +1,24 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version-spring-boot}' +} + +tasks.named("bootJar") { + mainClass = 'com.example.ExampleApplication' +} + +// tag::caches[] +tasks.named("bootBuildImage") { + buildCache { + image { + name = "docker.io/library/${rootProject.name}:build" + } + } +} +// end::caches[] + +tasks.register("bootBuildImageCaches") { + doFirst { + bootBuildImage.buildCache.asCache().with { println "buildCache=$name" } + } +} diff --git a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle.kts b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle.kts new file mode 100644 index 000000000000..ea20581a168f --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle.kts @@ -0,0 +1,22 @@ +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage + +plugins { + java + id("org.springframework.boot") version "{version-spring-boot}" +} + +// tag::caches[] +tasks.named("bootBuildImage") { + buildCache { + image { + name.set("docker.io/library/${rootProject.name}:build") + } + } +} +// end::caches[] + +tasks.register("bootBuildImageCaches") { + doFirst { + println("buildCache=" + tasks.getByName("bootBuildImage").buildCache.asCache()?.image?.name) + } +} diff --git a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc index 8e3b4bbfaded..9ffcbe13a296 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc +++ b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc @@ -573,6 +573,24 @@ include::example$packaging/boot-build-image-bind-caches.gradle.kts[tags=caches] ---- ====== +The build cache can also be stored in an image instead of a named volume, which allows the cache to be shared between hosts by pushing and pulling it from an image registry, as shown in the following example: + +[tabs] +====== +Groovy:: ++ +[source,groovy,indent=0,subs="verbatim,attributes"] +---- +include::example$packaging/boot-build-image-image-caches.gradle[tags=caches] +---- +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,attributes"] +---- +include::example$packaging/boot-build-image-image-caches.gradle.kts[tags=caches] +---- +====== + [[build-image.examples.docker]] diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java index b459b0cb205b..499f03fc754a 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java @@ -74,6 +74,19 @@ public void bind(Action action) { this.cache = Cache.bind(spec.getSource().get()); } + /** + * Configures an image cache using the given {@code action}. + * @param action the action + */ + public void image(Action action) { + if (this.cache != null) { + throw new GradleException("Each image building cache can be configured only once"); + } + ImageCacheSpec spec = this.objectFactory.newInstance(ImageCacheSpec.class); + action.execute(spec); + this.cache = Cache.image(spec.getName().get()); + } + /** * Configuration for an image building cache stored in a Docker volume. */ @@ -102,4 +115,18 @@ public abstract static class BindCacheSpec { } + /** + * Configuration for an image building cache stored in an image. + */ + public abstract static class ImageCacheSpec { + + /** + * Returns the name of the cache image. + * @return the cache image name + */ + @Input + public abstract Property getName(); + + } + } diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java index a9a3488d0efd..6de73280aea5 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java @@ -324,6 +324,13 @@ void bootBuildImageWithBindCaches() { .containsPattern("launchCache=/tmp/cache-gradle-[\\d]+.launch"); } + @TestTemplate + void bootBuildImageWithImageCaches() { + BuildResult result = this.gradleBuild.script(Examples.DIR + "packaging/boot-build-image-image-caches") + .build("bootBuildImageCaches"); + assertThat(result.getOutput()).containsPattern("buildCache=docker.io/library/gradle-[\\d]+:build"); + } + 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/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging-oci-image/image-caches-pom.xml b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging-oci-image/image-caches-pom.xml new file mode 100644 index 000000000000..ac8f689957dd --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging-oci-image/image-caches-pom.xml @@ -0,0 +1,22 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + docker.io/library/${project.artifactId}:build + + + + + + + + + \ No newline at end of file diff --git a/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc index e099dcef4ff3..0c492ca6b557 100644 --- a/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc +++ b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc @@ -494,6 +494,13 @@ The caches and the build workspace can be configured to use bind mounts instead include::example$packaging-oci-image/bind-caches-pom.xml[tags=caches] ---- +The build cache can also be stored in an image instead of a named volume, which allows the cache to be shared between hosts by pushing and pulling it from an image registry, as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes"] +---- +include::example$packaging-oci-image/image-caches-pom.xml[tags=caches] +---- + [[build-image.examples.docker]] diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java index 98d23db744bd..0a0df4d5c920 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java @@ -52,6 +52,13 @@ public void setBind(BindCacheInfo info) { this.cache = Cache.bind(source); } + public void setImage(ImageCacheInfo info) { + Assert.state(this.cache == null, "Each image building cache can be configured only once"); + String name = info.getName(); + Assert.state(name != null, "'name' must not be null"); + this.cache = Cache.image(name); + } + @Nullable Cache asCache() { return this.cache; } @@ -68,6 +75,12 @@ static CacheInfo fromBind(BindCacheInfo cacheInfo) { return new CacheInfo(Cache.bind(source)); } + static CacheInfo fromImage(ImageCacheInfo cacheInfo) { + String name = cacheInfo.getName(); + Assert.state(name != null, "'name' must not be null"); + return new CacheInfo(Cache.image(name)); + } + /** * Encapsulates configuration of an image building cache stored in a volume. */ @@ -116,4 +129,28 @@ void setSource(@Nullable String source) { } + /** + * Encapsulates configuration of an image building cache stored in an image. + */ + public static class ImageCacheInfo { + + private @Nullable String name; + + public ImageCacheInfo() { + } + + ImageCacheInfo(String name) { + this.name = name; + } + + public @Nullable String getName() { + return this.name; + } + + void setName(@Nullable String name) { + this.name = name; + } + + } + } diff --git a/build-plugin/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java b/build-plugin/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java index a6a2b4d55bac..e4c4dfb348b9 100644 --- a/build-plugin/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java +++ b/build-plugin/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java @@ -37,9 +37,11 @@ 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.ImageCacheInfo; import org.springframework.boot.maven.CacheInfo.VolumeCacheInfo; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.entry; import static org.mockito.Mockito.mock; @@ -245,6 +247,23 @@ void getBuildRequestWhenHasLaunchCacheBindUsesCache() { assertThat(request.getLaunchCache()).isEqualTo(Cache.bind("launch-cache-dir")); } + @Test + void getBuildRequestWhenHasBuildCacheImageUsesCache() { + Image image = new Image(); + image.buildCache = CacheInfo.fromImage(new ImageCacheInfo("build-cache-image")); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getBuildCache()).isEqualTo(Cache.image("build-cache-image")); + } + + @Test + void getBuildRequestWhenHasLaunchCacheImageThrowsException() { + Image image = new Image(); + image.launchCache = CacheInfo.fromImage(new ImageCacheInfo("launch-cache-image")); + assertThatIllegalArgumentException() + .isThrownBy(() -> image.getBuildRequest(createArtifact(), mockApplicationContent())) + .withMessage("Launch cache must not be an image cache"); + } + @Test void getBuildRequestWhenHasCreatedDateUsesCreatedDate() { Image image = new Image(); diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java index bdbf888926c2..d5f83e2f4ca2 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java @@ -423,6 +423,7 @@ public BuildRequest withBuildCache(Cache buildCache) { */ public BuildRequest withLaunchCache(Cache launchCache) { Assert.notNull(launchCache, "'launchCache' must not be null"); + Assert.isNull(launchCache.getImage(), "Launch cache must not be an image cache"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java index a3e4f98a815b..749ed1280747 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java @@ -45,7 +45,12 @@ public enum Format { /** * A cache stored as a bind mount. */ - BIND("bind mount"); + BIND("bind mount"), + + /** + * A cache stored as an image. + */ + IMAGE("image"); private final String description; @@ -81,6 +86,14 @@ public String getDescription() { return (this.format.equals(Format.BIND)) ? (Bind) this : null; } + /** + * Return the details of the cache if it is an image cache. + * @return the cache, or {@code null} if it is not an image cache + */ + public @Nullable Image getImage() { + return (this.format.equals(Format.IMAGE)) ? (Image) this : null; + } + /** * Create a new {@code Cache} that uses a volume with the provided name. * @param name the cache volume name @@ -111,6 +124,16 @@ public static Cache bind(String source) { return new Bind(source); } + /** + * Create a new {@code Cache} that uses an image with the provided name. + * @param name the cache image name + * @return a new cache instance + */ + public static Cache image(String name) { + Assert.notNull(name, "'name' must not be null"); + return new Image(name); + } + @Override public boolean equals(@Nullable Object obj) { if (this == obj) { @@ -222,4 +245,49 @@ public String toString() { } + /** + * Details of a cache stored in an image. + */ + public static class Image extends Cache { + + private final String name; + + Image(String name) { + super(Format.IMAGE); + this.name = name; + } + + public String getName() { + return this.name; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + if (!super.equals(obj)) { + return false; + } + Image other = (Image) obj; + return Objects.equals(this.name, other.name); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.name); + return result; + } + + @Override + public String toString() { + return this.format.getDescription() + " '" + this.name + "'"; + } + + } + } diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java index b5467db65173..1c1598524fce 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java @@ -195,7 +195,7 @@ private Phase createPhase() { Assert.state(runImage != null, "'runImage' must not be null"); phase.withRunImage(runImage); phase.withLayers(Directory.LAYERS, Binding.from(getCacheBindingSource(this.layers), Directory.LAYERS)); - phase.withBuildCache(Directory.CACHE, Binding.from(getCacheBindingSource(this.buildCache), Directory.CACHE)); + configureBuildCache(phase); phase.withLaunchCache(Directory.LAUNCH_CACHE, Binding.from(getCacheBindingSource(this.launchCache), Directory.LAUNCH_CACHE)); configureDaemonAccess(phase); @@ -215,6 +215,10 @@ private Phase createPhase() { private Phase analyzePhase() { Phase phase = new Phase("analyzer", isVerboseLogging()); configureDaemonAccess(phase); + Cache.Image buildCacheImage = this.buildCache.getImage(); + if (buildCacheImage != null) { + phase.withBuildCache(buildCacheImage.getName()); + } phase.withLaunchCache(Directory.LAUNCH_CACHE, Binding.from(getCacheBindingSource(this.launchCache), Directory.LAUNCH_CACHE)); phase.withLayers(Directory.LAYERS, Binding.from(getCacheBindingSource(this.layers), Directory.LAYERS)); @@ -239,7 +243,7 @@ private Phase detectPhase() { private Phase restorePhase() { Phase phase = new Phase("restorer", isVerboseLogging()); configureDaemonAccess(phase); - phase.withBuildCache(Directory.CACHE, Binding.from(getCacheBindingSource(this.buildCache), Directory.CACHE)); + configureBuildCache(phase); phase.withLayers(Directory.LAYERS, Binding.from(getCacheBindingSource(this.layers), Directory.LAYERS)); configureOptions(phase); return phase; @@ -260,7 +264,7 @@ private Phase exportPhase() { configureDaemonAccess(phase); phase.withApp(this.applicationDirectory, Binding.from(getCacheBindingSource(this.application), this.applicationDirectory)); - phase.withBuildCache(Directory.CACHE, Binding.from(getCacheBindingSource(this.buildCache), Directory.CACHE)); + configureBuildCache(phase); phase.withLaunchCache(Directory.LAUNCH_CACHE, Binding.from(getCacheBindingSource(this.launchCache), Directory.LAUNCH_CACHE)); phase.withLayers(Directory.LAYERS, Binding.from(getCacheBindingSource(this.layers), Directory.LAYERS)); @@ -343,6 +347,17 @@ private void configureDaemonAccess(Phase phase) { } } + private void configureBuildCache(Phase phase) { + Cache.Image image = this.buildCache.getImage(); + if (image != null) { + phase.withBuildCache(image.getName()); + } + else { + phase.withBuildCache(Directory.CACHE, + Binding.from(getCacheBindingSource(this.buildCache), Directory.CACHE)); + } + } + private void configureCreatedDate(Phase phase) { if (this.request.getCreatedDate() != null) { phase.withEnv(SOURCE_DATE_EPOCH_KEY, Long.toString(this.request.getCreatedDate().getEpochSecond())); diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Phase.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Phase.java index d037d33b072e..985e6992b875 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Phase.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Phase.java @@ -75,6 +75,10 @@ void withBuildCache(String path, Binding binding) { withBinding(binding); } + void withBuildCache(String image) { + withArgs("-cache-image", image); + } + /** * Update this phase with Docker daemon access. */ diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java index 7bbb13d3e72a..dab8563813ae 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java @@ -349,6 +349,14 @@ void withLaunchBindCacheAddsCache() throws IOException { assertThat(withCache.getLaunchCache()).isEqualTo(Cache.bind("/tmp/launch-cache")); } + @Test + void withLaunchImageCacheThrowsException() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + assertThatIllegalArgumentException() + .isThrownBy(() -> request.withLaunchCache(Cache.image("launch-cache-image"))) + .withMessage("Launch cache must not be an image cache"); + } + @Test @SuppressWarnings("NullAway") // Test null check void withLaunchVolumeCacheWhenCacheIsNullThrowsException() throws IOException { diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java index 69c8b8f477b3..eac76947b9fb 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java @@ -291,6 +291,27 @@ void executeWithCacheBindMountsExecutesPhases(boolean trustBuilder) throws Excep assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); } + @ParameterizedTest + @BooleanValueSource + void executeWithImageBuildCacheExecutesPhases(boolean trustBuilder) throws Exception { + given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); + BuildRequest request = getTestRequest(trustBuilder).withBuildCache(Cache.image("my-cache-image")); + createLifecycle(request).execute(); + if (trustBuilder) { + assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-image-cache.json")); + } + else { + assertPhaseWasRun("analyzer", withExpectedConfig("lifecycle-analyzer-image-cache.json")); + assertPhaseWasRun("detector", withExpectedConfig("lifecycle-detector.json")); + assertPhaseWasRun("restorer", withExpectedConfig("lifecycle-restorer-image-cache.json")); + assertPhaseWasRun("builder", withExpectedConfig("lifecycle-builder.json")); + assertPhaseWasRun("exporter", withExpectedConfig("lifecycle-exporter-image-cache.json")); + } + assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + } + @ParameterizedTest @BooleanValueSource void executeWithCreatedDateExecutesPhases(boolean trustBuilder) throws Exception { diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PhaseTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PhaseTests.java index 42d06b41f148..0771fed79281 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PhaseTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PhaseTests.java @@ -102,6 +102,17 @@ void applyWhenWithArgsUpdatesConfigurationWithArguments() { then(update).shouldHaveNoMoreInteractions(); } + @Test + void applyWhenWithImageBuildCacheUpdatesConfigurationWithCacheImage() { + Phase phase = new Phase("test", false); + phase.withBuildCache("my-cache-image"); + Update update = mock(Update.class); + phase.apply(update); + then(update).should().withCommand("/cnb/lifecycle/test", "-cache-image", "my-cache-image"); + then(update).should().withLabel("author", "spring-boot"); + then(update).shouldHaveNoMoreInteractions(); + } + @Test void applyWhenWithBindsUpdatesConfigurationWithBinds() { Phase phase = new Phase("test", false); diff --git a/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-analyzer-image-cache.json b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-analyzer-image-cache.json new file mode 100644 index 000000000000..e18ed997905c --- /dev/null +++ b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-analyzer-image-cache.json @@ -0,0 +1,33 @@ +{ + "User": "root", + "Image": "pack.local/ephemeral-builder", + "Cmd": [ + "/cnb/lifecycle/analyzer", + "-daemon", + "-cache-image", + "my-cache-image", + "-launch-cache", + "/launch-cache", + "-layers", + "/layers", + "-run-image", + "docker.io/cloudfoundry/run:latest", + "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-cache-b35197ac41ea.launch:/launch-cache", + "pack-layers-aaaaaaaaaa:/layers" + ], + "SecurityOpt" : [ + "label=disable" + ] + } +} diff --git a/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-image-cache.json b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-image-cache.json new file mode 100644 index 000000000000..babbd120c8ef --- /dev/null +++ b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-image-cache.json @@ -0,0 +1,38 @@ +{ + "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-image", + "my-cache-image", + "-launch-cache", + "/launch-cache", + "-daemon", + "docker.io/library/my-application:latest" + ], + "Env": [ + "CNB_PLATFORM_API=0.8" + ], + "Labels": { + "author": "spring-boot" + }, + "HostConfig": { + "Binds": [ + "pack-app-aaaaaaaaaa:/workspace", + "pack-layers-aaaaaaaaaa:/layers", + "pack-cache-b35197ac41ea.launch:/launch-cache", + "/var/run/docker.sock:/var/run/docker.sock" + ], + "SecurityOpt" : [ + "label=disable" + ] + } +} diff --git a/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-exporter-image-cache.json b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-exporter-image-cache.json new file mode 100644 index 000000000000..34a4f76b2cad --- /dev/null +++ b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-exporter-image-cache.json @@ -0,0 +1,34 @@ +{ + "User": "root", + "Image": "pack.local/ephemeral-builder", + "Cmd": [ + "/cnb/lifecycle/exporter", + "-daemon", + "-app", + "/workspace", + "-cache-image", + "my-cache-image", + "-launch-cache", + "/launch-cache", + "-layers", + "/layers", + "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-app-aaaaaaaaaa:/workspace", + "pack-cache-b35197ac41ea.launch:/launch-cache", + "pack-layers-aaaaaaaaaa:/layers" + ], + "SecurityOpt" : [ + "label=disable" + ] + } +} diff --git a/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-restorer-image-cache.json b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-restorer-image-cache.json new file mode 100644 index 000000000000..a4e74529206a --- /dev/null +++ b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-restorer-image-cache.json @@ -0,0 +1,27 @@ +{ + "User": "root", + "Image": "pack.local/ephemeral-builder", + "Cmd": [ + "/cnb/lifecycle/restorer", + "-daemon", + "-cache-image", + "my-cache-image", + "-layers", + "/layers" + ], + "Env": [ + "CNB_PLATFORM_API=0.8" + ], + "Labels": { + "author": "spring-boot" + }, + "HostConfig": { + "Binds": [ + "/var/run/docker.sock:/var/run/docker.sock", + "pack-layers-aaaaaaaaaa:/layers" + ], + "SecurityOpt" : [ + "label=disable" + ] + } +}