Polish "Add support for image-based build caches"

See gh-50899
This commit is contained in:
Stéphane Nicoll
2026-07-27 18:22:16 +02:00
parent a36d078632
commit 5bd3e6435c
15 changed files with 519 additions and 359 deletions
@@ -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<? extends String>) 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<CacheSpec> action) {
public void buildWorkspace(Action<LocalCacheSpec> 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<CacheSpec> action) {
public void launchCache(Action<LocalCacheSpec> 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);
}
@@ -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<ImageCacheSpec> 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<String> 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<String> getSource();
}
/**
* Configuration for an image building cache stored in an image.
*/
@@ -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<VolumeCacheSpec> 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<BindCacheSpec> 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<String> 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<String> getSource();
}
}
@@ -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"));
}
}
@@ -19,4 +19,4 @@
</plugins>
</build>
</project>
<!-- end::caches[] -->
<!-- end::caches[] -->
@@ -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.
*/
@@ -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<String> 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);
}
@@ -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;
}
}
}
@@ -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();
@@ -95,11 +95,11 @@ public class BuildRequest {
private final List<ImageReference> 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<String, String> env,
boolean cleanCache, boolean verboseLogging, PullPolicy pullPolicy, boolean publish,
List<BuildpackReference> buildpacks, List<Binding> bindings, @Nullable String network,
List<ImageReference> tags, @Nullable Cache buildWorkspace, @Nullable Cache buildCache,
@Nullable Cache launchCache, @Nullable Instant createdDate, @Nullable String applicationDirectory,
List<ImageReference> tags, @Nullable LocalCache buildWorkspace, @Nullable Cache buildCache,
@Nullable LocalCache launchCache, @Nullable Instant createdDate, @Nullable String applicationDirectory,
@Nullable List<String> 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;
}
@@ -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);
}
}
@@ -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 + "'";
}
}
@@ -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()));
}
@@ -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 + "'";
}
}
}
@@ -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 {