Polish "Add option to customize cache volume names when building an image"

See gh-28292
This commit is contained in:
Scott Frederick
2021-10-14 14:55:04 -05:00
parent dc36346285
commit 871468931f
27 changed files with 937 additions and 157 deletions

View File

@@ -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;
}
/**

View File

@@ -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;
}
}
}

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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);

View File

@@ -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"
]
}
}