diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java index 078180fafae..09939ea281a 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java @@ -47,6 +47,7 @@ import org.springframework.boot.buildpack.platform.build.Builder; import org.springframework.boot.buildpack.platform.build.BuildpackReference; import org.springframework.boot.buildpack.platform.build.Cache; import org.springframework.boot.buildpack.platform.build.Creator; +import org.springframework.boot.buildpack.platform.build.LocalCache; import org.springframework.boot.buildpack.platform.build.PullPolicy; import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException; import org.springframework.boot.buildpack.platform.docker.type.Binding; @@ -75,11 +76,11 @@ public abstract class BootBuildImage extends DefaultTask { private final String projectName; - private final CacheSpec buildWorkspace; + private final LocalCacheSpec buildWorkspace; private final CacheSpec buildCache; - private final CacheSpec launchCache; + private final LocalCacheSpec launchCache; private final DockerSpec docker; @@ -100,9 +101,9 @@ public abstract class BootBuildImage extends DefaultTask { getCleanCache().convention(false); getVerboseLogging().convention(false); getPublish().convention(false); - this.buildWorkspace = getProject().getObjects().newInstance(CacheSpec.class); + this.buildWorkspace = getProject().getObjects().newInstance(LocalCacheSpec.class); this.buildCache = getProject().getObjects().newInstance(CacheSpec.class); - this.launchCache = getProject().getObjects().newInstance(CacheSpec.class); + this.launchCache = getProject().getObjects().newInstance(LocalCacheSpec.class); this.docker = getProject().getObjects().newInstance(DockerSpec.class); this.pullPolicy = getProject().getObjects().property(PullPolicy.class); getSecurityOptions().convention((Iterable) null); @@ -280,17 +281,17 @@ public abstract class BootBuildImage extends DefaultTask { */ @Nested @Optional - public CacheSpec getBuildWorkspace() { + public LocalCacheSpec getBuildWorkspace() { return this.buildWorkspace; } /** - * Customizes the {@link CacheSpec} for the build temporary workspace using the given - * {@code action}. + * Customizes the {@link LocalCacheSpec} for the build temporary workspace using the + * given {@code action}. * @param action the action * @since 3.2.0 */ - public void buildWorkspace(Action action) { + public void buildWorkspace(Action action) { action.execute(this.buildWorkspace); } @@ -319,16 +320,16 @@ public abstract class BootBuildImage extends DefaultTask { */ @Nested @Optional - public CacheSpec getLaunchCache() { + public LocalCacheSpec getLaunchCache() { return this.launchCache; } /** - * Customizes the {@link CacheSpec} for the launch cache using the given + * Customizes the {@link LocalCacheSpec} for the launch cache using the given * {@code action}. * @param action the action */ - public void launchCache(Action action) { + public void launchCache(Action action) { action.execute(this.launchCache); } @@ -495,7 +496,7 @@ public abstract class BootBuildImage extends DefaultTask { } private BuildRequest customizeCaches(BuildRequest request) { - Cache buildWorkspaceCache = this.buildWorkspace.asCache(); + LocalCache buildWorkspaceCache = this.buildWorkspace.asCache(); if (buildWorkspaceCache != null) { request = request.withBuildWorkspace(buildWorkspaceCache); } @@ -503,7 +504,7 @@ public abstract class BootBuildImage extends DefaultTask { if (buildCache != null) { request = request.withBuildCache(buildCache); } - Cache launchCache = this.launchCache.asCache(); + LocalCache launchCache = this.launchCache.asCache(); if (launchCache != null) { request = request.withLaunchCache(launchCache); } 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 499f03fc754..303b1b63556 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 @@ -26,6 +26,8 @@ import org.gradle.api.tasks.Input; import org.jspecify.annotations.Nullable; import org.springframework.boot.buildpack.platform.build.Cache; +import org.springframework.boot.gradle.tasks.bundling.LocalCacheSpec.BindCacheSpec; +import org.springframework.boot.gradle.tasks.bundling.LocalCacheSpec.VolumeCacheSpec; /** * Configuration for an image building cache. @@ -77,6 +79,7 @@ public class CacheSpec { /** * Configures an image cache using the given {@code action}. * @param action the action + * @since 4.2.0 */ public void image(Action action) { if (this.cache != null) { @@ -87,34 +90,6 @@ public class CacheSpec { this.cache = Cache.image(spec.getName().get()); } - /** - * Configuration for an image building cache stored in a Docker volume. - */ - public abstract static class VolumeCacheSpec { - - /** - * Returns the name of the cache. - * @return the cache name - */ - @Input - public abstract Property getName(); - - } - - /** - * 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(); - - } - /** * Configuration for an image building cache stored in an image. */ diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LocalCacheSpec.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LocalCacheSpec.java new file mode 100644 index 00000000000..0283f77546e --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LocalCacheSpec.java @@ -0,0 +1,107 @@ +/* + * Copyright 2012-present 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.springframework.boot.gradle.tasks.bundling; + +import javax.inject.Inject; + +import org.gradle.api.Action; +import org.gradle.api.GradleException; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.buildpack.platform.build.Cache; +import org.springframework.boot.buildpack.platform.build.LocalCache; + +/** + * Configuration for a local image building cache stored in a volume or bind mount. + * + * @author Scott Frederick + * @author Stephane Nicoll + * @since 4.2.0 + */ +public class LocalCacheSpec { + + private final ObjectFactory objectFactory; + + private @Nullable LocalCache cache; + + @Inject + public LocalCacheSpec(ObjectFactory objectFactory) { + this.objectFactory = objectFactory; + } + + public @Nullable LocalCache asCache() { + return this.cache; + } + + /** + * Configures a volume cache using the given {@code action}. + * @param action the action + */ + public void volume(Action action) { + if (this.cache != null) { + throw new GradleException("Each image building cache can be configured only once"); + } + VolumeCacheSpec spec = this.objectFactory.newInstance(VolumeCacheSpec.class); + action.execute(spec); + 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. + */ + public abstract static class VolumeCacheSpec { + + /** + * Returns the name of the cache. + * @return the cache name + */ + @Input + public abstract Property getName(); + + } + + /** + * 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/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java index 9a917d02382..ce72a74808e 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java @@ -29,6 +29,7 @@ import org.junit.jupiter.api.io.TempDir; import org.springframework.boot.buildpack.platform.build.BuildRequest; import org.springframework.boot.buildpack.platform.build.BuildpackReference; +import org.springframework.boot.buildpack.platform.build.Cache; import org.springframework.boot.buildpack.platform.build.PullPolicy; import org.springframework.boot.buildpack.platform.docker.ImagePlatform; import org.springframework.boot.buildpack.platform.docker.type.Binding; @@ -357,4 +358,28 @@ class BootBuildImageTests { assertThat(this.buildImage.createRequest().getImagePlatform()).isEqualTo(ImagePlatform.of("linux/arm64/v1")); } + @Test + void whenBuildWorkspaceIsConfiguredThenRequestHasBuildWorkspace() { + this.buildImage.buildWorkspace((cache) -> cache.volume((volume) -> volume.getName().set("workspace-vol"))); + assertThat(this.buildImage.createRequest().getBuildWorkspace()).isEqualTo(Cache.volume("workspace-vol")); + } + + @Test + void whenBuildCacheIsConfiguredWithVolumeThenRequestHasBuildCache() { + this.buildImage.buildCache((cache) -> cache.volume((volume) -> volume.getName().set("build-vol"))); + assertThat(this.buildImage.createRequest().getBuildCache()).isEqualTo(Cache.volume("build-vol")); + } + + @Test + void whenBuildCacheIsConfiguredWithImageThenRequestHasBuildCache() { + this.buildImage.buildCache((cache) -> cache.image((image) -> image.getName().set("build-cache-image"))); + assertThat(this.buildImage.createRequest().getBuildCache()).isEqualTo(Cache.image("build-cache-image")); + } + + @Test + void whenLaunchCacheIsConfiguredThenRequestHasLaunchCache() { + this.buildImage.launchCache((cache) -> cache.bind((bind) -> bind.getSource().set("/tmp/launch"))); + assertThat(this.buildImage.createRequest().getLaunchCache()).isEqualTo(Cache.bind("/tmp/launch")); + } + } 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 index ac8f689957d..72e273228a2 100644 --- 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 @@ -19,4 +19,4 @@ - \ No newline at end of file + 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 0a0df4d5c92..32a05ea6d44 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 @@ -19,6 +19,8 @@ package org.springframework.boot.maven; import org.jspecify.annotations.Nullable; import org.springframework.boot.buildpack.platform.build.Cache; +import org.springframework.boot.maven.LocalCacheInfo.BindCacheInfo; +import org.springframework.boot.maven.LocalCacheInfo.VolumeCacheInfo; import org.springframework.util.Assert; /** @@ -81,54 +83,6 @@ public class CacheInfo { return new CacheInfo(Cache.image(name)); } - /** - * Encapsulates configuration of an image building cache stored in a volume. - */ - public static class VolumeCacheInfo { - - private @Nullable String name; - - public VolumeCacheInfo() { - } - - VolumeCacheInfo(String name) { - this.name = name; - } - - public @Nullable String getName() { - return this.name; - } - - void setName(@Nullable String name) { - this.name = name; - } - - } - - /** - * Encapsulates configuration of an image building cache stored in a bind mount. - */ - public static class BindCacheInfo { - - private @Nullable String source; - - public BindCacheInfo() { - } - - BindCacheInfo(String name) { - this.source = name; - } - - public @Nullable String getSource() { - return this.source; - } - - void setSource(@Nullable String source) { - this.source = source; - } - - } - /** * Encapsulates configuration of an image building cache stored in an image. */ diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java index 42e7f7ca4cd..3b359cc4a89 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java @@ -26,6 +26,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.boot.buildpack.platform.build.BuildRequest; import org.springframework.boot.buildpack.platform.build.BuildpackReference; import org.springframework.boot.buildpack.platform.build.Cache; +import org.springframework.boot.buildpack.platform.build.LocalCache; import org.springframework.boot.buildpack.platform.build.PullPolicy; import org.springframework.boot.buildpack.platform.docker.type.Binding; import org.springframework.boot.buildpack.platform.docker.type.ImageName; @@ -74,11 +75,11 @@ public class Image { @Nullable List tags; - @Nullable CacheInfo buildWorkspace; + @Nullable LocalCacheInfo buildWorkspace; @Nullable CacheInfo buildCache; - @Nullable CacheInfo launchCache; + @Nullable LocalCacheInfo launchCache; @Nullable String createdDate; @@ -284,7 +285,7 @@ public class Image { request = request.withTags(this.tags.stream().map(ImageReference::of).toList()); } if (this.buildWorkspace != null) { - Cache cache = this.buildWorkspace.asCache(); + LocalCache cache = this.buildWorkspace.asCache(); Assert.state(cache != null, "'cache' must not be null"); request = request.withBuildWorkspace(cache); } @@ -294,7 +295,7 @@ public class Image { request = request.withBuildCache(cache); } if (this.launchCache != null) { - Cache cache = this.launchCache.asCache(); + LocalCache cache = this.launchCache.asCache(); Assert.state(cache != null, "'cache' must not be null"); request = request.withLaunchCache(cache); } diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LocalCacheInfo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LocalCacheInfo.java new file mode 100644 index 00000000000..8d088f24675 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LocalCacheInfo.java @@ -0,0 +1,121 @@ +/* + * Copyright 2012-present 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.springframework.boot.maven; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.buildpack.platform.build.Cache; +import org.springframework.boot.buildpack.platform.build.LocalCache; +import org.springframework.util.Assert; + +/** + * Encapsulates configuration of a local image building cache. + * + * @author Scott Frederick + * @author Stephane Nicoll + * @since 4.2.0 + */ +public class LocalCacheInfo { + + private @Nullable LocalCache cache; + + public LocalCacheInfo() { + } + + LocalCacheInfo(LocalCache cache) { + this.cache = cache; + } + + public void setVolume(VolumeCacheInfo 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.volume(name); + } + + public void setBind(BindCacheInfo info) { + Assert.state(this.cache == null, "Each image building cache can be configured only once"); + String source = info.getSource(); + Assert.state(source != null, "'source' must not be null"); + this.cache = Cache.bind(source); + } + + @Nullable LocalCache asCache() { + return this.cache; + } + + static LocalCacheInfo fromVolume(VolumeCacheInfo cacheInfo) { + String name = cacheInfo.getName(); + Assert.state(name != null, "'name' must not be null"); + return new LocalCacheInfo(Cache.volume(name)); + } + + static LocalCacheInfo fromBind(BindCacheInfo cacheInfo) { + String source = cacheInfo.getSource(); + Assert.state(source != null, "'source' must not be null"); + return new LocalCacheInfo(Cache.bind(source)); + } + + /** + * Encapsulates configuration of an image building cache stored in a volume. + */ + public static class VolumeCacheInfo { + + private @Nullable String name; + + public VolumeCacheInfo() { + } + + VolumeCacheInfo(String name) { + this.name = name; + } + + public @Nullable String getName() { + return this.name; + } + + void setName(@Nullable String name) { + this.name = name; + } + + } + + /** + * Encapsulates configuration of an image building cache stored in a bind mount. + */ + public static class BindCacheInfo { + + private @Nullable String source; + + public BindCacheInfo() { + } + + BindCacheInfo(String name) { + this.source = name; + } + + public @Nullable String getSource() { + return this.source; + } + + void setSource(@Nullable String source) { + this.source = source; + } + + } + +} 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 e4c4dfb348b..2e0c7b7a180 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 @@ -36,12 +36,11 @@ 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.ImageCacheInfo; -import org.springframework.boot.maven.CacheInfo.VolumeCacheInfo; +import org.springframework.boot.maven.LocalCacheInfo.BindCacheInfo; +import org.springframework.boot.maven.LocalCacheInfo.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; @@ -202,7 +201,7 @@ class ImageTests { @Test void getBuildRequestWhenHasBuildWorkspaceVolumeUsesWorkspace() { Image image = new Image(); - image.buildWorkspace = CacheInfo.fromVolume(new VolumeCacheInfo("build-work-vol")); + image.buildWorkspace = LocalCacheInfo.fromVolume(new VolumeCacheInfo("build-work-vol")); BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); assertThat(request.getBuildWorkspace()).isEqualTo(Cache.volume("build-work-vol")); } @@ -218,7 +217,7 @@ class ImageTests { @Test void getBuildRequestWhenHasLaunchCacheVolumeUsesCache() { Image image = new Image(); - image.launchCache = CacheInfo.fromVolume(new VolumeCacheInfo("launch-cache-vol")); + image.launchCache = LocalCacheInfo.fromVolume(new VolumeCacheInfo("launch-cache-vol")); BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); assertThat(request.getLaunchCache()).isEqualTo(Cache.volume("launch-cache-vol")); } @@ -226,7 +225,7 @@ class ImageTests { @Test void getBuildRequestWhenHasBuildWorkspaceBindUsesWorkspace() { Image image = new Image(); - image.buildWorkspace = CacheInfo.fromBind(new BindCacheInfo("build-work-dir")); + image.buildWorkspace = LocalCacheInfo.fromBind(new BindCacheInfo("build-work-dir")); BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); assertThat(request.getBuildWorkspace()).isEqualTo(Cache.bind("build-work-dir")); } @@ -242,7 +241,7 @@ class ImageTests { @Test void getBuildRequestWhenHasLaunchCacheBindUsesCache() { Image image = new Image(); - image.launchCache = CacheInfo.fromBind(new BindCacheInfo("launch-cache-dir")); + image.launchCache = LocalCacheInfo.fromBind(new BindCacheInfo("launch-cache-dir")); BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); assertThat(request.getLaunchCache()).isEqualTo(Cache.bind("launch-cache-dir")); } @@ -255,15 +254,6 @@ class ImageTests { 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 d5f83e2f4ca..f3f996311da 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 @@ -95,11 +95,11 @@ public class BuildRequest { private final List tags; - private final @Nullable Cache buildWorkspace; + private final @Nullable LocalCache buildWorkspace; private final @Nullable Cache buildCache; - private final @Nullable Cache launchCache; + private final @Nullable LocalCache launchCache; private final @Nullable Instant createdDate; @@ -140,8 +140,8 @@ public class BuildRequest { @Nullable Boolean trustBuilder, @Nullable ImageReference runImage, Creator creator, Map env, boolean cleanCache, boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List buildpacks, List bindings, @Nullable String network, - List tags, @Nullable Cache buildWorkspace, @Nullable Cache buildCache, - @Nullable Cache launchCache, @Nullable Instant createdDate, @Nullable String applicationDirectory, + List tags, @Nullable LocalCache buildWorkspace, @Nullable Cache buildCache, + @Nullable LocalCache launchCache, @Nullable Instant createdDate, @Nullable String applicationDirectory, @Nullable List securityOptions, @Nullable ImagePlatform platform) { this.name = name; this.applicationContent = applicationContent; @@ -395,7 +395,7 @@ public class BuildRequest { * @return an updated build request * @since 3.2.0 */ - public BuildRequest withBuildWorkspace(Cache buildWorkspace) { + public BuildRequest withBuildWorkspace(LocalCache buildWorkspace) { Assert.notNull(buildWorkspace, "'buildWorkspace' must not be null"); 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, @@ -421,9 +421,8 @@ public class BuildRequest { * @param launchCache the cache * @return an updated build request */ - public BuildRequest withLaunchCache(Cache launchCache) { + public BuildRequest withLaunchCache(LocalCache 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, @@ -633,7 +632,7 @@ public class BuildRequest { * @return the build workspace or {@code null} * @since 3.2.0 */ - public @Nullable Cache getBuildWorkspace() { + public @Nullable LocalCache getBuildWorkspace() { return this.buildWorkspace; } @@ -649,7 +648,7 @@ public class BuildRequest { * Return the custom launch cache that should be used by the lifecycle. * @return the launch cache */ - public @Nullable Cache getLaunchCache() { + public @Nullable LocalCache getLaunchCache() { return this.launchCache; } 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 749ed128074..ce4c20032f5 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 @@ -16,82 +16,46 @@ package org.springframework.boot.buildpack.platform.build; -import java.util.Objects; - import org.jspecify.annotations.Nullable; +import org.springframework.boot.buildpack.platform.build.LocalCache.Bind; +import org.springframework.boot.buildpack.platform.build.LocalCache.Volume; import org.springframework.boot.buildpack.platform.docker.type.VolumeName; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; /** * Details of a cache for use by the CNB builder. * * @author Scott Frederick + * @author Tim Ysewyn + * @author Stephane Nicoll * @since 2.6.0 */ -public class Cache { - - /** - * The format of the cache. - */ - public enum Format { - - /** - * A cache stored as a volume in the Docker daemon. - */ - VOLUME("volume"), - - /** - * A cache stored as a bind mount. - */ - BIND("bind mount"), - - /** - * A cache stored as an image. - */ - IMAGE("image"); - - private final String description; - - Format(String description) { - this.description = description; - } - - public String getDescription() { - return this.description; - } - - } - - protected final Format format; - - Cache(Format format) { - this.format = format; - } +public sealed interface Cache permits LocalCache, ImageCache { /** * Return the details of the cache if it is a volume cache. * @return the cache, or {@code null} if it is not a volume cache */ - public @Nullable Volume getVolume() { - return (this.format.equals(Format.VOLUME)) ? (Volume) this : null; + default LocalCache.@Nullable Volume getVolume() { + return (this instanceof LocalCache.Volume volume) ? volume : 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 @Nullable Bind getBind() { - return (this.format.equals(Format.BIND)) ? (Bind) this : null; + default LocalCache.@Nullable Bind getBind() { + return (this instanceof LocalCache.Bind bind) ? bind : 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 + * @since 4.2.0 */ - public @Nullable Image getImage() { - return (this.format.equals(Format.IMAGE)) ? (Image) this : null; + default @Nullable ImageCache getImage() { + return (this instanceof ImageCache image) ? image : null; } /** @@ -99,7 +63,7 @@ public class Cache { * @param name the cache volume name * @return a new cache instance */ - public static Cache volume(String name) { + static LocalCache volume(String name) { Assert.notNull(name, "'name' must not be null"); return new Volume(VolumeName.of(name)); } @@ -109,7 +73,7 @@ public class Cache { * @param name the cache volume name * @return a new cache instance */ - public static Cache volume(VolumeName name) { + static LocalCache volume(VolumeName name) { Assert.notNull(name, "'name' must not be null"); return new Volume(name); } @@ -119,7 +83,7 @@ public class Cache { * @param source the cache bind mount source * @return a new cache instance */ - public static Cache bind(String source) { + static LocalCache bind(String source) { Assert.notNull(source, "'source' must not be null"); return new Bind(source); } @@ -128,166 +92,11 @@ public class Cache { * Create a new {@code Cache} that uses an image with the provided name. * @param name the cache image name * @return a new cache instance + * @since 4.2.0 */ - public static Cache image(String name) { + static ImageCache 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) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - Cache other = (Cache) obj; - return Objects.equals(this.format, other.format); - } - - @Override - public int hashCode() { - return ObjectUtils.nullSafeHashCode(this.format); - } - - /** - * Details of a cache stored in a Docker volume. - */ - public static class Volume extends Cache { - - private final VolumeName name; - - Volume(VolumeName name) { - super(Format.VOLUME); - this.name = name; - } - - public String getName() { - return this.name.toString(); - } - - public VolumeName getVolumeName() { - 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; - } - Volume other = (Volume) 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 + "'"; - } - - } - - /** - * 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(@Nullable 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 + "'"; - } - - } - - /** - * 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 + "'"; - } - + return new ImageCache(name); } } diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageCache.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageCache.java new file mode 100644 index 00000000000..98f8a3a4eed --- /dev/null +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageCache.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-present 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.springframework.boot.buildpack.platform.build; + +import java.util.Objects; + +import org.jspecify.annotations.Nullable; + +import org.springframework.util.ObjectUtils; + +/** + * Details of a cache stored in an image. + * + * @author Tim Ysewyn + * @author Stephane Nicoll + * @since 4.2.0 + */ +public final class ImageCache implements Cache { + + private final String name; + + ImageCache(String name) { + 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; + } + ImageCache other = (ImageCache) obj; + return Objects.equals(this.name, other.name); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.name); + } + + @Override + public String toString() { + return "image '" + 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 1c1598524fc..a4b1dc841f0 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 @@ -27,7 +27,6 @@ import java.util.function.Consumer; import com.sun.jna.Platform; import org.jspecify.annotations.Nullable; -import org.springframework.boot.buildpack.platform.build.Cache.Bind; import org.springframework.boot.buildpack.platform.docker.ApiVersion; import org.springframework.boot.buildpack.platform.docker.DockerApi; import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent; @@ -78,13 +77,13 @@ class Lifecycle implements Closeable { private final ApiVersion platformVersion; - private final Cache layers; + private final LocalCache layers; - private final Cache application; + private final LocalCache application; private final Cache buildCache; - private final Cache launchCache; + private final LocalCache launchCache; private final String applicationDirectory; @@ -130,7 +129,7 @@ class Lifecycle implements Closeable { return createVolumeCache(request, "build"); } - private Cache getLaunchCache(BuildRequest request) { + private LocalCache getLaunchCache(BuildRequest request) { if (request.getLaunchCache() != null) { return request.getLaunchCache(); } @@ -215,9 +214,8 @@ class Lifecycle implements Closeable { private Phase analyzePhase() { Phase phase = new Phase("analyzer", isVerboseLogging()); configureDaemonAccess(phase); - Cache.Image buildCacheImage = this.buildCache.getImage(); - if (buildCacheImage != null) { - phase.withBuildCache(buildCacheImage.getName()); + if (this.buildCache instanceof ImageCache imageBuildCache) { + phase.withBuildCache(imageBuildCache.getName()); } phase.withLaunchCache(Directory.LAUNCH_CACHE, Binding.from(getCacheBindingSource(this.launchCache), Directory.LAUNCH_CACHE)); @@ -277,44 +275,43 @@ class Lifecycle implements Closeable { return phase; } - private Cache getLayersBindingSource(BuildRequest request) { + private LocalCache getLayersBindingSource(BuildRequest request) { if (request.getBuildWorkspace() != null) { return getBuildWorkspaceBindingSource(request.getBuildWorkspace(), "layers"); } return createVolumeCache("pack-layers-"); } - private Cache getApplicationBindingSource(BuildRequest request) { + private LocalCache getApplicationBindingSource(BuildRequest request) { if (request.getBuildWorkspace() != null) { return getBuildWorkspaceBindingSource(request.getBuildWorkspace(), "app"); } return createVolumeCache("pack-app-"); } - private Cache getBuildWorkspaceBindingSource(Cache buildWorkspace, String suffix) { + private LocalCache getBuildWorkspaceBindingSource(LocalCache buildWorkspace, String suffix) { if (buildWorkspace.getVolume() != null) { return Cache.volume(buildWorkspace.getVolume().getName() + "-" + suffix); } - - Bind bind = buildWorkspace.getBind(); + LocalCache.Bind bind = buildWorkspace.getBind(); Assert.state(bind != null, "'bind' must not be null"); return Cache.bind(bind.getSource() + "-" + suffix); } - private String getCacheBindingSource(Cache cache) { + private String getCacheBindingSource(LocalCache cache) { if (cache.getVolume() != null) { return cache.getVolume().getName(); } - Bind bind = cache.getBind(); + LocalCache.Bind bind = cache.getBind(); Assert.state(bind != null, "'bind' must not be null"); return bind.getSource(); } - private Cache createVolumeCache(String prefix) { + private LocalCache createVolumeCache(String prefix) { return Cache.volume(createRandomVolumeName(prefix)); } - private Cache createVolumeCache(BuildRequest request, String suffix) { + private LocalCache createVolumeCache(BuildRequest request, String suffix) { return Cache.volume( VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", "." + suffix, 6)); } @@ -348,13 +345,11 @@ class Lifecycle implements Closeable { } private void configureBuildCache(Phase phase) { - Cache.Image image = this.buildCache.getImage(); - if (image != null) { + if (this.buildCache instanceof ImageCache image) { phase.withBuildCache(image.getName()); } - else { - phase.withBuildCache(Directory.CACHE, - Binding.from(getCacheBindingSource(this.buildCache), Directory.CACHE)); + else if (this.buildCache instanceof LocalCache localCache) { + phase.withBuildCache(Directory.CACHE, Binding.from(getCacheBindingSource(localCache), Directory.CACHE)); } } @@ -436,7 +431,7 @@ class Lifecycle implements Closeable { this.docker.volume().delete(name, true); } - private void deleteBind(Cache.Bind bind) { + private void deleteBind(LocalCache.Bind bind) { try { FileSystemUtils.deleteRecursively(Path.of(bind.getSource())); } diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/LocalCache.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/LocalCache.java new file mode 100644 index 00000000000..4a46d4c9c95 --- /dev/null +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/LocalCache.java @@ -0,0 +1,117 @@ +/* + * Copyright 2012-present 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.springframework.boot.buildpack.platform.build; + +import java.util.Objects; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.buildpack.platform.docker.type.VolumeName; +import org.springframework.util.ObjectUtils; + +/** + * Details of a local cache stored as a volume or bind mount. + * + * @author Scott Frederick + * @author Stephane Nicoll + * @since 4.2.0 + */ +public sealed interface LocalCache extends Cache permits LocalCache.Volume, LocalCache.Bind { + + /** + * Details of a cache stored in a Docker volume. + */ + final class Volume implements LocalCache { + + private final VolumeName name; + + Volume(VolumeName name) { + this.name = name; + } + + public String getName() { + return this.name.toString(); + } + + public VolumeName getVolumeName() { + return this.name; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Volume other = (Volume) obj; + return Objects.equals(this.name, other.name); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.name); + } + + @Override + public String toString() { + return "volume '" + this.name + "'"; + } + + } + + /** + * Details of a cache stored in a bind mount. + */ + final class Bind implements LocalCache { + + private final String source; + + Bind(String source) { + this.source = source; + } + + public String getSource() { + return this.source; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Bind other = (Bind) obj; + return Objects.equals(this.source, other.source); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.source); + } + + @Override + public String toString() { + return "bind mount '" + this.source + "'"; + } + + } + +} 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 dab8563813a..19ca097fc3d 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 @@ -325,6 +325,14 @@ class BuildRequestTests { assertThat(withCache.getBuildCache()).isEqualTo(Cache.bind("/tmp/build-cache")); } + @Test + void withBuildImageCacheAddsCache() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + BuildRequest withCache = request.withBuildCache(Cache.image("build-cache-image")); + assertThat(request.getBuildCache()).isNull(); + assertThat(withCache.getBuildCache()).isEqualTo(Cache.image("build-cache-image")); + } + @Test @SuppressWarnings("NullAway") // Test null check void withBuildVolumeCacheWhenCacheIsNullThrowsException() throws IOException { @@ -349,14 +357,6 @@ class BuildRequestTests { 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 {