Polish "Add option to customize cache volume names when building an image"
See gh-28292
This commit is contained in:
parent
dc36346285
commit
871468931f
|
@ -75,7 +75,9 @@ public class BuildRequest {
|
|||
|
||||
private final List<ImageReference> tags;
|
||||
|
||||
private final Map<String, String> cacheVolumeNames;
|
||||
private final Cache buildCache;
|
||||
|
||||
private final Cache launchCache;
|
||||
|
||||
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent) {
|
||||
Assert.notNull(name, "Name must not be null");
|
||||
|
@ -94,13 +96,14 @@ public class BuildRequest {
|
|||
this.bindings = Collections.emptyList();
|
||||
this.network = null;
|
||||
this.tags = Collections.emptyList();
|
||||
this.cacheVolumeNames = Collections.emptyMap();
|
||||
this.buildCache = null;
|
||||
this.launchCache = null;
|
||||
}
|
||||
|
||||
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent, ImageReference builder,
|
||||
ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache,
|
||||
boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List<BuildpackReference> buildpacks,
|
||||
List<Binding> bindings, String network, List<ImageReference> tags, Map<String, String> cacheVolumeNames) {
|
||||
List<Binding> bindings, String network, List<ImageReference> tags, Cache buildCache, Cache launchCache) {
|
||||
this.name = name;
|
||||
this.applicationContent = applicationContent;
|
||||
this.builder = builder;
|
||||
|
@ -115,7 +118,8 @@ public class BuildRequest {
|
|||
this.bindings = bindings;
|
||||
this.network = network;
|
||||
this.tags = tags;
|
||||
this.cacheVolumeNames = cacheVolumeNames;
|
||||
this.buildCache = buildCache;
|
||||
this.launchCache = launchCache;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -127,7 +131,7 @@ public class BuildRequest {
|
|||
Assert.notNull(builder, "Builder must not be null");
|
||||
return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage,
|
||||
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
|
||||
this.buildpacks, this.bindings, this.network, this.tags, this.cacheVolumeNames);
|
||||
this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.launchCache);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -138,7 +142,7 @@ public class BuildRequest {
|
|||
public BuildRequest withRunImage(ImageReference runImageName) {
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(),
|
||||
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
|
||||
this.buildpacks, this.bindings, this.network, this.tags, this.cacheVolumeNames);
|
||||
this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.launchCache);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -150,7 +154,7 @@ public class BuildRequest {
|
|||
Assert.notNull(creator, "Creator must not be null");
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env,
|
||||
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
|
||||
this.network, this.tags, this.cacheVolumeNames);
|
||||
this.network, this.tags, this.buildCache, this.launchCache);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -166,7 +170,7 @@ public class BuildRequest {
|
|||
env.put(name, value);
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
|
||||
Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
|
||||
this.buildpacks, this.bindings, this.network, this.tags, this.cacheVolumeNames);
|
||||
this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.launchCache);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -180,7 +184,8 @@ public class BuildRequest {
|
|||
updatedEnv.putAll(env);
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
|
||||
Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy,
|
||||
this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.cacheVolumeNames);
|
||||
this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildCache,
|
||||
this.launchCache);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -191,7 +196,7 @@ public class BuildRequest {
|
|||
public BuildRequest withCleanCache(boolean cleanCache) {
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
||||
cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
|
||||
this.network, this.tags, this.cacheVolumeNames);
|
||||
this.network, this.tags, this.buildCache, this.launchCache);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -202,7 +207,7 @@ public class BuildRequest {
|
|||
public BuildRequest withVerboseLogging(boolean verboseLogging) {
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
||||
this.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
|
||||
this.network, this.tags, this.cacheVolumeNames);
|
||||
this.network, this.tags, this.buildCache, this.launchCache);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -213,7 +218,7 @@ public class BuildRequest {
|
|||
public BuildRequest withPullPolicy(PullPolicy pullPolicy) {
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
||||
this.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks, this.bindings,
|
||||
this.network, this.tags, this.cacheVolumeNames);
|
||||
this.network, this.tags, this.buildCache, this.launchCache);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -224,7 +229,7 @@ public class BuildRequest {
|
|||
public BuildRequest withPublish(boolean publish) {
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
||||
this.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks, this.bindings,
|
||||
this.network, this.tags, this.cacheVolumeNames);
|
||||
this.network, this.tags, this.buildCache, this.launchCache);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -248,7 +253,7 @@ public class BuildRequest {
|
|||
Assert.notNull(buildpacks, "Buildpacks must not be null");
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
||||
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks, this.bindings,
|
||||
this.network, this.tags, this.cacheVolumeNames);
|
||||
this.network, this.tags, this.buildCache, this.launchCache);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -272,7 +277,7 @@ public class BuildRequest {
|
|||
Assert.notNull(bindings, "Bindings must not be null");
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
||||
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, bindings,
|
||||
this.network, this.tags, this.cacheVolumeNames);
|
||||
this.network, this.tags, this.buildCache, this.launchCache);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -284,7 +289,7 @@ public class BuildRequest {
|
|||
public BuildRequest withNetwork(String network) {
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
||||
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
|
||||
network, this.tags, this.cacheVolumeNames);
|
||||
network, this.tags, this.buildCache, this.launchCache);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -306,39 +311,31 @@ public class BuildRequest {
|
|||
Assert.notNull(tags, "Tags must not be null");
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
||||
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
|
||||
this.network, tags, this.cacheVolumeNames);
|
||||
this.network, tags, this.buildCache, this.launchCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link BuildRequest} with an additional cache volume name.
|
||||
* @param type the cache volume type
|
||||
* @param name the cache volume name
|
||||
* Return a new {@link BuildRequest} with an updated build cache.
|
||||
* @param buildCache the build cache
|
||||
* @return an updated build request
|
||||
*/
|
||||
public BuildRequest withCacheVolumeName(String type, String name) {
|
||||
Assert.hasText(type, "Type must not be empty");
|
||||
Assert.state((type.equals("build") || type.equals("launch")), "Type must be either 'build' or 'launch'");
|
||||
Assert.hasText(name, "Name must not be empty");
|
||||
Map<String, String> cacheVolumeNames = new LinkedHashMap<>(this.cacheVolumeNames);
|
||||
cacheVolumeNames.put(type, name);
|
||||
public BuildRequest withBuildCache(Cache buildCache) {
|
||||
Assert.notNull(buildCache, "BuildCache must not be null");
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
||||
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
|
||||
this.network, this.tags, Collections.unmodifiableMap(cacheVolumeNames));
|
||||
this.network, this.tags, buildCache, this.launchCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link BuildRequest} with additional cache volume names.
|
||||
* @param entries the additional cache volume names
|
||||
* Return a new {@link BuildRequest} with an updated launch cache.
|
||||
* @param launchCache the cache
|
||||
* @return an updated build request
|
||||
*/
|
||||
public BuildRequest withCacheVolumeNames(Map<String, String> entries) {
|
||||
Assert.notNull(entries, "Entries must not be null");
|
||||
Assert.state(!entries.isEmpty(), "Entries must not be empty");
|
||||
BuildRequest request = null;
|
||||
for (Map.Entry<String, String> entry : entries.entrySet()) {
|
||||
request = withCacheVolumeName(entry.getKey(), entry.getValue());
|
||||
}
|
||||
return request;
|
||||
public BuildRequest withLaunchCache(Cache launchCache) {
|
||||
Assert.notNull(launchCache, "LaunchCache must not be null");
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
||||
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
|
||||
this.network, this.tags, this.buildCache, launchCache);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -459,11 +456,19 @@ public class BuildRequest {
|
|||
}
|
||||
|
||||
/**
|
||||
* Return the custom cache volume names that should be used by the lifecycle.
|
||||
* @return the cache volume names
|
||||
* Return the custom build cache that should be used by the lifecycle.
|
||||
* @return the build cache
|
||||
*/
|
||||
public Map<String, String> getCacheVolumeNames() {
|
||||
return this.cacheVolumeNames;
|
||||
public Cache getBuildCache() {
|
||||
return this.buildCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the custom launch cache that should be used by the lifecycle.
|
||||
* @return the launch cache
|
||||
*/
|
||||
public Cache getLaunchCache() {
|
||||
return this.launchCache;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright 2012-2021 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.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Details of a cache for use by the CNB builder.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
* @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;
|
||||
|
||||
}
|
||||
|
||||
protected final Format format;
|
||||
|
||||
Cache(Format format) {
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Volume getVolume() {
|
||||
return (this.format.equals(Format.VOLUME)) ? (Volume) 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(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(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 String name;
|
||||
|
||||
Volume(String name) {
|
||||
super(Format.VOLUME);
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -87,18 +87,36 @@ class Lifecycle implements Closeable {
|
|||
this.platformVersion = getPlatformVersion(builder.getBuilderMetadata().getLifecycle());
|
||||
this.layersVolume = createRandomVolumeName("pack-layers-");
|
||||
this.applicationVolume = createRandomVolumeName("pack-app-");
|
||||
this.buildCacheVolume = createCacheVolumeName(request, "build");
|
||||
this.launchCacheVolume = createCacheVolumeName(request, "launch");
|
||||
this.buildCacheVolume = getBuildCacheVolumeName(request);
|
||||
this.launchCacheVolume = getLaunchCacheVolumeName(request);
|
||||
}
|
||||
|
||||
protected VolumeName createRandomVolumeName(String prefix) {
|
||||
return VolumeName.random(prefix);
|
||||
}
|
||||
|
||||
private VolumeName createCacheVolumeName(BuildRequest request, String suffix) {
|
||||
if (!request.getCacheVolumeNames().isEmpty() && request.getCacheVolumeNames().containsKey(suffix)) {
|
||||
return VolumeName.of(request.getCacheVolumeNames().get(suffix));
|
||||
private VolumeName getBuildCacheVolumeName(BuildRequest request) {
|
||||
if (request.getBuildCache() != null) {
|
||||
return getVolumeName(request.getBuildCache());
|
||||
}
|
||||
return createCacheVolumeName(request, "build");
|
||||
}
|
||||
|
||||
private VolumeName getLaunchCacheVolumeName(BuildRequest request) {
|
||||
if (request.getLaunchCache() != null) {
|
||||
return getVolumeName(request.getLaunchCache());
|
||||
}
|
||||
return createCacheVolumeName(request, "launch");
|
||||
}
|
||||
|
||||
private VolumeName getVolumeName(Cache cache) {
|
||||
if (cache.getVolume() != null) {
|
||||
return VolumeName.of(cache.getVolume().getName());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private VolumeName createCacheVolumeName(BuildRequest request, String suffix) {
|
||||
return VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", "." + suffix, 6);
|
||||
}
|
||||
|
||||
|
|
|
@ -210,11 +210,11 @@ class BuildRequestTests {
|
|||
@Test
|
||||
void withTagsAddsTags() throws IOException {
|
||||
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
|
||||
BuildRequest witTags = request.withTags(ImageReference.of("docker.io/library/my-app:latest"),
|
||||
BuildRequest withTags = request.withTags(ImageReference.of("docker.io/library/my-app:latest"),
|
||||
ImageReference.of("example.com/custom/my-app:0.0.1"),
|
||||
ImageReference.of("example.com/custom/my-app:latest"));
|
||||
assertThat(request.getTags()).isEmpty();
|
||||
assertThat(witTags.getTags()).containsExactly(ImageReference.of("docker.io/library/my-app:latest"),
|
||||
assertThat(withTags.getTags()).containsExactly(ImageReference.of("docker.io/library/my-app:latest"),
|
||||
ImageReference.of("example.com/custom/my-app:0.0.1"),
|
||||
ImageReference.of("example.com/custom/my-app:latest"));
|
||||
}
|
||||
|
@ -226,6 +226,36 @@ class BuildRequestTests {
|
|||
.withMessage("Tags must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withBuildVolumeCacheAddsCache() throws IOException {
|
||||
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
|
||||
BuildRequest withCache = request.withBuildCache(Cache.volume("build-volume"));
|
||||
assertThat(request.getBuildCache()).isNull();
|
||||
assertThat(withCache.getBuildCache()).isEqualTo(Cache.volume("build-volume"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void withBuildVolumeCacheWhenCacheIsNullThrowsException() throws IOException {
|
||||
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> request.withBuildCache(null))
|
||||
.withMessage("BuildCache must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withLaunchVolumeCacheAddsCache() throws IOException {
|
||||
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
|
||||
BuildRequest withCache = request.withLaunchCache(Cache.volume("launch-volume"));
|
||||
assertThat(request.getLaunchCache()).isNull();
|
||||
assertThat(withCache.getLaunchCache()).isEqualTo(Cache.volume("launch-volume"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void withLaunchVolumeCacheWhenCacheIsNullThrowsException() throws IOException {
|
||||
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> request.withLaunchCache(null))
|
||||
.withMessage("LaunchCache must not be null");
|
||||
}
|
||||
|
||||
private void hasExpectedJarContent(TarArchive archive) {
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
|
|
|
@ -200,6 +200,18 @@ class LifecycleTests {
|
|||
assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeWithCacheVolumeNamesExecutesPhases() 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.volume("build-volume"))
|
||||
.withLaunchCache(Cache.volume("launch-volume"));
|
||||
createLifecycle(request).execute();
|
||||
assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-cache-volumes.json"));
|
||||
assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
|
||||
}
|
||||
|
||||
private DockerApi mockDockerApi() {
|
||||
DockerApi docker = mock(DockerApi.class);
|
||||
ImageApi imageApi = mock(ImageApi.class);
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"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",
|
||||
"-process-type=web",
|
||||
"docker.io/library/my-application:latest"
|
||||
],
|
||||
"Env": [
|
||||
"CNB_PLATFORM_API=0.4"
|
||||
],
|
||||
"Labels": {
|
||||
"author": "spring-boot"
|
||||
},
|
||||
"HostConfig": {
|
||||
"Binds": [
|
||||
"/var/run/docker.sock:/var/run/docker.sock",
|
||||
"pack-layers-aaaaaaaaaa:/layers",
|
||||
"pack-app-aaaaaaaaaa:/workspace",
|
||||
"build-volume:/cache",
|
||||
"launch-volume:/launch-cache"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -181,10 +181,15 @@ The value supplied will be passed unvalidated to Docker when creating the builde
|
|||
| A list of one or more additional tags to apply to the generated image.
|
||||
|
|
||||
|
||||
| `cacheVolumeNames`
|
||||
| `buildCache`
|
||||
|
|
||||
| Cache volume names that should be used by the builder instead of generating random names.
|
||||
| A cache containing layers created by buildpacks and used by the image building process.
|
||||
| A named volume in the Docker daemon, with a name derived from the image name.
|
||||
|
||||
| `launchCache`
|
||||
|
|
||||
| A cache containing layers created by buildpacks and used by the image launching process.
|
||||
| A named volume in the Docker daemon, with a name derived from the image name.
|
||||
|
||||
|===
|
||||
|
||||
|
@ -384,10 +389,30 @@ The publish option can be specified on the command line as well, as shown in thi
|
|||
$ gradle bootBuildImage --imageName=docker.example.com/library/my-app:v1 --publishImage
|
||||
----
|
||||
|
||||
[[build-image.examples.caches]]
|
||||
=== Builder Cache Configuration
|
||||
|
||||
The CNB builder caches layers that are used when building and launching an image.
|
||||
By default, these caches are stored as named volumes in the Docker daemon with names that are derived from the full name of the target image.
|
||||
If the image name changes frequently, for example when the project version is used as a tag in the image name, then the caches can be invalidated frequently.
|
||||
|
||||
The cache volumes can be configured to use alternative names to give more control over cache lifecycle as shown in the following example:
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Groovy
|
||||
----
|
||||
include::../gradle/packaging/boot-build-image-caches.gradle[tags=caches]
|
||||
----
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
include::../gradle/packaging/boot-build-image-caches.gradle.kts[tags=caches]
|
||||
----
|
||||
|
||||
[[build-image.examples.docker]]
|
||||
=== Docker Configuration
|
||||
|
||||
If you need the plugin to communicate with the Docker daemon using a remote connection instead of the default local connection, the connection details can be provided using `docker` properties as shown in the following example:
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '{gradle-project-version}'
|
||||
}
|
||||
|
||||
bootJar {
|
||||
mainClass = 'com.example.ExampleApplication'
|
||||
}
|
||||
|
||||
// tag::cacheVolumeNames[]
|
||||
bootBuildImage {
|
||||
cacheVolumeNames = [
|
||||
"build": "example-build-cachevol",
|
||||
"launch": "example-launch-cachevol"
|
||||
]
|
||||
}
|
||||
// end::cacheVolumeNames[]
|
||||
|
||||
task bootBuildImageCacheVolumeNames {
|
||||
doFirst {
|
||||
bootBuildImage.cacheVolumeNames.each { type, name -> println "$type=$name" }
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
|
||||
|
||||
plugins {
|
||||
java
|
||||
id("org.springframework.boot") version "{gradle-project-version}"
|
||||
}
|
||||
|
||||
// tag::cacheVolumeNames[]
|
||||
tasks.getByName<BootBuildImage>("bootBuildImage") {
|
||||
cacheVolumeNames = mapOf("build" to "example-build-cachevol",
|
||||
"launch" to "example-launch-cachevol")
|
||||
}
|
||||
// end::cacheVolumeNames[]
|
||||
|
||||
tasks.register("bootBuildImageEnvironment") {
|
||||
doFirst {
|
||||
for((type, name) in tasks.getByName<BootBuildImage>("bootBuildImage").cacheVolumeNames) {
|
||||
print(type + "=" + name)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '{gradle-project-version}'
|
||||
}
|
||||
|
||||
bootJar {
|
||||
mainClass = 'com.example.ExampleApplication'
|
||||
}
|
||||
|
||||
// tag::caches[]
|
||||
bootBuildImage {
|
||||
buildCache {
|
||||
volume {
|
||||
name = "cache-${rootProject.name}.build"
|
||||
}
|
||||
}
|
||||
launchCache {
|
||||
volume {
|
||||
name = "cache-${rootProject.name}.launch"
|
||||
}
|
||||
}
|
||||
}
|
||||
// end::caches[]
|
||||
|
||||
task bootBuildImageCaches {
|
||||
doFirst {
|
||||
bootBuildImage.buildCache.asCache().with { println "buildCache=$name" }
|
||||
bootBuildImage.launchCache.asCache().with { println "launchCache=$name" }
|
||||
}
|
||||
}
|
|
@ -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.getByName<BootBuildImage>("bootBuildImage") {
|
||||
buildCache {
|
||||
volume {
|
||||
name = "cache-${rootProject.name}.build"
|
||||
}
|
||||
}
|
||||
launchCache {
|
||||
volume {
|
||||
name = "cache-${rootProject.name}.launch"
|
||||
}
|
||||
}
|
||||
}
|
||||
// end::caches[]
|
||||
|
||||
tasks.register("bootBuildImageCaches") {
|
||||
doFirst {
|
||||
println("buildCache=" + tasks.getByName<BootBuildImage>("bootBuildImage").buildCache.asCache().volume.name)
|
||||
println("launchCache=" + tasks.getByName<BootBuildImage>("bootBuildImage").launchCache.asCache().volume.name)
|
||||
}
|
||||
}
|
|
@ -99,7 +99,9 @@ public class BootBuildImage extends DefaultTask {
|
|||
|
||||
private final ListProperty<String> tags;
|
||||
|
||||
private Map<String, String> cacheVolumeNames = new HashMap<>();
|
||||
private final CacheSpec buildCache = new CacheSpec();
|
||||
|
||||
private final CacheSpec launchCache = new CacheSpec();
|
||||
|
||||
private final DockerSpec docker = new DockerSpec();
|
||||
|
||||
|
@ -420,41 +422,6 @@ public class BootBuildImage extends DefaultTask {
|
|||
this.tags.addAll(tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cache volume names that will be used when building the image.
|
||||
* @return the cache volume names
|
||||
*/
|
||||
@Input
|
||||
@Optional
|
||||
public Map<String, String> getCacheVolumeNames() {
|
||||
return this.cacheVolumeNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the cache volume names that will be used when building the image.
|
||||
* @param cacheVolumeNames the cache volume names
|
||||
*/
|
||||
public void setCacheVolumeNames(Map<String, String> cacheVolumeNames) {
|
||||
this.cacheVolumeNames = cacheVolumeNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an entry to cache volume names that will be used when building the image.
|
||||
* @param type the type of the entry
|
||||
* @param name the name of the entry
|
||||
*/
|
||||
public void cacheVolumeName(String type, String name) {
|
||||
this.cacheVolumeNames.put(type, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds entries to cache volume names that will be used when building the image.
|
||||
* @param entries the entries to add to cache volume names
|
||||
*/
|
||||
public void cacheVolumeNames(Map<String, String> entries) {
|
||||
this.cacheVolumeNames.putAll(entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the network the build container will connect to.
|
||||
* @return the network
|
||||
|
@ -474,6 +441,62 @@ public class BootBuildImage extends DefaultTask {
|
|||
this.network = network;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the build cache that will be used when building the image.
|
||||
* @return the cache
|
||||
*/
|
||||
@Nested
|
||||
@Optional
|
||||
public CacheSpec getBuildCache() {
|
||||
return this.buildCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customizes the {@link CacheSpec} for the build cache using the given
|
||||
* {@code action}.
|
||||
* @param action the action
|
||||
*/
|
||||
public void buildCache(Action<CacheSpec> action) {
|
||||
action.execute(this.buildCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Customizes the {@link CacheSpec} for the build cache using the given
|
||||
* {@code closure}.
|
||||
* @param closure the closure
|
||||
*/
|
||||
public void buildCache(Closure<?> closure) {
|
||||
buildCache(ConfigureUtil.configureUsing(closure));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the launch cache that will be used when building the image.
|
||||
* @return the cache
|
||||
*/
|
||||
@Nested
|
||||
@Optional
|
||||
public CacheSpec getLaunchCache() {
|
||||
return this.launchCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customizes the {@link CacheSpec} for the launch cache using the given
|
||||
* {@code action}.
|
||||
* @param action the action
|
||||
*/
|
||||
public void launchCache(Action<CacheSpec> action) {
|
||||
action.execute(this.launchCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Customizes the {@link CacheSpec} for the launch cache using the given
|
||||
* {@code closure}.
|
||||
* @param closure the closure
|
||||
*/
|
||||
public void launchCache(Closure<?> closure) {
|
||||
launchCache(ConfigureUtil.configureUsing(closure));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Docker configuration the builder will use.
|
||||
* @return docker configuration.
|
||||
|
@ -537,7 +560,7 @@ public class BootBuildImage extends DefaultTask {
|
|||
request = customizeBuildpacks(request);
|
||||
request = customizeBindings(request);
|
||||
request = customizeTags(request);
|
||||
request = customizeCacheVolumeNames(request);
|
||||
request = customizeCaches(request);
|
||||
request = request.withNetwork(this.network);
|
||||
return request;
|
||||
}
|
||||
|
@ -615,9 +638,12 @@ public class BootBuildImage extends DefaultTask {
|
|||
return request;
|
||||
}
|
||||
|
||||
private BuildRequest customizeCacheVolumeNames(BuildRequest request) {
|
||||
if (this.cacheVolumeNames != null && !this.cacheVolumeNames.isEmpty()) {
|
||||
request = request.withCacheVolumeNames(this.cacheVolumeNames);
|
||||
private BuildRequest customizeCaches(BuildRequest request) {
|
||||
if (this.buildCache.asCache() != null) {
|
||||
request = request.withBuildCache(this.buildCache.asCache());
|
||||
}
|
||||
if (this.launchCache.asCache() != null) {
|
||||
request = request.withLaunchCache(this.launchCache.asCache());
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright 2021 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 groovy.lang.Closure;
|
||||
import org.gradle.api.Action;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.util.ConfigureUtil;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.build.Cache;
|
||||
|
||||
/**
|
||||
* Configuration for an image building cache.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
* @since 2.6.0
|
||||
*/
|
||||
public class CacheSpec {
|
||||
|
||||
private Cache cache = null;
|
||||
|
||||
CacheSpec() {
|
||||
}
|
||||
|
||||
public Cache 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 = new VolumeCacheSpec();
|
||||
action.execute(spec);
|
||||
this.cache = Cache.volume(spec.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures a volume cache using the given {@code closure}.
|
||||
* @param closure the closure
|
||||
*/
|
||||
public void volume(Closure<?> closure) {
|
||||
if (this.cache != null) {
|
||||
throw new GradleException("Each image building cache can be configured only once");
|
||||
}
|
||||
volume(ConfigureUtil.configureUsing(closure));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for an image building cache stored in a Docker volume.
|
||||
*/
|
||||
public static class VolumeCacheSpec {
|
||||
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* Returns the name of the cache.
|
||||
* @return the cache name
|
||||
*/
|
||||
@Input
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of the cache.
|
||||
* @param name the cache name
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -316,6 +316,14 @@ class PackagingDocumentationTests {
|
|||
.contains("urn:cnb:builder:paketo-buildpacks/java");
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void bootBuildImageWithCaches() {
|
||||
BuildResult result = this.gradleBuild.script("src/docs/gradle/packaging/boot-build-image-caches")
|
||||
.build("bootBuildImageCaches");
|
||||
assertThat(result.getOutput()).containsPattern("buildCache=cache-gradle-[\\d]+.build")
|
||||
.containsPattern("launchCache=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"));
|
||||
|
|
|
@ -40,7 +40,10 @@ import org.junit.jupiter.api.condition.DisabledOnOs;
|
|||
import org.junit.jupiter.api.condition.OS;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.docker.DockerApi;
|
||||
import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi;
|
||||
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
|
||||
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.FilePermissions;
|
||||
import org.springframework.boot.gradle.junit.GradleCompatibility;
|
||||
import org.springframework.boot.testsupport.gradle.testkit.GradleBuild;
|
||||
|
@ -73,7 +76,7 @@ class BootBuildImageIntegrationTests {
|
|||
assertThat(result.getOutput()).contains("env: BP_JVM_VERSION=8.*");
|
||||
assertThat(result.getOutput()).contains("Network status: HTTP/2 200");
|
||||
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
|
||||
removeImage(projectName);
|
||||
removeImages(projectName);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
|
@ -91,7 +94,7 @@ class BootBuildImageIntegrationTests {
|
|||
File buildLibs = new File(this.gradleBuild.getProjectDir(), "build/libs");
|
||||
assertThat(buildLibs.listFiles())
|
||||
.containsExactly(new File(buildLibs, this.gradleBuild.getProjectDir().getName() + ".war"));
|
||||
removeImage(projectName);
|
||||
removeImages(projectName);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
|
@ -107,7 +110,7 @@ class BootBuildImageIntegrationTests {
|
|||
File buildLibs = new File(this.gradleBuild.getProjectDir(), "build/libs");
|
||||
assertThat(buildLibs.listFiles())
|
||||
.containsExactly(new File(buildLibs, this.gradleBuild.getProjectDir().getName() + ".war"));
|
||||
removeImage(projectName);
|
||||
removeImages(projectName);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
|
@ -119,7 +122,7 @@ class BootBuildImageIntegrationTests {
|
|||
assertThat(result.getOutput()).contains("example/test-image-name");
|
||||
assertThat(result.getOutput()).contains("---> Test Info buildpack building");
|
||||
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
|
||||
removeImage("example/test-image-name");
|
||||
removeImages("example/test-image-name");
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
|
@ -131,7 +134,7 @@ class BootBuildImageIntegrationTests {
|
|||
assertThat(result.getOutput()).contains("example/test-image-custom");
|
||||
assertThat(result.getOutput()).contains("---> Test Info buildpack building");
|
||||
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
|
||||
removeImage("example/test-image-custom");
|
||||
removeImages("example/test-image-custom");
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
|
@ -146,7 +149,7 @@ class BootBuildImageIntegrationTests {
|
|||
assertThat(result.getOutput()).contains("example/test-image-cmd");
|
||||
assertThat(result.getOutput()).contains("---> Test Info buildpack building");
|
||||
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
|
||||
removeImage("example/test-image-cmd");
|
||||
removeImages("example/test-image-cmd");
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
|
@ -160,7 +163,7 @@ class BootBuildImageIntegrationTests {
|
|||
result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT");
|
||||
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
assertThat(result.getOutput()).doesNotContain("Pulled builder image").doesNotContain("Pulled run image");
|
||||
removeImage(projectName);
|
||||
removeImages(projectName);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
|
@ -173,7 +176,7 @@ class BootBuildImageIntegrationTests {
|
|||
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
|
||||
assertThat(result.getOutput()).contains("---> Test Info buildpack building")
|
||||
.contains("---> Test Info buildpack done");
|
||||
removeImage(projectName);
|
||||
removeImages(projectName);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
|
@ -187,7 +190,7 @@ class BootBuildImageIntegrationTests {
|
|||
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
|
||||
assertThat(result.getOutput()).contains("---> Hello World buildpack");
|
||||
removeImage(projectName);
|
||||
removeImages(projectName);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
|
@ -202,7 +205,7 @@ class BootBuildImageIntegrationTests {
|
|||
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
|
||||
assertThat(result.getOutput()).contains("---> Hello World buildpack");
|
||||
removeImage(projectName);
|
||||
removeImages(projectName);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
|
@ -215,7 +218,7 @@ class BootBuildImageIntegrationTests {
|
|||
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
|
||||
assertThat(result.getOutput()).contains("---> Test Info buildpack building")
|
||||
.contains("---> Test Info buildpack done");
|
||||
removeImage(projectName);
|
||||
removeImages(projectName);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
|
@ -232,7 +235,7 @@ class BootBuildImageIntegrationTests {
|
|||
assertThat(result.getOutput()).contains("binding: certificates/test1.crt=---certificate one---");
|
||||
assertThat(result.getOutput()).contains("binding: certificates/test2.crt=---certificate two---");
|
||||
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
|
||||
removeImage(projectName);
|
||||
removeImages(projectName);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
|
@ -246,8 +249,7 @@ class BootBuildImageIntegrationTests {
|
|||
assertThat(result.getOutput()).contains("---> Test Info buildpack building");
|
||||
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
|
||||
assertThat(result.getOutput()).contains("example.com/myapp:latest");
|
||||
removeImage(projectName);
|
||||
removeImage("example.com/myapp:latest");
|
||||
removeImages(projectName, "example.com/myapp:latest");
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
|
@ -260,7 +262,7 @@ class BootBuildImageIntegrationTests {
|
|||
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
|
||||
assertThat(result.getOutput()).contains("---> Test Info buildpack building");
|
||||
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
|
||||
removeImage(projectName);
|
||||
removeImages(projectName);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
|
@ -274,7 +276,21 @@ class BootBuildImageIntegrationTests {
|
|||
assertThat(result.getOutput()).contains("---> Test Info buildpack building");
|
||||
assertThat(result.getOutput()).contains("Network status: curl failed");
|
||||
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
|
||||
removeImage(projectName);
|
||||
removeImages(projectName);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void buildsImageWithVolumeCaches() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT");
|
||||
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);
|
||||
deleteVolumes("build-cache-volume", "launch-cache-volume");
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
|
@ -325,6 +341,14 @@ class BootBuildImageIntegrationTests {
|
|||
.containsPattern("example/Invalid-Tag-Name");
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWhenCachesAreConfiguredTwice() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage");
|
||||
assertThat(result.getOutput()).containsPattern("Each image building cache can be configured only once");
|
||||
}
|
||||
|
||||
private void writeMainClass() throws IOException {
|
||||
File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example");
|
||||
examplePackage.mkdirs();
|
||||
|
@ -447,9 +471,18 @@ class BootBuildImageIntegrationTests {
|
|||
}
|
||||
}
|
||||
|
||||
private void removeImage(String name) throws IOException {
|
||||
ImageReference imageReference = ImageReference.of(name);
|
||||
new DockerApi().image().remove(imageReference, false);
|
||||
private void removeImages(String... names) throws IOException {
|
||||
ImageApi imageApi = new DockerApi().image();
|
||||
for (String name : names) {
|
||||
imageApi.remove(ImageReference.of(name), false);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteVolumes(String... names) throws IOException {
|
||||
VolumeApi volumeApi = new DockerApi().volume();
|
||||
for (String name : names) {
|
||||
volumeApi.delete(VolumeName.of(name), false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '{version}'
|
||||
}
|
||||
|
||||
sourceCompatibility = '1.8'
|
||||
targetCompatibility = '1.8'
|
||||
|
||||
bootBuildImage {
|
||||
builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1"
|
||||
buildCache {
|
||||
volume {
|
||||
name = "build-cache-volume"
|
||||
}
|
||||
}
|
||||
launchCache {
|
||||
volume {
|
||||
name = "launch-cache-volume"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '{version}'
|
||||
}
|
||||
|
||||
sourceCompatibility = '1.8'
|
||||
targetCompatibility = '1.8'
|
||||
|
||||
bootBuildImage {
|
||||
builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1"
|
||||
buildCache {
|
||||
volume {
|
||||
name = "build-cache-volume1"
|
||||
}
|
||||
volume {
|
||||
name = "build-cache-volum2"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -187,10 +187,18 @@ The value supplied will be passed unvalidated to Docker when creating the builde
|
|||
| One or more additional tags to apply to the generated image.
|
||||
|
|
||||
|
||||
| `cacheVolumeNames`
|
||||
| `caches`
|
||||
| Cache volume names that should be used by the builder instead of generating random names.
|
||||
|
|
||||
|
||||
| `buildCache`
|
||||
| A cache containing layers created by buildpacks and used by the image building process.
|
||||
| A named volume in the Docker daemon, with a name derived from the image name.
|
||||
|
||||
| `launchCache`
|
||||
| A cache containing layers created by buildpacks and used by the image launching process.
|
||||
| A named volume in the Docker daemon, with a name derived from the image name.
|
||||
|
||||
|===
|
||||
|
||||
NOTE: The plugin detects the target Java compatibility of the project using the compiler's plugin configuration or the `maven.compiler.target` property.
|
||||
|
@ -344,10 +352,23 @@ The `publish` option can be specified on the command line as well, as shown in t
|
|||
$ mvn spring-boot:build-image -Dspring-boot.build-image.imageName=docker.example.com/library/my-app:v1 -Dspring-boot.build-image.publish=true
|
||||
----
|
||||
|
||||
[[build-image.examples.caches]]
|
||||
=== Builder Cache Configuration
|
||||
|
||||
The CNB builder caches layers that are used when building and launching an image.
|
||||
By default, these caches are stored as named volumes in the Docker daemon with names that are derived from the full name of the target image.
|
||||
If the image name changes frequently, for example when the project version is used as a tag in the image name, then the caches can be invalidated frequently.
|
||||
|
||||
The cache volumes can be configured to use alternative names to give more control over cache lifecycle as shown in the following example:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,attributes",tabsize=4]
|
||||
----
|
||||
include::../maven/packaging-oci-image/caches-pom.xml[tags=caches]
|
||||
----
|
||||
|
||||
[[build-image.examples.docker]]
|
||||
=== Docker Configuration
|
||||
|
||||
If you need the plugin to communicate with the Docker daemon using a remote connection instead of the default local connection, the connection details can be provided using `docker` parameters as shown in the following example:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,attributes",tabsize=4]
|
||||
|
|
|
@ -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>
|
||||
<volume>
|
||||
<name>cache-${project.artifactId}.build</name>
|
||||
</volume>
|
||||
</buildCache>
|
||||
<launchCache>
|
||||
<volume>
|
||||
<name>cache-${project.artifactId}.launch</name>
|
||||
</volume>
|
||||
</launchCache>
|
||||
</image>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
<!-- end::caches[] -->
|
|
@ -27,8 +27,10 @@ import org.junit.jupiter.api.TestTemplate;
|
|||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.docker.DockerApi;
|
||||
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
|
||||
import org.springframework.boot.buildpack.platform.docker.type.ImageName;
|
||||
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
|
||||
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
|
||||
import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
@ -306,6 +308,18 @@ class BuildImageTests extends AbstractArchiveIntegrationTests {
|
|||
});
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void whenBuildImageIsInvokedWithVolumeCaches(MavenBuild mavenBuild) {
|
||||
mavenBuild.project("build-image-caches").goals("package")
|
||||
.systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT").execute((project) -> {
|
||||
assertThat(buildLog(project)).contains("Building image")
|
||||
.contains("docker.io/library/build-image-caches:0.0.1.BUILD-SNAPSHOT")
|
||||
.contains("Successfully built image");
|
||||
removeImage("build-image-caches", "0.0.1.BUILD-SNAPSHOT");
|
||||
deleteVolumes("build-cache-volume", "launch-cache-volume");
|
||||
});
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWhenBuildImageIsInvokedOnMultiModuleProjectWithBuildImageGoal(MavenBuild mavenBuild) {
|
||||
mavenBuild.project("build-image-multi-module").goals("spring-boot:build-image")
|
||||
|
@ -343,6 +357,13 @@ class BuildImageTests extends AbstractArchiveIntegrationTests {
|
|||
.contains("is required for building an image"));
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWhenCachesAreConfiguredTwice(MavenBuild mavenBuild) {
|
||||
mavenBuild.project("build-image-caches-multiple").goals("package")
|
||||
.executeAndFail((project) -> assertThat(buildLog(project))
|
||||
.contains("Each image building cache can be configured only once"));
|
||||
}
|
||||
|
||||
private void writeLongNameResource(File project) {
|
||||
StringBuilder name = new StringBuilder();
|
||||
new Random().ints('a', 'z' + 1).limit(128).forEach((i) -> name.append((char) i));
|
||||
|
@ -366,4 +387,11 @@ class BuildImageTests extends AbstractArchiveIntegrationTests {
|
|||
}
|
||||
}
|
||||
|
||||
private void deleteVolumes(String... names) throws IOException {
|
||||
VolumeApi volumeApi = new DockerApi().volume();
|
||||
for (String name : names) {
|
||||
volumeApi.delete(VolumeName.of(name), false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
<?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-caches-multiple</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</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<image>
|
||||
<builder>projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1</builder>
|
||||
<buildCache>
|
||||
<volume>
|
||||
<name>build-cache-volume1</name>
|
||||
</volume>
|
||||
<volume>
|
||||
<name>build-cache-volume2</name>
|
||||
</volume>
|
||||
</buildCache>
|
||||
</image>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright 2012-2021 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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-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</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<image>
|
||||
<builder>projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1</builder>
|
||||
<buildCache>
|
||||
<volume>
|
||||
<name>build-cache-volume</name>
|
||||
</volume>
|
||||
</buildCache>
|
||||
<launchCache>
|
||||
<volume>
|
||||
<name>launch-cache-volume</name>
|
||||
</volume>
|
||||
</launchCache>
|
||||
</image>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright 2012-2021 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright 2012-2021 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.springframework.boot.buildpack.platform.build.Cache;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Encapsulates configuration of an image building cache.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
* @since 2.6.0
|
||||
*/
|
||||
public class CacheInfo {
|
||||
|
||||
private Cache cache;
|
||||
|
||||
public CacheInfo() {
|
||||
}
|
||||
|
||||
CacheInfo(VolumeCacheInfo volumeCacheInfo) {
|
||||
this.cache = Cache.volume(volumeCacheInfo.getName());
|
||||
}
|
||||
|
||||
public void setVolume(VolumeCacheInfo info) {
|
||||
Assert.state(this.cache == null, "Each image building cache can be configured only once");
|
||||
this.cache = Cache.volume(info.getName());
|
||||
}
|
||||
|
||||
Cache asCache() {
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates configuration of an image building cache stored in a volume.
|
||||
*/
|
||||
public static class VolumeCacheInfo {
|
||||
|
||||
private String name;
|
||||
|
||||
public VolumeCacheInfo() {
|
||||
}
|
||||
|
||||
VolumeCacheInfo(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -70,7 +70,9 @@ public class Image {
|
|||
|
||||
List<String> tags;
|
||||
|
||||
Map<String, String> cacheVolumeNames;
|
||||
CacheInfo buildCache;
|
||||
|
||||
CacheInfo launchCache;
|
||||
|
||||
/**
|
||||
* The name of the created image.
|
||||
|
@ -215,8 +217,11 @@ public class Image {
|
|||
if (!CollectionUtils.isEmpty(this.tags)) {
|
||||
request = request.withTags(this.tags.stream().map(ImageReference::of).collect(Collectors.toList()));
|
||||
}
|
||||
if (this.cacheVolumeNames != null && !this.cacheVolumeNames.isEmpty()) {
|
||||
request = request.withCacheVolumeNames(this.cacheVolumeNames);
|
||||
if (this.buildCache != null) {
|
||||
request = request.withBuildCache(this.buildCache.asCache());
|
||||
}
|
||||
if (this.launchCache != null) {
|
||||
request = request.withLaunchCache(this.launchCache.asCache());
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
|
|
@ -28,11 +28,13 @@ import org.junit.jupiter.api.Test;
|
|||
|
||||
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.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.VolumeCacheInfo;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.entry;
|
||||
|
@ -167,6 +169,22 @@ class ImageTests {
|
|||
ImageReference.of("example.com/my-app:0.0.1-SNAPSHOT"), ImageReference.of("example.com/my-app:latest"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBuildRequestWhenHasBuildVolumeCacheUsesCache() {
|
||||
Image image = new Image();
|
||||
image.buildCache = new CacheInfo(new VolumeCacheInfo("build-cache-vol"));
|
||||
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
|
||||
assertThat(request.getBuildCache()).isEqualTo(Cache.volume("build-cache-vol"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBuildRequestWhenHasLaunchVolumeCacheUsesCache() {
|
||||
Image image = new Image();
|
||||
image.launchCache = new CacheInfo(new VolumeCacheInfo("launch-cache-vol"));
|
||||
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
|
||||
assertThat(request.getLaunchCache()).isEqualTo(Cache.volume("launch-cache-vol"));
|
||||
}
|
||||
|
||||
private Artifact createArtifact() {
|
||||
return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile",
|
||||
"jar", null, new DefaultArtifactHandler());
|
||||
|
|
Loading…
Reference in New Issue