Add support for build workspace option when building images

Closes gh-37478
pull/37500/head
Scott Frederick 1 year ago
parent d8576f319e
commit 4433fcd1f2

@ -77,6 +77,8 @@ public class BuildRequest {
private final List<ImageReference> tags; private final List<ImageReference> tags;
private final Cache buildWorkspace;
private final Cache buildCache; private final Cache buildCache;
private final Cache launchCache; private final Cache launchCache;
@ -102,6 +104,7 @@ public class BuildRequest {
this.bindings = Collections.emptyList(); this.bindings = Collections.emptyList();
this.network = null; this.network = null;
this.tags = Collections.emptyList(); this.tags = Collections.emptyList();
this.buildWorkspace = null;
this.buildCache = null; this.buildCache = null;
this.launchCache = null; this.launchCache = null;
this.createdDate = null; this.createdDate = null;
@ -111,8 +114,8 @@ public class BuildRequest {
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent, ImageReference builder, BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent, ImageReference builder,
ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache, ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache,
boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List<BuildpackReference> buildpacks, boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List<BuildpackReference> buildpacks,
List<Binding> bindings, String network, List<ImageReference> tags, Cache buildCache, Cache launchCache, List<Binding> bindings, String network, List<ImageReference> tags, Cache buildWorkspace, Cache buildCache,
Instant createdDate, String applicationDirectory) { Cache launchCache, Instant createdDate, String applicationDirectory) {
this.name = name; this.name = name;
this.applicationContent = applicationContent; this.applicationContent = applicationContent;
this.builder = builder; this.builder = builder;
@ -127,6 +130,7 @@ public class BuildRequest {
this.bindings = bindings; this.bindings = bindings;
this.network = network; this.network = network;
this.tags = tags; this.tags = tags;
this.buildWorkspace = buildWorkspace;
this.buildCache = buildCache; this.buildCache = buildCache;
this.launchCache = launchCache; this.launchCache = launchCache;
this.createdDate = createdDate; this.createdDate = createdDate;
@ -142,8 +146,8 @@ public class BuildRequest {
Assert.notNull(builder, "Builder must not be null"); Assert.notNull(builder, "Builder must not be null");
return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage, return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage,
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.launchCache, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache,
this.createdDate, this.applicationDirectory); this.launchCache, this.createdDate, this.applicationDirectory);
} }
/** /**
@ -154,8 +158,8 @@ public class BuildRequest {
public BuildRequest withRunImage(ImageReference runImageName) { public BuildRequest withRunImage(ImageReference runImageName) {
return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(), return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(),
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.launchCache, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache,
this.createdDate, this.applicationDirectory); this.launchCache, this.createdDate, this.applicationDirectory);
} }
/** /**
@ -167,7 +171,7 @@ public class BuildRequest {
Assert.notNull(creator, "Creator must not be null"); Assert.notNull(creator, "Creator must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env, 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.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory); this.applicationDirectory);
} }
@ -184,8 +188,8 @@ public class BuildRequest {
env.put(name, value); env.put(name, value);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.launchCache, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache,
this.createdDate, this.applicationDirectory); this.launchCache, this.createdDate, this.applicationDirectory);
} }
/** /**
@ -199,8 +203,8 @@ public class BuildRequest {
updatedEnv.putAll(env); updatedEnv.putAll(env);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy, Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy,
this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace,
this.launchCache, this.createdDate, this.applicationDirectory); this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory);
} }
/** /**
@ -211,7 +215,7 @@ public class BuildRequest {
public BuildRequest withCleanCache(boolean cleanCache) { public BuildRequest withCleanCache(boolean cleanCache) {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, 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, cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory); this.applicationDirectory);
} }
@ -223,7 +227,7 @@ public class BuildRequest {
public BuildRequest withVerboseLogging(boolean verboseLogging) { public BuildRequest withVerboseLogging(boolean verboseLogging) {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, 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.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory); this.applicationDirectory);
} }
@ -235,7 +239,7 @@ public class BuildRequest {
public BuildRequest withPullPolicy(PullPolicy pullPolicy) { public BuildRequest withPullPolicy(PullPolicy pullPolicy) {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, 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.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory); this.applicationDirectory);
} }
@ -247,7 +251,7 @@ public class BuildRequest {
public BuildRequest withPublish(boolean publish) { public BuildRequest withPublish(boolean publish) {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, 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.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks, this.bindings,
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory); this.applicationDirectory);
} }
@ -272,7 +276,7 @@ public class BuildRequest {
Assert.notNull(buildpacks, "Buildpacks must not be null"); Assert.notNull(buildpacks, "Buildpacks must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, 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.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks, this.bindings,
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory); this.applicationDirectory);
} }
@ -297,7 +301,7 @@ public class BuildRequest {
Assert.notNull(bindings, "Bindings must not be null"); Assert.notNull(bindings, "Bindings must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, 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.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, bindings,
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory); this.applicationDirectory);
} }
@ -310,7 +314,8 @@ public class BuildRequest {
public BuildRequest withNetwork(String network) { public BuildRequest withNetwork(String network) {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, 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.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
network, this.tags, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory);
} }
/** /**
@ -332,7 +337,21 @@ public class BuildRequest {
Assert.notNull(tags, "Tags must not be null"); Assert.notNull(tags, "Tags must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, 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.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network, tags, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); this.network, tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory);
}
/**
* Return a new {@link BuildRequest} with an updated build workspace.
* @param buildWorkspace the build workspace
* @return an updated build request
*/
public BuildRequest withBuildWorkspace(Cache buildWorkspace) {
Assert.notNull(buildWorkspace, "BuildWorkspace 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, buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory);
} }
/** /**
@ -344,7 +363,8 @@ public class BuildRequest {
Assert.notNull(buildCache, "BuildCache must not be null"); Assert.notNull(buildCache, "BuildCache must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, 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.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network, this.tags, buildCache, this.launchCache, this.createdDate, this.applicationDirectory); this.network, this.tags, this.buildWorkspace, buildCache, this.launchCache, this.createdDate,
this.applicationDirectory);
} }
/** /**
@ -356,7 +376,8 @@ public class BuildRequest {
Assert.notNull(launchCache, "LaunchCache must not be null"); Assert.notNull(launchCache, "LaunchCache must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, 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.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network, this.tags, this.buildCache, launchCache, this.createdDate, this.applicationDirectory); this.network, this.tags, this.buildWorkspace, this.buildCache, launchCache, this.createdDate,
this.applicationDirectory);
} }
/** /**
@ -368,8 +389,8 @@ public class BuildRequest {
Assert.notNull(createdDate, "CreatedDate must not be null"); Assert.notNull(createdDate, "CreatedDate must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, 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.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network, this.tags, this.buildCache, this.launchCache, parseCreatedDate(createdDate), this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache,
this.applicationDirectory); parseCreatedDate(createdDate), this.applicationDirectory);
} }
private Instant parseCreatedDate(String createdDate) { private Instant parseCreatedDate(String createdDate) {
@ -393,7 +414,8 @@ public class BuildRequest {
Assert.notNull(applicationDirectory, "ApplicationDirectory must not be null"); Assert.notNull(applicationDirectory, "ApplicationDirectory must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, 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.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, applicationDirectory); this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate,
applicationDirectory);
} }
/** /**
@ -513,6 +535,10 @@ public class BuildRequest {
return this.tags; return this.tags;
} }
public Cache getBuildWorkspace() {
return this.buildWorkspace;
}
/** /**
* Return the custom build cache that should be used by the lifecycle. * Return the custom build cache that should be used by the lifecycle.
* @return the build cache * @return the build cache

@ -18,7 +18,9 @@ package org.springframework.boot.buildpack.platform.build;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import com.sun.jna.Platform; import com.sun.jna.Platform;
@ -70,9 +72,9 @@ class Lifecycle implements Closeable {
private final ApiVersion platformVersion; private final ApiVersion platformVersion;
private final VolumeName layersVolume; private final Cache layers;
private final VolumeName applicationVolume; private final Cache application;
private final Cache buildCache; private final Cache buildCache;
@ -101,17 +103,13 @@ class Lifecycle implements Closeable {
this.builder = builder; this.builder = builder;
this.lifecycleVersion = LifecycleVersion.parse(builder.getBuilderMetadata().getLifecycle().getVersion()); this.lifecycleVersion = LifecycleVersion.parse(builder.getBuilderMetadata().getLifecycle().getVersion());
this.platformVersion = getPlatformVersion(builder.getBuilderMetadata().getLifecycle()); this.platformVersion = getPlatformVersion(builder.getBuilderMetadata().getLifecycle());
this.layersVolume = createRandomVolumeName("pack-layers-"); this.layers = getLayersBindingSource(request);
this.applicationVolume = createRandomVolumeName("pack-app-"); this.application = getApplicationBindingSource(request);
this.buildCache = getBuildCache(request); this.buildCache = getBuildCache(request);
this.launchCache = getLaunchCache(request); this.launchCache = getLaunchCache(request);
this.applicationDirectory = getApplicationDirectory(request); this.applicationDirectory = getApplicationDirectory(request);
} }
protected VolumeName createRandomVolumeName(String prefix) {
return VolumeName.random(prefix);
}
private Cache getBuildCache(BuildRequest request) { private Cache getBuildCache(BuildRequest request) {
if (request.getBuildCache() != null) { if (request.getBuildCache() != null) {
return request.getBuildCache(); return request.getBuildCache();
@ -130,11 +128,6 @@ class Lifecycle implements Closeable {
return (request.getApplicationDirectory() != null) ? request.getApplicationDirectory() : Directory.APPLICATION; return (request.getApplicationDirectory() != null) ? request.getApplicationDirectory() : Directory.APPLICATION;
} }
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) { private ApiVersion getPlatformVersion(BuilderMetadata.Lifecycle lifecycle) {
if (lifecycle.getApis().getPlatform() != null) { if (lifecycle.getApis().getPlatform() != null) {
String[] supportedVersions = lifecycle.getApis().getPlatform(); String[] supportedVersions = lifecycle.getApis().getPlatform();
@ -153,12 +146,7 @@ class Lifecycle implements Closeable {
this.executed = true; this.executed = true;
this.log.executingLifecycle(this.request, this.lifecycleVersion, this.buildCache); this.log.executingLifecycle(this.request, this.lifecycleVersion, this.buildCache);
if (this.request.isCleanCache()) { if (this.request.isCleanCache()) {
if (this.buildCache.getVolume() != null) { deleteCache(this.buildCache);
deleteVolume(this.buildCache.getVolume().getVolumeName());
}
if (this.buildCache.getBind() != null) {
deleteBind(this.buildCache.getBind().getSource());
}
} }
run(createPhase()); run(createPhase());
this.log.executedLifecycle(this.request); this.log.executedLifecycle(this.request);
@ -183,8 +171,8 @@ class Lifecycle implements Closeable {
phase.withArgs("-process-type=web"); phase.withArgs("-process-type=web");
} }
phase.withArgs(this.request.getName()); phase.withArgs(this.request.getName());
phase.withBinding(Binding.from(this.layersVolume, Directory.LAYERS)); phase.withBinding(Binding.from(getCacheBindingSource(this.layers), Directory.LAYERS));
phase.withBinding(Binding.from(this.applicationVolume, this.applicationDirectory)); phase.withBinding(Binding.from(getCacheBindingSource(this.application), this.applicationDirectory));
phase.withBinding(Binding.from(getCacheBindingSource(this.buildCache), Directory.CACHE)); phase.withBinding(Binding.from(getCacheBindingSource(this.buildCache), Directory.CACHE));
phase.withBinding(Binding.from(getCacheBindingSource(this.launchCache), Directory.LAUNCH_CACHE)); phase.withBinding(Binding.from(getCacheBindingSource(this.launchCache), Directory.LAUNCH_CACHE));
if (this.request.getBindings() != null) { if (this.request.getBindings() != null) {
@ -200,10 +188,42 @@ class Lifecycle implements Closeable {
return phase; return phase;
} }
private Cache getLayersBindingSource(BuildRequest request) {
if (request.getBuildWorkspace() != null) {
return getBuildWorkspaceBindingSource(request.getBuildWorkspace(), "layers");
}
return createVolumeCache("pack-layers-");
}
private Cache getApplicationBindingSource(BuildRequest request) {
if (request.getBuildWorkspace() != null) {
return getBuildWorkspaceBindingSource(request.getBuildWorkspace(), "app");
}
return createVolumeCache("pack-app-");
}
private Cache getBuildWorkspaceBindingSource(Cache buildWorkspace, String suffix) {
return (buildWorkspace.getVolume() != null) ? Cache.volume(buildWorkspace.getVolume().getName() + "-" + suffix)
: Cache.bind(buildWorkspace.getBind().getSource() + "-" + suffix);
}
private String getCacheBindingSource(Cache cache) { private String getCacheBindingSource(Cache cache) {
return (cache.getVolume() != null) ? cache.getVolume().getName() : cache.getBind().getSource(); return (cache.getVolume() != null) ? cache.getVolume().getName() : cache.getBind().getSource();
} }
private Cache createVolumeCache(String prefix) {
return Cache.volume(createRandomVolumeName(prefix));
}
private Cache createVolumeCache(BuildRequest request, String suffix) {
return Cache.volume(
VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", "." + suffix, 6));
}
protected VolumeName createRandomVolumeName(String prefix) {
return VolumeName.random(prefix);
}
private void configureDaemonAccess(Phase phase) { private void configureDaemonAccess(Phase phase) {
if (this.dockerHost != null) { if (this.dockerHost != null) {
if (this.dockerHost.isRemote()) { if (this.dockerHost.isRemote()) {
@ -255,6 +275,9 @@ class Lifecycle implements Closeable {
return this.docker.container().create(config); return this.docker.container().create(config);
} }
try { try {
if (this.application.getBind() != null) {
Files.createDirectories(Path.of(this.application.getBind().getSource()));
}
TarArchive applicationContent = this.request.getApplicationContent(this.builder.getBuildOwner()); TarArchive applicationContent = this.request.getApplicationContent(this.builder.getBuildOwner());
return this.docker.container() return this.docker.container()
.create(config, ContainerContent.of(applicationContent, this.applicationDirectory)); .create(config, ContainerContent.of(applicationContent, this.applicationDirectory));
@ -266,8 +289,17 @@ class Lifecycle implements Closeable {
@Override @Override
public void close() throws IOException { public void close() throws IOException {
deleteVolume(this.layersVolume); deleteCache(this.layers);
deleteVolume(this.applicationVolume); deleteCache(this.application);
}
private void deleteCache(Cache cache) throws IOException {
if (cache.getVolume() != null) {
deleteVolume(cache.getVolume().getVolumeName());
}
if (cache.getBind() != null) {
deleteBind(cache.getBind().getSource());
}
} }
private void deleteVolume(VolumeName name) throws IOException { private void deleteVolume(VolumeName name) throws IOException {

@ -233,6 +233,22 @@ class BuildRequestTests {
.withMessage("Tags must not be null"); .withMessage("Tags must not be null");
} }
@Test
void withBuildWorkspaceVolumeAddsWorkspace() throws IOException {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
BuildRequest withWorkspace = request.withBuildWorkspace(Cache.volume("build-workspace"));
assertThat(request.getBuildWorkspace()).isNull();
assertThat(withWorkspace.getBuildWorkspace()).isEqualTo(Cache.volume("build-workspace"));
}
@Test
void withBuildWorkspaceBindAddsWorkspace() throws IOException {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
BuildRequest withWorkspace = request.withBuildWorkspace(Cache.bind("/tmp/build-workspace"));
assertThat(request.getBuildWorkspace()).isNull();
assertThat(withWorkspace.getBuildWorkspace()).isEqualTo(Cache.bind("/tmp/build-workspace"));
}
@Test @Test
void withBuildVolumeCacheAddsCache() throws IOException { void withBuildVolumeCacheAddsCache() throws IOException {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));

@ -211,7 +211,8 @@ class LifecycleTests {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest().withBuildCache(Cache.volume("build-volume")) BuildRequest request = getTestRequest().withBuildWorkspace(Cache.volume("work-volume"))
.withBuildCache(Cache.volume("build-volume"))
.withLaunchCache(Cache.volume("launch-volume")); .withLaunchCache(Cache.volume("launch-volume"));
createLifecycle(request).execute(); createLifecycle(request).execute();
assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-cache-volumes.json")); assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-cache-volumes.json"));
@ -223,7 +224,8 @@ class LifecycleTests {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest().withBuildCache(Cache.bind("/tmp/build-cache")) BuildRequest request = getTestRequest().withBuildWorkspace(Cache.bind("/tmp/work"))
.withBuildCache(Cache.bind("/tmp/build-cache"))
.withLaunchCache(Cache.bind("/tmp/launch-cache")); .withLaunchCache(Cache.bind("/tmp/launch-cache"));
createLifecycle(request).execute(); createLifecycle(request).execute();
assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-cache-bind-mounts.json")); assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-cache-bind-mounts.json"));

@ -27,8 +27,8 @@
"HostConfig": { "HostConfig": {
"Binds": [ "Binds": [
"/var/run/docker.sock:/var/run/docker.sock", "/var/run/docker.sock:/var/run/docker.sock",
"pack-layers-aaaaaaaaaa:/layers", "/tmp/work-layers:/layers",
"pack-app-aaaaaaaaaa:/workspace", "/tmp/work-app:/workspace",
"/tmp/build-cache:/cache", "/tmp/build-cache:/cache",
"/tmp/launch-cache:/launch-cache" "/tmp/launch-cache:/launch-cache"
], ],

@ -27,8 +27,8 @@
"HostConfig": { "HostConfig": {
"Binds": [ "Binds": [
"/var/run/docker.sock:/var/run/docker.sock", "/var/run/docker.sock:/var/run/docker.sock",
"pack-layers-aaaaaaaaaa:/layers", "work-volume-layers:/layers",
"pack-app-aaaaaaaaaa:/workspace", "work-volume-app:/workspace",
"build-volume:/cache", "build-volume:/cache",
"launch-volume:/launch-cache" "launch-volume:/launch-cache"
], ],

@ -193,14 +193,22 @@ The value supplied will be passed unvalidated to Docker when creating the builde
The values provided to the `tags` option should be full image references in the form of `[image name]:[tag]` or `[repository]/[image name]:[tag]`. The values provided to the `tags` option should be full image references in the form of `[image name]:[tag]` or `[repository]/[image name]:[tag]`.
| |
| `buildWorkspace`
|
| A temporary workspace that will be used by the builder and buildpacks to store files during image building.
The value can be a named volume or a bind mount location.
| A named volume in the Docker daemon, with a name derived from the image name.
| `buildCache` | `buildCache`
| |
| A cache containing layers created by buildpacks and used by the image building process. | A cache containing layers created by buildpacks and used by the image building process.
The value can be a named volume or a bind mount location.
| A named volume in the Docker daemon, with a name derived from the image name. | A named volume in the Docker daemon, with a name derived from the image name.
| `launchCache` | `launchCache`
| |
| A cache containing layers created by buildpacks and used by the image launching process. | A cache containing layers created by buildpacks and used by the image launching process.
The value can be a named volume or a bind mount location.
| A named volume in the Docker daemon, with a name derived from the image name. | A named volume in the Docker daemon, with a name derived from the image name.
| `createdDate` | `createdDate`
@ -420,7 +428,7 @@ The publish option can be specified on the command line as well, as shown in thi
---- ----
[[build-image.examples.caches]] [[build-image.examples.caches]]
=== Builder Cache Configuration === Builder Cache and Workspace Configuration
The CNB builder caches layers that are used when building and launching an image. 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. 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.
@ -440,7 +448,10 @@ include::../gradle/packaging/boot-build-image-caches.gradle[tags=caches]
include::../gradle/packaging/boot-build-image-caches.gradle.kts[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: Builders and buildpacks need a location to store temporary files during image building.
By default, this temporary build workspace is stored in a named volume.
The caches and the build workspace 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"] [source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
.Groovy .Groovy

@ -9,6 +9,11 @@ tasks.named("bootJar") {
// tag::caches[] // tag::caches[]
tasks.named("bootBuildImage") { tasks.named("bootBuildImage") {
buildWorkspace {
bind {
source = "/tmp/cache-${rootProject.name}.work"
}
}
buildCache { buildCache {
bind { bind {
source = "/tmp/cache-${rootProject.name}.build" source = "/tmp/cache-${rootProject.name}.build"
@ -24,6 +29,7 @@ tasks.named("bootBuildImage") {
tasks.register("bootBuildImageCaches") { tasks.register("bootBuildImageCaches") {
doFirst { doFirst {
bootBuildImage.buildWorkspace.asCache().with { print "buildWorkspace=$source" }
bootBuildImage.buildCache.asCache().with { println "buildCache=$source" } bootBuildImage.buildCache.asCache().with { println "buildCache=$source" }
bootBuildImage.launchCache.asCache().with { println "launchCache=$source" } bootBuildImage.launchCache.asCache().with { println "launchCache=$source" }
} }

@ -7,6 +7,11 @@ plugins {
// tag::caches[] // tag::caches[]
tasks.named<BootBuildImage>("bootBuildImage") { tasks.named<BootBuildImage>("bootBuildImage") {
buildWorkspace {
bind {
source.set("/tmp/cache-${rootProject.name}.work")
}
}
buildCache { buildCache {
bind { bind {
source.set("/tmp/cache-${rootProject.name}.build") source.set("/tmp/cache-${rootProject.name}.build")
@ -22,6 +27,7 @@ tasks.named<BootBuildImage>("bootBuildImage") {
tasks.register("bootBuildImageCaches") { tasks.register("bootBuildImageCaches") {
doFirst { doFirst {
println("buildWorkspace=" + tasks.getByName<BootBuildImage>("bootBuildImage").buildWorkspace.asCache().bind.source)
println("buildCache=" + tasks.getByName<BootBuildImage>("bootBuildImage").buildCache.asCache().bind.source) println("buildCache=" + tasks.getByName<BootBuildImage>("bootBuildImage").buildCache.asCache().bind.source)
println("launchCache=" + tasks.getByName<BootBuildImage>("bootBuildImage").launchCache.asCache().bind.source) println("launchCache=" + tasks.getByName<BootBuildImage>("bootBuildImage").launchCache.asCache().bind.source)
} }

@ -69,6 +69,8 @@ public abstract class BootBuildImage extends DefaultTask {
private final String projectName; private final String projectName;
private final CacheSpec buildWorkspace;
private final CacheSpec buildCache; private final CacheSpec buildCache;
private final CacheSpec launchCache; private final CacheSpec launchCache;
@ -91,6 +93,7 @@ public abstract class BootBuildImage extends DefaultTask {
getCleanCache().convention(false); getCleanCache().convention(false);
getVerboseLogging().convention(false); getVerboseLogging().convention(false);
getPublish().convention(false); getPublish().convention(false);
this.buildWorkspace = getProject().getObjects().newInstance(CacheSpec.class);
this.buildCache = getProject().getObjects().newInstance(CacheSpec.class); this.buildCache = getProject().getObjects().newInstance(CacheSpec.class);
this.launchCache = getProject().getObjects().newInstance(CacheSpec.class); this.launchCache = getProject().getObjects().newInstance(CacheSpec.class);
this.docker = getProject().getObjects().newInstance(DockerSpec.class); this.docker = getProject().getObjects().newInstance(DockerSpec.class);
@ -222,6 +225,25 @@ public abstract class BootBuildImage extends DefaultTask {
@Option(option = "network", description = "Connect detect and build containers to network") @Option(option = "network", description = "Connect detect and build containers to network")
public abstract Property<String> getNetwork(); public abstract Property<String> getNetwork();
/**
* Returns the build temporary workspace that will be used when building the image.
* @return the cache
*/
@Nested
@Optional
public CacheSpec getBuildWorkspace() {
return this.buildWorkspace;
}
/**
* Customizes the {@link CacheSpec} for the build temporary workspace using the given
* {@code action}.
* @param action the action
*/
public void buildWorkspace(Action<CacheSpec> action) {
action.execute(this.buildWorkspace);
}
/** /**
* Returns the build cache that will be used when building the image. * Returns the build cache that will be used when building the image.
* @return the cache * @return the cache
@ -400,6 +422,9 @@ public abstract class BootBuildImage extends DefaultTask {
} }
private BuildRequest customizeCaches(BuildRequest request) { private BuildRequest customizeCaches(BuildRequest request) {
if (this.buildWorkspace.asCache() != null) {
request = request.withBuildWorkspace((this.buildWorkspace.asCache()));
}
if (this.buildCache.asCache() != null) { if (this.buildCache.asCache() != null) {
request = request.withBuildCache(this.buildCache.asCache()); request = request.withBuildCache(this.buildCache.asCache());
} }

@ -343,7 +343,8 @@ class PackagingDocumentationTests {
void bootBuildImageWithBindCaches() { void bootBuildImageWithBindCaches() {
BuildResult result = this.gradleBuild.script("src/docs/gradle/packaging/boot-build-image-bind-caches") BuildResult result = this.gradleBuild.script("src/docs/gradle/packaging/boot-build-image-bind-caches")
.build("bootBuildImageCaches"); .build("bootBuildImageCaches");
assertThat(result.getOutput()).containsPattern("buildCache=/tmp/cache-gradle-[\\d]+.build") assertThat(result.getOutput()).containsPattern("buildWorkspace=/tmp/cache-gradle-[\\d]+.work")
.containsPattern("buildCache=/tmp/cache-gradle-[\\d]+.build")
.containsPattern("launchCache=/tmp/cache-gradle-[\\d]+.launch"); .containsPattern("launchCache=/tmp/cache-gradle-[\\d]+.launch");
} }

@ -11,6 +11,11 @@ java {
bootBuildImage { bootBuildImage {
builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2" builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2"
pullPolicy = "IF_NOT_PRESENT" pullPolicy = "IF_NOT_PRESENT"
buildWorkspace {
bind {
source = System.getProperty('java.io.tmpdir') + "/junit-image-pack-${rootProject.name}-work"
}
}
buildCache { buildCache {
bind { bind {
source = System.getProperty('java.io.tmpdir') + "/junit-image-cache-${rootProject.name}-build" source = System.getProperty('java.io.tmpdir') + "/junit-image-cache-${rootProject.name}-build"

@ -11,6 +11,11 @@ java {
bootBuildImage { bootBuildImage {
builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2" builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2"
pullPolicy = "IF_NOT_PRESENT" pullPolicy = "IF_NOT_PRESENT"
buildWorkspace {
volume {
name = "pack-${rootProject.name}.work"
}
}
buildCache { buildCache {
volume { volume {
name = "cache-${rootProject.name}.build" name = "cache-${rootProject.name}.build"

@ -202,12 +202,19 @@ The value supplied will be passed unvalidated to Docker when creating the builde
The values provided to the `tags` option should be full image references in the form of `[image name]:[tag]` or `[repository]/[image name]:[tag]`. The values provided to the `tags` option should be full image references in the form of `[image name]:[tag]` or `[repository]/[image name]:[tag]`.
| |
| `buildWorkspace`
| A temporary workspace that will be used by the builder and buildpacks to store files during image building.
The value can be a named volume or a bind mount location.
| A named volume in the Docker daemon, with a name derived from the image name.
| `buildCache` | `buildCache`
| A cache containing layers created by buildpacks and used by the image building process. | A cache containing layers created by buildpacks and used by the image building process.
The value can be a named volume or a bind mount location.
| A named volume in the Docker daemon, with a name derived from the image name. | A named volume in the Docker daemon, with a name derived from the image name.
| `launchCache` | `launchCache`
| A cache containing layers created by buildpacks and used by the image launching process. | A cache containing layers created by buildpacks and used by the image launching process.
The value can be a named volume or a bind mount location.
| A named volume in the Docker daemon, with a name derived from the image name. | A named volume in the Docker daemon, with a name derived from the image name.
| `createdDate` + | `createdDate` +
@ -403,7 +410,7 @@ include::../maven/packaging-oci-image/docker-pom-authentication-command-line.xml
---- ----
[[build-image.examples.caches]] [[build-image.examples.caches]]
=== Builder Cache Configuration === Builder Cache and Workspace Configuration
The CNB builder caches layers that are used when building and launching an image. 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. 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.
@ -416,7 +423,10 @@ The cache volumes can be configured to use alternative names to give more contro
include::../maven/packaging-oci-image/caches-pom.xml[tags=caches] 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: Builders and buildpacks need a location to store temporary files during image building.
By default, this temporary build workspace is stored in a named volume.
The caches and the build workspace 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] [source,xml,indent=0,subs="verbatim,attributes",tabsize=4]
---- ----

@ -8,6 +8,11 @@
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
<configuration> <configuration>
<image> <image>
<buildWorkspace>
<bind>
<source>/tmp/cache-${project.artifactId}.work</source>
</bind>
</buildWorkspace>
<buildCache> <buildCache>
<bind> <bind>
<source>/tmp/cache-${project.artifactId}.build</source> <source>/tmp/cache-${project.artifactId}.build</source>

@ -24,6 +24,11 @@
<configuration> <configuration>
<image> <image>
<builder>projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2</builder> <builder>projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2</builder>
<buildWorkspace>
<bind>
<source>${java.io.tmpdir}/junit-image-cache-${test-build-id}-work</source>
</bind>
</buildWorkspace>
<buildCache> <buildCache>
<bind> <bind>
<source>${java.io.tmpdir}/junit-image-cache-${test-build-id}-build</source> <source>${java.io.tmpdir}/junit-image-cache-${test-build-id}-build</source>

@ -24,6 +24,11 @@
<configuration> <configuration>
<image> <image>
<builder>projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2</builder> <builder>projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2</builder>
<buildWorkspace>
<volume>
<name>cache-${test-build-id}.work</name>
</volume>
</buildWorkspace>
<buildCache> <buildCache>
<volume> <volume>
<name>cache-${test-build-id}.build</name> <name>cache-${test-build-id}.build</name>

@ -69,6 +69,8 @@ public class Image {
List<String> tags; List<String> tags;
CacheInfo buildWorkspace;
CacheInfo buildCache; CacheInfo buildCache;
CacheInfo launchCache; CacheInfo launchCache;
@ -243,6 +245,9 @@ public class Image {
if (!CollectionUtils.isEmpty(this.tags)) { if (!CollectionUtils.isEmpty(this.tags)) {
request = request.withTags(this.tags.stream().map(ImageReference::of).toList()); request = request.withTags(this.tags.stream().map(ImageReference::of).toList());
} }
if (this.buildWorkspace != null) {
request = request.withBuildWorkspace(this.buildWorkspace.asCache());
}
if (this.buildCache != null) { if (this.buildCache != null) {
request = request.withBuildCache(this.buildCache.asCache()); request = request.withBuildCache(this.buildCache.asCache());
} }

@ -170,6 +170,14 @@ class ImageTests {
ImageReference.of("example.com/my-app:0.0.1-SNAPSHOT"), ImageReference.of("example.com/my-app:latest")); ImageReference.of("example.com/my-app:0.0.1-SNAPSHOT"), ImageReference.of("example.com/my-app:latest"));
} }
@Test
void getBuildRequestWhenHasBuildWorkspaceVolumeUsesWorkspace() {
Image image = new Image();
image.buildWorkspace = CacheInfo.fromVolume(new VolumeCacheInfo("build-work-vol"));
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
assertThat(request.getBuildWorkspace()).isEqualTo(Cache.volume("build-work-vol"));
}
@Test @Test
void getBuildRequestWhenHasBuildCacheVolumeUsesCache() { void getBuildRequestWhenHasBuildCacheVolumeUsesCache() {
Image image = new Image(); Image image = new Image();
@ -186,6 +194,14 @@ class ImageTests {
assertThat(request.getLaunchCache()).isEqualTo(Cache.volume("launch-cache-vol")); assertThat(request.getLaunchCache()).isEqualTo(Cache.volume("launch-cache-vol"));
} }
@Test
void getBuildRequestWhenHasBuildWorkspaceBindUsesWorkspace() {
Image image = new Image();
image.buildWorkspace = CacheInfo.fromBind(new BindCacheInfo("build-work-dir"));
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
assertThat(request.getBuildWorkspace()).isEqualTo(Cache.bind("build-work-dir"));
}
@Test @Test
void getBuildRequestWhenHasBuildCacheBindUsesCache() { void getBuildRequestWhenHasBuildCacheBindUsesCache() {
Image image = new Image(); Image image = new Image();

Loading…
Cancel
Save