Add support for caching to bind mounts when building images

When building an image using the Maven `spring-boot:build-image` goal or
the Gradle `bootBuildImage` task, the build and launch caches can be
configured to use a bind mount as an alternative to using a named
volume.

Closes gh-28387
This commit is contained in:
Scott Frederick 2023-08-21 15:03:39 -05:00
parent d46a58f0f6
commit c17ecf0f0b
25 changed files with 575 additions and 47 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -68,6 +68,12 @@ public abstract class AbstractBuildLog implements BuildLog {
log(" > Using build cache volume '" + buildCacheVolume + "'");
}
@Override
public void executingLifecycle(BuildRequest request, LifecycleVersion version, Cache buildCache) {
log(" > Executing lifecycle version " + version);
log(" > Using build cache " + buildCache);
}
@Override
public Consumer<LogUpdateEvent> runningPhase(BuildRequest request, String name) {
log();

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -79,6 +79,14 @@ public interface BuildLog {
*/
void executingLifecycle(BuildRequest request, LifecycleVersion version, VolumeName buildCacheVolume);
/**
* Log that the lifecycle is executing.
* @param request the build request
* @param version the lifecycle version
* @param buildCache the build cache in use
*/
void executingLifecycle(BuildRequest request, LifecycleVersion version, Cache buildCache);
/**
* Log that a specific phase is running.
* @param request the build request

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,6 +18,7 @@ package org.springframework.boot.buildpack.platform.build;
import java.util.Objects;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@ -37,7 +38,22 @@ public class Cache {
/**
* A cache stored as a volume in the Docker daemon.
*/
VOLUME;
VOLUME("volume"),
/**
* A cache stored as a bind mount.
*/
BIND("bind mount");
private final String description;
Format(String description) {
this.description = description;
}
public String getDescription() {
return this.description;
}
}
@ -55,16 +71,44 @@ public class Cache {
return (this.format.equals(Format.VOLUME)) ? (Volume) this : null;
}
/**
* Return the details of the cache if it is a bind cache.
* @return the cache, or {@code null} if it is not a bind cache
*/
public Bind getBind() {
return (this.format.equals(Format.BIND)) ? (Bind) this : null;
}
/**
* Create a new {@code Cache} that uses a volume with the provided name.
* @param name the cache volume name
* @return a new cache instance
*/
public static Cache volume(String name) {
Assert.notNull(name, "Name must not be null");
return new Volume(VolumeName.of(name));
}
/**
* Create a new {@code Cache} that uses a volume with the provided name.
* @param name the cache volume name
* @return a new cache instance
*/
public static Cache volume(VolumeName name) {
Assert.notNull(name, "Name must not be null");
return new Volume(name);
}
/**
* Create a new {@code Cache} that uses a bind mount with the provided source.
* @param source the cache bind mount source
* @return a new cache instance
*/
public static Cache bind(String source) {
Assert.notNull(source, "Source must not be null");
return new Bind(source);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
@ -87,14 +131,18 @@ public class Cache {
*/
public static class Volume extends Cache {
private final String name;
private final VolumeName name;
Volume(String name) {
Volume(VolumeName name) {
super(Format.VOLUME);
this.name = name;
}
public String getName() {
return this.name.toString();
}
public VolumeName getVolumeName() {
return this.name;
}
@ -120,6 +168,56 @@ public class Cache {
return result;
}
@Override
public String toString() {
return this.format.getDescription() + " '" + this.name + "'";
}
}
/**
* Details of a cache stored in a bind mount.
*/
public static class Bind extends Cache {
private final String source;
Bind(String source) {
super(Format.BIND);
this.source = source;
}
public String getSource() {
return this.source;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
if (!super.equals(obj)) {
return false;
}
Bind other = (Bind) obj;
return Objects.equals(this.source, other.source);
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + ObjectUtils.nullSafeHashCode(this.source);
return result;
}
@Override
public String toString() {
return this.format.getDescription() + " '" + this.source + "'";
}
}
}

View File

@ -18,6 +18,7 @@ package org.springframework.boot.buildpack.platform.build;
import java.io.Closeable;
import java.io.IOException;
import java.nio.file.Path;
import java.util.function.Consumer;
import com.sun.jna.Platform;
@ -34,6 +35,7 @@ import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
import org.springframework.boot.buildpack.platform.io.TarArchive;
import org.springframework.util.Assert;
import org.springframework.util.FileSystemUtils;
/**
* A buildpack lifecycle used to run the build {@link Phase phases} needed to package an
@ -72,9 +74,9 @@ class Lifecycle implements Closeable {
private final VolumeName applicationVolume;
private final VolumeName buildCacheVolume;
private final Cache buildCache;
private final VolumeName launchCacheVolume;
private final Cache launchCache;
private final String applicationDirectory;
@ -101,8 +103,8 @@ class Lifecycle implements Closeable {
this.platformVersion = getPlatformVersion(builder.getBuilderMetadata().getLifecycle());
this.layersVolume = createRandomVolumeName("pack-layers-");
this.applicationVolume = createRandomVolumeName("pack-app-");
this.buildCacheVolume = getBuildCacheVolumeName(request);
this.launchCacheVolume = getLaunchCacheVolumeName(request);
this.buildCache = getBuildCache(request);
this.launchCache = getLaunchCache(request);
this.applicationDirectory = getApplicationDirectory(request);
}
@ -110,33 +112,27 @@ class Lifecycle implements Closeable {
return VolumeName.random(prefix);
}
private VolumeName getBuildCacheVolumeName(BuildRequest request) {
private Cache getBuildCache(BuildRequest request) {
if (request.getBuildCache() != null) {
return getVolumeName(request.getBuildCache());
return request.getBuildCache();
}
return createCacheVolumeName(request, "build");
return createVolumeCache(request, "build");
}
private VolumeName getLaunchCacheVolumeName(BuildRequest request) {
private Cache getLaunchCache(BuildRequest request) {
if (request.getLaunchCache() != null) {
return getVolumeName(request.getLaunchCache());
return request.getLaunchCache();
}
return createCacheVolumeName(request, "launch");
}
private VolumeName getVolumeName(Cache cache) {
if (cache.getVolume() != null) {
return VolumeName.of(cache.getVolume().getName());
}
return null;
return createVolumeCache(request, "launch");
}
private String getApplicationDirectory(BuildRequest request) {
return (request.getApplicationDirectory() != null) ? request.getApplicationDirectory() : Directory.APPLICATION;
}
private VolumeName createCacheVolumeName(BuildRequest request, String suffix) {
return VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", "." + suffix, 6);
private Cache createVolumeCache(BuildRequest request, String suffix) {
return Cache.volume(
VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", "." + suffix, 6));
}
private ApiVersion getPlatformVersion(BuilderMetadata.Lifecycle lifecycle) {
@ -155,9 +151,14 @@ class Lifecycle implements Closeable {
void execute() throws IOException {
Assert.state(!this.executed, "Lifecycle has already been executed");
this.executed = true;
this.log.executingLifecycle(this.request, this.lifecycleVersion, this.buildCacheVolume);
this.log.executingLifecycle(this.request, this.lifecycleVersion, this.buildCache);
if (this.request.isCleanCache()) {
deleteVolume(this.buildCacheVolume);
if (this.buildCache.getVolume() != null) {
deleteVolume(this.buildCache.getVolume().getVolumeName());
}
if (this.buildCache.getBind() != null) {
deleteBind(this.buildCache.getBind().getSource());
}
}
run(createPhase());
this.log.executedLifecycle(this.request);
@ -184,8 +185,8 @@ class Lifecycle implements Closeable {
phase.withArgs(this.request.getName());
phase.withBinding(Binding.from(this.layersVolume, Directory.LAYERS));
phase.withBinding(Binding.from(this.applicationVolume, this.applicationDirectory));
phase.withBinding(Binding.from(this.buildCacheVolume, Directory.CACHE));
phase.withBinding(Binding.from(this.launchCacheVolume, Directory.LAUNCH_CACHE));
phase.withBinding(Binding.from(getCacheBindingSource(this.buildCache), Directory.CACHE));
phase.withBinding(Binding.from(getCacheBindingSource(this.launchCache), Directory.LAUNCH_CACHE));
if (this.request.getBindings() != null) {
this.request.getBindings().forEach(phase::withBinding);
}
@ -199,6 +200,10 @@ class Lifecycle implements Closeable {
return phase;
}
private String getCacheBindingSource(Cache cache) {
return (cache.getVolume() != null) ? cache.getVolume().getName() : cache.getBind().getSource();
}
private void configureDaemonAccess(Phase phase) {
if (this.dockerHost != null) {
if (this.dockerHost.isRemote()) {
@ -269,6 +274,15 @@ class Lifecycle implements Closeable {
this.docker.volume().delete(name, true);
}
private void deleteBind(String source) {
try {
FileSystemUtils.deleteRecursively(Path.of(source));
}
catch (IOException ex) {
throw new IllegalStateException("Error cleaning bind mount directory '" + source + "'", ex);
}
}
/**
* Common directories used by the various phases.
*/

View File

@ -239,6 +239,14 @@ class BuildRequestTests {
assertThat(withCache.getBuildCache()).isEqualTo(Cache.volume("build-volume"));
}
@Test
void withBuildBindCacheAddsCache() throws IOException {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
BuildRequest withCache = request.withBuildCache(Cache.bind("/tmp/build-cache"));
assertThat(request.getBuildCache()).isNull();
assertThat(withCache.getBuildCache()).isEqualTo(Cache.bind("/tmp/build-cache"));
}
@Test
void withBuildVolumeCacheWhenCacheIsNullThrowsException() throws IOException {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
@ -254,6 +262,14 @@ class BuildRequestTests {
assertThat(withCache.getLaunchCache()).isEqualTo(Cache.volume("launch-volume"));
}
@Test
void withLaunchBindCacheAddsCache() throws IOException {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
BuildRequest withCache = request.withLaunchCache(Cache.bind("/tmp/launch-cache"));
assertThat(request.getLaunchCache()).isNull();
assertThat(withCache.getLaunchCache()).isEqualTo(Cache.bind("/tmp/launch-cache"));
}
@Test
void withLaunchVolumeCacheWhenCacheIsNullThrowsException() throws IOException {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));

View File

@ -218,6 +218,18 @@ class LifecycleTests {
assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
}
@Test
void executeWithCacheBindMountsExecutesPhases() throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest().withBuildCache(Cache.bind("/tmp/build-cache"))
.withLaunchCache(Cache.bind("/tmp/launch-cache"));
createLifecycle(request).execute();
assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-cache-bind-mounts.json"));
assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
}
@Test
void executeWithCreatedDateExecutesPhases() throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -67,7 +67,7 @@ class PrintStreamBuildLogTests {
Consumer<TotalProgressEvent> pullRunImageConsumer = log.pullingImage(runImageReference, ImageType.RUNNER);
pullRunImageConsumer.accept(new TotalProgressEvent(100));
log.pulledImage(runImage, ImageType.RUNNER);
log.executingLifecycle(request, LifecycleVersion.parse("0.5"), VolumeName.of("pack-abc.cache"));
log.executingLifecycle(request, LifecycleVersion.parse("0.5"), Cache.volume(VolumeName.of("pack-abc.cache")));
Consumer<LogUpdateEvent> phase1Consumer = log.runningPhase(request, "alphabet");
phase1Consumer.accept(mockLogEvent("one"));
phase1Consumer.accept(mockLogEvent("two"));

View File

@ -0,0 +1,39 @@
{
"User": "root",
"Image": "pack.local/ephemeral-builder",
"Cmd": [
"/cnb/lifecycle/creator",
"-app",
"/workspace",
"-platform",
"/platform",
"-run-image",
"docker.io/cloudfoundry/run:latest",
"-layers",
"/layers",
"-cache-dir",
"/cache",
"-launch-cache",
"/launch-cache",
"-daemon",
"docker.io/library/my-application:latest"
],
"Env": [
"CNB_PLATFORM_API=0.8"
],
"Labels": {
"author": "spring-boot"
},
"HostConfig": {
"Binds": [
"/var/run/docker.sock:/var/run/docker.sock",
"pack-layers-aaaaaaaaaa:/layers",
"pack-app-aaaaaaaaaa:/workspace",
"/tmp/build-cache:/cache",
"/tmp/launch-cache:/launch-cache"
],
"SecurityOpt" : [
"label=disable"
]
}
}

View File

@ -440,6 +440,20 @@ include::../gradle/packaging/boot-build-image-caches.gradle[tags=caches]
include::../gradle/packaging/boot-build-image-caches.gradle.kts[tags=caches]
----
The caches can be configured to use bind mounts instead of named volumes, as shown in the following example:
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
.Groovy
----
include::../gradle/packaging/boot-build-image-bind-caches.gradle[tags=caches]
----
[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"]
.Kotlin
----
include::../gradle/packaging/boot-build-image-bind-caches.gradle.kts[tags=caches]
----
[[build-image.examples.docker]]
=== Docker Configuration

View File

@ -0,0 +1,30 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{gradle-project-version}'
}
tasks.named("bootJar") {
mainClass = 'com.example.ExampleApplication'
}
// tag::caches[]
tasks.named("bootBuildImage") {
buildCache {
bind {
source = "/tmp/cache-${rootProject.name}.build"
}
}
launchCache {
bind {
source = "/tmp/cache-${rootProject.name}.launch"
}
}
}
// end::caches[]
tasks.register("bootBuildImageCaches") {
doFirst {
bootBuildImage.buildCache.asCache().with { println "buildCache=$source" }
bootBuildImage.launchCache.asCache().with { println "launchCache=$source" }
}
}

View File

@ -0,0 +1,28 @@
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
plugins {
java
id("org.springframework.boot") version "{gradle-project-version}"
}
// tag::caches[]
tasks.named<BootBuildImage>("bootBuildImage") {
buildCache {
bind {
source.set("/tmp/cache-${rootProject.name}.build")
}
}
launchCache {
bind {
source.set("/tmp/cache-${rootProject.name}.launch")
}
}
}
// end::caches[]
tasks.register("bootBuildImageCaches") {
doFirst {
println("buildCache=" + tasks.getByName<BootBuildImage>("bootBuildImage").buildCache.asCache().bind.source)
println("launchCache=" + tasks.getByName<BootBuildImage>("bootBuildImage").launchCache.asCache().bind.source)
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2021-2022 the original author or authors.
* Copyright 2021-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -60,6 +60,19 @@ public class CacheSpec {
this.cache = Cache.volume(spec.getName().get());
}
/**
* Configures a bind cache using the given {@code action}.
* @param action the action
*/
public void bind(Action<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.
*/
@ -74,4 +87,18 @@ public class CacheSpec {
}
/**
* Configuration for an image building cache stored in a bind mount.
*/
public abstract static class BindCacheSpec {
/**
* Returns the source of the cache.
* @return the cache source
*/
@Input
public abstract Property<String> getSource();
}
}

View File

@ -339,6 +339,14 @@ class PackagingDocumentationTests {
.containsPattern("launchCache=cache-gradle-[\\d]+.launch");
}
@TestTemplate
void bootBuildImageWithBindCaches() {
BuildResult result = this.gradleBuild.script("src/docs/gradle/packaging/boot-build-image-bind-caches")
.build("bootBuildImageCaches");
assertThat(result.getOutput()).containsPattern("buildCache=/tmp/cache-gradle-[\\d]+.build")
.containsPattern("launchCache=/tmp/cache-gradle-[\\d]+.launch");
}
protected void jarFile(File file) throws IOException {
try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(file))) {
jar.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF"));

View File

@ -50,6 +50,7 @@ import org.springframework.boot.gradle.junit.GradleCompatibility;
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
import org.springframework.boot.testsupport.junit.DisabledOnOs;
import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable;
import org.springframework.util.FileSystemUtils;
import static org.assertj.core.api.Assertions.assertThat;
@ -297,6 +298,26 @@ class BootBuildImageIntegrationTests {
deleteVolumes("cache-" + projectName + ".build", "cache-" + projectName + ".launch");
}
@TestTemplate
void buildsImageWithBindCaches() throws IOException {
writeMainClass();
writeLongNameResource();
BuildResult result = this.gradleBuild.build("bootBuildImage");
String projectName = this.gradleBuild.getProjectDir().getName();
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
assertThat(result.getOutput()).contains("---> Test Info buildpack building");
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
removeImages(projectName);
String tempDir = System.getProperty("java.io.tmpdir");
Path buildCachePath = Paths.get(tempDir, "junit-image-cache-" + projectName + "-build");
Path launchCachePath = Paths.get(tempDir, "junit-image-cache-" + projectName + "-launch");
assertThat(buildCachePath).exists().isDirectory();
assertThat(launchCachePath).exists().isDirectory();
FileSystemUtils.deleteRecursively(buildCachePath);
FileSystemUtils.deleteRecursively(launchCachePath);
}
@TestTemplate
void buildsImageWithCreatedDate() throws IOException {
writeMainClass();

View File

@ -0,0 +1,24 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
java {
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
}
bootBuildImage {
builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2"
pullPolicy = "IF_NOT_PRESENT"
buildCache {
bind {
source = System.getProperty('java.io.tmpdir') + "/junit-image-cache-${rootProject.name}-build"
}
}
launchCache {
bind {
source = System.getProperty('java.io.tmpdir') + "/junit-image-cache-${rootProject.name}-launch"
}
}
}

View File

@ -12,10 +12,10 @@ bootBuildImage {
builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2"
buildCache {
volume {
name = "build-cache-volume1"
name = "build-cache-volume"
}
volume {
name = "build-cache-volum2"
bind {
name = "/tmp/build-cache-bind"
}
}
}

View File

@ -420,6 +420,13 @@ The cache volumes can be configured to use alternative names to give more contro
include::../maven/packaging-oci-image/caches-pom.xml[tags=caches]
----
The caches can be configured to use bind mounts instead of named volumes, as shown in the following example:
[source,xml,indent=0,subs="verbatim,attributes",tabsize=4]
----
include::../maven/packaging-oci-image/bind-caches-pom.xml[tags=caches]
----
[[build-image.examples.docker]]
=== Docker Configuration

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- tag::caches[] -->
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<buildCache>
<bind>
<source>/tmp/cache-${project.artifactId}.build</source>
</bind>
</buildCache>
<launchCache>
<bind>
<source>/tmp/cache-${project.artifactId}.launch</source>
</bind>
</launchCache>
</image>
</configuration>
</plugin>
</plugins>
</build>
</project>
<!-- end::caches[] -->

View File

@ -37,6 +37,7 @@ import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
import org.springframework.boot.testsupport.junit.DisabledOnOs;
import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable;
import org.springframework.util.FileSystemUtils;
import static org.assertj.core.api.Assertions.assertThat;
@ -385,19 +386,41 @@ class BuildImageTests extends AbstractArchiveIntegrationTests {
@TestTemplate
void whenBuildImageIsInvokedWithVolumeCaches(MavenBuild mavenBuild) {
String testBuildId = randomString();
mavenBuild.project("build-image-caches")
mavenBuild.project("build-image-volume-caches")
.goals("package")
.systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT")
.systemProperty("test-build-id", testBuildId)
.execute((project) -> {
assertThat(buildLog(project)).contains("Building image")
.contains("docker.io/library/build-image-caches:0.0.1.BUILD-SNAPSHOT")
.contains("docker.io/library/build-image-volume-caches:0.0.1.BUILD-SNAPSHOT")
.contains("Successfully built image");
removeImage("build-image-caches", "0.0.1.BUILD-SNAPSHOT");
removeImage("build-image-volume-caches", "0.0.1.BUILD-SNAPSHOT");
deleteVolumes("cache-" + testBuildId + ".build", "cache-" + testBuildId + ".launch");
});
}
@TestTemplate
void whenBuildImageIsInvokedWithBindCaches(MavenBuild mavenBuild) {
String testBuildId = randomString();
mavenBuild.project("build-image-bind-caches")
.goals("package")
.systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT")
.systemProperty("test-build-id", testBuildId)
.execute((project) -> {
assertThat(buildLog(project)).contains("Building image")
.contains("docker.io/library/build-image-bind-caches:0.0.1.BUILD-SNAPSHOT")
.contains("Successfully built image");
removeImage("build-image-bind-caches", "0.0.1.BUILD-SNAPSHOT");
String tempDir = System.getProperty("java.io.tmpdir");
Path buildCachePath = Paths.get(tempDir, "junit-image-cache-" + testBuildId + "-build");
Path launchCachePath = Paths.get(tempDir, "junit-image-cache-" + testBuildId + "-launch");
assertThat(buildCachePath).exists().isDirectory();
assertThat(launchCachePath).exists().isDirectory();
FileSystemUtils.deleteRecursively(buildCachePath);
FileSystemUtils.deleteRecursively(launchCachePath);
});
}
@TestTemplate
void whenBuildImageIsInvokedWithCreatedDate(MavenBuild mavenBuild) {
String testBuildId = randomString();

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>build-image-bind-caches</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>build-image-no-fork</goal>
</goals>
<configuration>
<image>
<builder>projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2</builder>
<buildCache>
<bind>
<source>${java.io.tmpdir}/junit-image-cache-${test-build-id}-build</source>
</bind>
</buildCache>
<launchCache>
<bind>
<source>${java.io.tmpdir}/junit-image-cache-${test-build-id}-launch</source>
</bind>
</launchCache>
</image>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@ -3,7 +3,7 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>build-image-caches</artifactId>
<artifactId>build-image-volume-caches</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

View File

@ -0,0 +1,28 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.test;
public class SampleApplication {
public static void main(String[] args) throws Exception {
System.out.println("Launched");
synchronized(args) {
args.wait(); // Prevent exit
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -32,8 +32,8 @@ public class CacheInfo {
public CacheInfo() {
}
CacheInfo(VolumeCacheInfo volumeCacheInfo) {
this.cache = Cache.volume(volumeCacheInfo.getName());
private CacheInfo(Cache cache) {
this.cache = cache;
}
public void setVolume(VolumeCacheInfo info) {
@ -41,10 +41,23 @@ public class CacheInfo {
this.cache = Cache.volume(info.getName());
}
public void setBind(BindCacheInfo info) {
Assert.state(this.cache == null, "Each image building cache can be configured only once");
this.cache = Cache.bind(info.getSource());
}
Cache asCache() {
return this.cache;
}
static CacheInfo fromVolume(VolumeCacheInfo cacheInfo) {
return new CacheInfo(Cache.volume(cacheInfo.getName()));
}
static CacheInfo fromBind(BindCacheInfo cacheInfo) {
return new CacheInfo(Cache.bind(cacheInfo.getSource()));
}
/**
* Encapsulates configuration of an image building cache stored in a volume.
*/
@ -69,4 +82,28 @@ public class CacheInfo {
}
/**
* Encapsulates configuration of an image building cache stored in a bind mount.
*/
public static class BindCacheInfo {
private String source;
public BindCacheInfo() {
}
BindCacheInfo(String name) {
this.source = name;
}
public String getSource() {
return this.source;
}
void setSource(String source) {
this.source = source;
}
}
}

View File

@ -34,6 +34,7 @@ import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.io.Owner;
import org.springframework.boot.buildpack.platform.io.TarArchive;
import org.springframework.boot.maven.CacheInfo.BindCacheInfo;
import org.springframework.boot.maven.CacheInfo.VolumeCacheInfo;
import static org.assertj.core.api.Assertions.assertThat;
@ -170,21 +171,37 @@ class ImageTests {
}
@Test
void getBuildRequestWhenHasBuildVolumeCacheUsesCache() {
void getBuildRequestWhenHasBuildCacheVolumeUsesCache() {
Image image = new Image();
image.buildCache = new CacheInfo(new VolumeCacheInfo("build-cache-vol"));
image.buildCache = CacheInfo.fromVolume(new VolumeCacheInfo("build-cache-vol"));
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
assertThat(request.getBuildCache()).isEqualTo(Cache.volume("build-cache-vol"));
}
@Test
void getBuildRequestWhenHasLaunchVolumeCacheUsesCache() {
void getBuildRequestWhenHasLaunchCacheVolumeUsesCache() {
Image image = new Image();
image.launchCache = new CacheInfo(new VolumeCacheInfo("launch-cache-vol"));
image.launchCache = CacheInfo.fromVolume(new VolumeCacheInfo("launch-cache-vol"));
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
assertThat(request.getLaunchCache()).isEqualTo(Cache.volume("launch-cache-vol"));
}
@Test
void getBuildRequestWhenHasBuildCacheBindUsesCache() {
Image image = new Image();
image.buildCache = CacheInfo.fromBind(new BindCacheInfo("build-cache-dir"));
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
assertThat(request.getBuildCache()).isEqualTo(Cache.bind("build-cache-dir"));
}
@Test
void getBuildRequestWhenHasLaunchCacheBindUsesCache() {
Image image = new Image();
image.launchCache = CacheInfo.fromBind(new BindCacheInfo("launch-cache-dir"));
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
assertThat(request.getLaunchCache()).isEqualTo(Cache.bind("launch-cache-dir"));
}
@Test
void getBuildRequestWhenHasCreatedDateUsesCreatedDate() {
Image image = new Image();