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:
parent
d46a58f0f6
commit
c17ecf0f0b
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 + "'";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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" }
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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[] -->
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
|
@ -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.
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue