Add bindings option for image building

This commit adds configuration to the Maven and Gradle plugins to
allow a list of volume mount bindings to be provided to the image
building goal and task. This enables service bindings to be mounted
in the builder image that are recognized by buildpacks to support
custom certificates, build tool configuration, APM integration, and
other buildpack features.

Fixes gh-23518
This commit is contained in:
Scott Frederick
2021-02-22 13:10:54 -06:00
parent 8cb24a426d
commit 89555a8745
24 changed files with 546 additions and 66 deletions

View File

@@ -24,6 +24,7 @@ import java.util.List;
import java.util.Map;
import java.util.function.Function;
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;
@@ -65,6 +66,8 @@ public class BuildRequest {
private final List<BuildpackReference> buildpacks;
private final List<Binding> bindings;
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent) {
Assert.notNull(name, "Name must not be null");
Assert.notNull(applicationContent, "ApplicationContent must not be null");
@@ -79,11 +82,13 @@ public class BuildRequest {
this.publish = false;
this.creator = Creator.withVersion("");
this.buildpacks = Collections.emptyList();
this.bindings = Collections.emptyList();
}
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) {
boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List<BuildpackReference> buildpacks,
List<Binding> bindings) {
this.name = name;
this.applicationContent = applicationContent;
this.builder = builder;
@@ -95,6 +100,7 @@ public class BuildRequest {
this.pullPolicy = pullPolicy;
this.publish = publish;
this.buildpacks = buildpacks;
this.bindings = bindings;
}
/**
@@ -106,7 +112,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.buildpacks, this.bindings);
}
/**
@@ -117,7 +123,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.buildpacks, this.bindings);
}
/**
@@ -128,7 +134,7 @@ public class BuildRequest {
public BuildRequest withCreator(Creator creator) {
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.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings);
}
/**
@@ -144,7 +150,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.buildpacks, this.bindings);
}
/**
@@ -158,7 +164,7 @@ 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.publish, this.buildpacks, this.bindings);
}
/**
@@ -168,7 +174,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);
cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings);
}
/**
@@ -178,7 +184,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.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings);
}
/**
@@ -188,7 +194,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.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks, this.bindings);
}
/**
@@ -198,7 +204,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.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks, this.bindings);
}
/**
@@ -219,7 +225,28 @@ public class BuildRequest {
public BuildRequest withBuildpacks(List<BuildpackReference> buildpacks) {
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.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks, this.bindings);
}
/**
* Return a new {@link BuildRequest} with updated bindings.
* @param bindings a collection of bindings to mount to the build container
* @return an updated build request
*/
public BuildRequest withBindings(Binding... bindings) {
Assert.notEmpty(bindings, "Bindings must not be empty");
return withBindings(Arrays.asList(bindings));
}
/**
* Return a new {@link BuildRequest} with updated bindings.
* @param bindings a collection of bindings to mount to the build container
* @return an updated build request
*/
public BuildRequest withBindings(List<Binding> bindings) {
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);
}
/**
@@ -307,12 +334,20 @@ public class BuildRequest {
/**
* Return the collection of buildpacks to use when building the image, if provided.
* @return the collection of buildpacks
* @return the buildpacks
*/
public List<BuildpackReference> getBuildpacks() {
return this.buildpacks;
}
/**
* Return the collection of bindings to mount to the build container.
* @return the bindings
*/
public List<Binding> getBindings() {
return this.bindings;
}
/**
* Factory method to create a new {@link BuildRequest} from a JAR file.
* @param jarFile the source jar file

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* 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.
@@ -22,6 +22,7 @@ import java.util.function.Consumer;
import org.springframework.boot.buildpack.platform.docker.DockerApi;
import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig;
import org.springframework.boot.buildpack.platform.docker.type.ContainerContent;
import org.springframework.boot.buildpack.platform.docker.type.ContainerReference;
@@ -138,10 +139,13 @@ class Lifecycle implements Closeable {
phase.withArgs("-process-type=web");
}
phase.withArgs(this.request.getName());
phase.withBinds(this.layersVolume, Directory.LAYERS);
phase.withBinds(this.applicationVolume, Directory.APPLICATION);
phase.withBinds(this.buildCacheVolume, Directory.CACHE);
phase.withBinds(this.launchCacheVolume, Directory.LAUNCH_CACHE);
phase.withBinding(Binding.from(this.layersVolume, Directory.LAYERS));
phase.withBinding(Binding.from(this.applicationVolume, Directory.APPLICATION));
phase.withBinding(Binding.from(this.buildCacheVolume, Directory.CACHE));
phase.withBinding(Binding.from(this.launchCacheVolume, Directory.LAUNCH_CACHE));
if (this.request.getBindings() != null) {
this.request.getBindings().forEach(phase::withBinding);
}
phase.withEnv(PLATFORM_API_VERSION_KEY, this.platformVersion.toString());
return phase;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* 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.
@@ -22,8 +22,8 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
import org.springframework.util.StringUtils;
/**
@@ -44,7 +44,7 @@ class Phase {
private final List<String> args = new ArrayList<>();
private final Map<VolumeName, String> binds = new LinkedHashMap<>();
private final List<Binding> bindings = new ArrayList<>();
private final Map<String, String> env = new LinkedHashMap<>();
@@ -86,11 +86,10 @@ class Phase {
/**
* Update this phase with an addition volume binding.
* @param source the source volume
* @param dest the destination location
* @param binding the binding
*/
void withBinds(VolumeName source, String dest) {
this.binds.put(source, dest);
void withBinding(Binding binding) {
this.bindings.add(binding);
}
/**
@@ -122,11 +121,11 @@ class Phase {
void apply(ContainerConfig.Update update) {
if (this.daemonAccess) {
update.withUser("root");
update.withBind(DOMAIN_SOCKET_PATH, DOMAIN_SOCKET_PATH);
update.withBinding(Binding.from(DOMAIN_SOCKET_PATH, DOMAIN_SOCKET_PATH));
}
update.withCommand("/cnb/lifecycle/" + this.name, StringUtils.toStringArray(this.args));
update.withLabel("author", "spring-boot");
this.binds.forEach(update::withBind);
this.bindings.forEach(update::withBinding);
this.env.forEach(update::withEnv);
}

View File

@@ -0,0 +1,97 @@
/*
* 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.docker.type;
import java.util.Objects;
import org.springframework.util.Assert;
/**
* Volume bindings to apply when creating a container.
*
* @author Scott Frederick
* @since 2.5.0
*/
public final class Binding {
private final String value;
private Binding(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
/**
* Create a {@link Binding} with the specified value containing a host source,
* container destination, and options.
* @param value the volume binding value
* @return a new {@link Binding} instance
*/
public static Binding of(String value) {
Assert.notNull(value, "Value must not be null");
return new Binding(value);
}
/**
* Create a {@link Binding} from the specified source and destination.
* @param sourceVolume the volume binding host source
* @param destination the volume binding container destination
* @return a new {@link Binding} instance
*/
public static Binding from(VolumeName sourceVolume, String destination) {
Assert.notNull(sourceVolume, "SourceVolume must not be null");
return from(sourceVolume.toString(), destination);
}
/**
* Create a {@link Binding} from the specified source and destination.
* @param source the volume binding host source
* @param destination the volume binding container destination
* @return a new {@link Binding} instance
*/
public static Binding from(String source, String destination) {
Assert.notNull(source, "Source must not be null");
Assert.notNull(destination, "Destination must not be null");
return new Binding(source + ":" + destination);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Binding)) {
return false;
}
Binding binding = (Binding) obj;
return Objects.equals(this.value, binding.value);
}
@Override
public int hashCode() {
return Objects.hash(this.value);
}
@Override
public String toString() {
return this.value;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* 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.
@@ -47,7 +47,7 @@ public class ContainerConfig {
private final String json;
ContainerConfig(String user, ImageReference image, String command, List<String> args, Map<String, String> labels,
Map<String, String> binds, Map<String, String> env) throws IOException {
List<Binding> bindings, Map<String, String> env) throws IOException {
Assert.notNull(image, "Image must not be null");
Assert.hasText(command, "Command must not be empty");
ObjectMapper objectMapper = SharedObjectMapper.get();
@@ -65,7 +65,7 @@ public class ContainerConfig {
labels.forEach(labelsNode::put);
ObjectNode hostConfigNode = node.putObject("HostConfig");
ArrayNode bindsNode = hostConfigNode.putArray("Binds");
binds.forEach((source, dest) -> bindsNode.add(source + ":" + dest));
bindings.forEach((binding) -> bindsNode.add(binding.getValue()));
this.json = objectMapper.writeValueAsString(node);
}
@@ -110,7 +110,7 @@ public class ContainerConfig {
private final Map<String, String> labels = new LinkedHashMap<>();
private final Map<String, String> binds = new LinkedHashMap<>();
private final List<Binding> bindings = new ArrayList<>();
private final Map<String, String> env = new LinkedHashMap<>();
@@ -121,7 +121,7 @@ public class ContainerConfig {
private ContainerConfig run(Consumer<Update> update) {
update.accept(this);
try {
return new ContainerConfig(this.user, this.image, this.command, this.args, this.labels, this.binds,
return new ContainerConfig(this.user, this.image, this.command, this.args, this.labels, this.bindings,
this.env);
}
catch (IOException ex) {
@@ -166,21 +166,11 @@ public class ContainerConfig {
}
/**
* Update the container config with an additional bind.
* @param sourceVolume the source volume
* @param dest the bind destination
* Update the container config with an additional binding.
* @param binding the binding
*/
public void withBind(VolumeName sourceVolume, String dest) {
this.binds.put(sourceVolume.toString(), dest);
}
/**
* Update the container config with an additional bind.
* @param source the bind source
* @param dest the bind destination
*/
public void withBind(String source, String dest) {
this.binds.put(source, dest);
public void withBinding(Binding binding) {
this.bindings.add(binding);
}
/**

View File

@@ -31,6 +31,7 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
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;
@@ -181,6 +182,23 @@ public class BuildRequestTests {
.withMessage("Buildpacks must not be null");
}
@Test
void withBindingsAddsBindings() throws IOException {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
BuildRequest withBindings = request.withBindings(Binding.of("/host/path:/container/path:ro"),
Binding.of("volume-name:/container/path:rw"));
assertThat(request.getBindings()).isEmpty();
assertThat(withBindings.getBindings()).containsExactly(Binding.of("/host/path:/container/path:ro"),
Binding.of("volume-name:/container/path:rw"));
}
@Test
void withBindingsWhenBindingsIsNullThrowsException() throws IOException {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
assertThatIllegalArgumentException().isThrownBy(() -> request.withBindings((List<Binding>) null))
.withMessage("Bindings must not be null");
}
private void hasExpectedJarContent(TarArchive archive) {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* 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.
@@ -36,6 +36,7 @@ import org.springframework.boot.buildpack.platform.docker.DockerApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi;
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.Binding;
import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig;
import org.springframework.boot.buildpack.platform.docker.type.ContainerContent;
import org.springframework.boot.buildpack.platform.docker.type.ContainerReference;
@@ -88,6 +89,18 @@ class LifecycleTests {
assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
}
@Test
void executeWithBindingsExecutesPhases() 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().withBindings(Binding.of("/host/src/path:/container/dest/path:ro"),
Binding.of("volume-name:/container/volume/path:rw"));
createLifecycle(request).execute();
assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-bindings.json"));
assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
}
@Test
void executeExecutesPhasesWithPlatformApi03() throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* 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.
@@ -18,6 +18,7 @@ package org.springframework.boot.buildpack.platform.build;
import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig.Update;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
@@ -65,7 +66,7 @@ class PhaseTests {
Update update = mock(Update.class);
phase.apply(update);
verify(update).withUser("root");
verify(update).withBind("/var/run/docker.sock", "/var/run/docker.sock");
verify(update).withBinding(Binding.from("/var/run/docker.sock", "/var/run/docker.sock"));
verify(update).withCommand("/cnb/lifecycle/test", NO_ARGS);
verify(update).withLabel("author", "spring-boot");
verifyNoMoreInteractions(update);
@@ -108,12 +109,12 @@ class PhaseTests {
void applyWhenWithBindsUpdatesConfigurationWithBinds() {
Phase phase = new Phase("test", false);
VolumeName volumeName = VolumeName.of("test");
phase.withBinds(volumeName, "/test");
phase.withBinding(Binding.from(volumeName, "/test"));
Update update = mock(Update.class);
phase.apply(update);
verify(update).withCommand("/cnb/lifecycle/test");
verify(update).withLabel("author", "spring-boot");
verify(update).withBind(volumeName, "/test");
verify(update).withBinding(Binding.from(volumeName, "/test"));
verifyNoMoreInteractions(update);
}

View File

@@ -0,0 +1,73 @@
/*
* 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.docker.type;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link Binding}.
*
* @author Scott Frederick
*/
class BindingTests {
@Test
void ofReturnsValue() {
Binding binding = Binding.of("host-src:container-dest:ro");
assertThat(binding.getValue()).isEqualTo("host-src:container-dest:ro");
}
@Test
void ofWithNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> Binding.of(null))
.withMessageContaining("Value must not be null");
}
@Test
void fromReturnsValue() {
Binding binding = Binding.from("host-src", "container-dest");
assertThat(binding.getValue()).isEqualTo("host-src:container-dest");
}
@Test
void fromWithNullSourceThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> Binding.from((String) null, "container-dest"))
.withMessageContaining("Source must not be null");
}
@Test
void fromWithNullDestinationThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> Binding.from("host-src", null))
.withMessageContaining("Destination must not be null");
}
@Test
void fromVolumeNameSourceReturnsValue() {
Binding binding = Binding.from(VolumeName.of("host-src"), "container-dest");
assertThat(binding.getValue()).isEqualTo("host-src:container-dest");
}
@Test
void fromVolumeNameSourceWithNullSourceThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> Binding.from((VolumeName) null, "container-dest"))
.withMessageContaining("SourceVolume must not be null");
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* 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.
@@ -56,7 +56,7 @@ class ContainerConfigTests extends AbstractJsonTests {
update.withCommand("ls", "-l");
update.withArgs("-h");
update.withLabel("spring", "boot");
update.withBind("bind-source", "bind-dest");
update.withBinding(Binding.from("bind-source", "bind-dest"));
update.withEnv("name1", "value1");
update.withEnv("name2", "value2");
});

View File

@@ -0,0 +1,12 @@
{
"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", "pack-cache-b35197ac41ea.build:/cache", "pack-cache-b35197ac41ea.launch:/launch-cache", "/host/src/path:/container/dest/path:ro", "volume-name:/container/volume/path:rw" ]
}
}

View File

@@ -136,12 +136,28 @@ a|Buildpacks that the builder should use when building the image.
Only the specified buildpacks will be used, overriding the default buildpacks included in the builder.
Buildpack references must be in one of the following forms:
* Buildpack in the builder - [urn:cnb:builder:]<buildpack id>[@<version>]
* Buildpack in a directory on the file system - [file://]<path>
* Buildpack in a gzipped tar (.tgz) file on the file system - [file://]<path>/<file name>
* Buildpack in an OCI image - [docker://]<host>/<repo>[:<tag>][@<digest>]
* Buildpack in the builder - `[urn:cnb:builder:]<buildpack ID>[@<version>]`
* Buildpack in a directory on the file system - `[file://]<path>`
* Buildpack in a gzipped tar (.tgz) file on the file system - `[file://]<path>/<file name>`
* Buildpack in an OCI image - `[docker://]<host>/<repo>[:<tag>][@<digest>]`
| None, indicating the builder should use the buildpacks included in it.
| `bindings`
|
a|https://docs.docker.com/storage/bind-mounts/[Volume bind mounts] that should be mounted to the builder container when building the image.
The bindings will be passed unparsed and unvalidated to Docker when creating the builder container.
Bindings must be in one of the following forms:
* `<host source path>:<container destination path>[:<options>]`
* `<host volume name>:<container destination path>[:<options>]`
Where `<options>` can contain:
* `ro` to mount the volume as read-only in the container
* `rw` to mount the volume as readable and writable in the container
* `volume-opt=key=value` to specify key-value pairs consisting of an option name and its value
|
| `cleanCache`
| `--cleanCache`
| Whether to clean the cache before building.
@@ -295,7 +311,7 @@ A buildpack located in a CNB Builder (version may be omitted if there is only on
A path to a directory containing buildpack content (not supported on Windows):
* `\file:///path/to/buildpack/`
* `/path/to/buildpack/`BootBuildImageIntegrationTests
* `/path/to/buildpack/`
A path to a gzipped tar file containing buildpack content:

View File

@@ -45,6 +45,7 @@ import org.springframework.boot.buildpack.platform.build.BuildpackReference;
import org.springframework.boot.buildpack.platform.build.Creator;
import org.springframework.boot.buildpack.platform.build.PullPolicy;
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.ImageName;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.io.ZipFileTarArchive;
@@ -87,9 +88,11 @@ public class BootBuildImage extends DefaultTask {
private boolean publish;
private ListProperty<String> buildpacks;
private final ListProperty<String> buildpacks;
private DockerSpec docker = new DockerSpec();
private final ListProperty<String> bindings;
private final DockerSpec docker = new DockerSpec();
public BootBuildImage() {
this.jar = getProject().getObjects().fileProperty();
@@ -99,6 +102,7 @@ public class BootBuildImage extends DefaultTask {
Project project = getProject();
this.projectVersion.set(getProject().provider(() -> project.getVersion().toString()));
this.buildpacks = getProject().getObjects().listProperty(String.class);
this.bindings = getProject().getObjects().listProperty(String.class);
}
/**
@@ -292,7 +296,7 @@ public class BootBuildImage extends DefaultTask {
/**
* Returns the buildpacks that will be used when building the image.
* @return the buildpacks
* @return the buildpack references
*/
@Input
@Optional
@@ -302,7 +306,7 @@ public class BootBuildImage extends DefaultTask {
/**
* Sets the buildpacks that will be used when building the image.
* @param buildpacks the buildpacks
* @param buildpacks the buildpack references
*/
public void setBuildpacks(List<String> buildpacks) {
this.buildpacks.set(buildpacks);
@@ -317,13 +321,51 @@ public class BootBuildImage extends DefaultTask {
}
/**
* Adds entries to the environment that will be used when building the image.
* Adds entries to the buildpacks that will be used when building the image.
* @param buildpacks the buildpack references
*/
public void buildpacks(List<String> buildpacks) {
this.buildpacks.addAll(buildpacks);
}
/**
* Returns the volume bindings that will be mounted to the container when building the
* image.
* @return the bindings
*/
@Input
@Optional
public List<String> getBindings() {
return this.bindings.getOrNull();
}
/**
* Sets the volume bindings that will be mounted to the container when building the
* image.
* @param bindings the bindings
*/
public void setBindings(List<String> bindings) {
this.bindings.set(bindings);
}
/**
* Add an entry to the volume bindings that will be mounted to the container when
* building the image.
* @param binding the binding
*/
public void binding(String binding) {
this.bindings.add(binding);
}
/**
* Add entries to the volume bindings that will be mounted to the container when
* building the image.
* @param bindings the bindings
*/
public void bindings(List<String> bindings) {
this.bindings.addAll(bindings);
}
/**
* Returns the Docker configuration the builder will use.
* @return docker configuration.
@@ -388,6 +430,7 @@ public class BootBuildImage extends DefaultTask {
request = customizePullPolicy(request);
request = customizePublish(request);
request = customizeBuildpacks(request);
request = customizeBindings(request);
return request;
}
@@ -448,6 +491,14 @@ public class BootBuildImage extends DefaultTask {
return request;
}
private BuildRequest customizeBindings(BuildRequest request) {
List<String> bindings = this.bindings.getOrNull();
if (bindings != null && !bindings.isEmpty()) {
return request.withBindings(bindings.stream().map(Binding::of).collect(Collectors.toList()));
}
return request;
}
private String translateTargetJavaVersion() {
return this.targetJavaVersion.get().getMajorVersion() + ".*";
}

View File

@@ -219,6 +219,17 @@ class BootBuildImageIntegrationTests {
}
}
@TestTemplate
void failsWithBindingContainingInvalidCertificate() throws IOException {
writeMainClass();
writeLongNameResource();
writeCertificateBindingFiles();
BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT");
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.FAILED);
assertThat(result.getOutput()).contains("failed to decode certificate")
.contains("/platform/bindings/certificates/test.crt");
}
@TestTemplate
void failsWithLaunchScript() throws IOException {
writeMainClass();
@@ -380,4 +391,17 @@ class BootBuildImageIntegrationTests {
tar.closeArchiveEntry();
}
private void writeCertificateBindingFiles() throws IOException {
File bindingDir = new File(this.gradleBuild.getProjectDir(), "bindings/ca-certificates");
bindingDir.mkdirs();
File type = new File(bindingDir, "type");
try (PrintWriter writer = new PrintWriter(new FileWriter(type))) {
writer.print("ca-certificates");
}
File cert = new File(bindingDir, "test.crt");
try (PrintWriter writer = new PrintWriter(new FileWriter(cert))) {
writer.println("not a valid certificate");
}
}
}

View File

@@ -31,6 +31,7 @@ import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.buildpack.platform.build.BuildRequest;
import org.springframework.boot.buildpack.platform.build.BuildpackReference;
import org.springframework.boot.buildpack.platform.build.PullPolicy;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -250,4 +251,31 @@ class BootBuildImageTests {
BuildpackReference.of("example/buildpack1"), BuildpackReference.of("example/buildpack2"));
}
@Test
void whenNoBindingsAreConfiguredThenRequestHasNoBindings() {
assertThat(this.buildImage.createRequest().getBindings()).isEmpty();
}
@Test
void whenBindingsAreConfiguredThenRequestHasBindings() {
this.buildImage.setBindings(Arrays.asList("host-src:container-dest:ro", "volume-name:container-dest:rw"));
assertThat(this.buildImage.createRequest().getBindings())
.containsExactly(Binding.of("host-src:container-dest:ro"), Binding.of("volume-name:container-dest:rw"));
}
@Test
void whenEntriesAreAddedToBindingsThenRequestHasBindings() {
this.buildImage.bindings(Arrays.asList("host-src:container-dest:ro", "volume-name:container-dest:rw"));
assertThat(this.buildImage.createRequest().getBindings())
.containsExactly(Binding.of("host-src:container-dest:ro"), Binding.of("volume-name:container-dest:rw"));
}
@Test
void whenIndividualEntriesAreAddedToBindingsThenRequestHasBindings() {
this.buildImage.binding("host-src:container-dest:ro");
this.buildImage.binding("volume-name:container-dest:rw");
assertThat(this.buildImage.createRequest().getBindings())
.containsExactly(Binding.of("host-src:container-dest:ro"), Binding.of("volume-name:container-dest:rw"));
}
}

View File

@@ -0,0 +1,11 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
bootBuildImage {
bindings = [ "${projectDir}/bindings/ca-certificates:/platform/bindings/certificates" ]
}

View File

@@ -157,13 +157,29 @@ a|Buildpacks that the builder should use when building the image.
Only the specified buildpacks will be used, overriding the default buildpacks included in the builder.
Buildpack references must be in one of the following forms:
* Buildpack in the builder - [urn:cnb:builder:]<buildpack id>[@<version>]
* Buildpack in a directory on the file system - [file://]<path>
* Buildpack in a gzipped tar (.tgz) file on the file system - [file://]<path>/<file name>
* Buildpack in an OCI image - [docker://]<host>/<repo>[:<tag>][@<digest>]
* Buildpack in the builder - `[urn:cnb:builder:]<buildpack ID>[@<version>]`
* Buildpack in a directory on the file system - `[file://]<path>`
* Buildpack in a gzipped tar (.tgz) file on the file system - `[file://]<path>/<file name>`
* Buildpack in an OCI image - `[docker://]<host>/<repo>[:<tag>][@<digest>]`
|
| None, indicating the builder should use the buildpacks included in it.
| `bindings`
a|https://docs.docker.com/storage/bind-mounts/[Volume bind mounts] that should be mounted to the builder container when building the image.
The bindings will be passed unparsed and unvalidated to Docker when creating the builder container.
Bindings must be in one of the following forms:
* `<host source path>:<container destination path>[:<options>]`
* `<host volume name>:<container destination path>[:<options>]`
Where `<options>` can contain:
* `ro` to mount the volume as read-only in the container
* `rw` to mount the volume as readable and writable in the container
* `volume-opt=key=value` to specify key-value pairs consisting of an option name and its value
|
|
| `cleanCache`
| Whether to clean the cache before building.
| `spring-boot.build-image.cleanCache`

View File

@@ -168,6 +168,15 @@ public class BuildImageTests extends AbstractArchiveIntegrationTests {
});
}
@TestTemplate
void failsWithBindingContainingInvalidCertificate(MavenBuild mavenBuild) {
mavenBuild.project("build-image-bindings").goals("package")
.systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT")
.executeAndFail((project) -> assertThat(buildLog(project)).contains("Building image")
.contains("failed to decode certificate")
.contains("/platform/bindings/ca-certificates/test.crt"));
}
@TestTemplate
void failsWhenPublishWithoutPublishRegistryConfigured(MavenBuild mavenBuild) {
mavenBuild.project("build-image").goals("package").systemProperty("spring-boot.build-image.publish", "true")

View File

@@ -0,0 +1,36 @@
<?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-bindings</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>
<bindings>
<binding>${basedir}/bindings/ca-certificates:/platform/bindings/ca-certificates</binding>
</bindings>
</image>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

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

View File

@@ -26,6 +26,7 @@ import org.apache.maven.artifact.Artifact;
import org.springframework.boot.buildpack.platform.build.BuildRequest;
import org.springframework.boot.buildpack.platform.build.BuildpackReference;
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.ImageName;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.io.Owner;
@@ -59,6 +60,8 @@ public class Image {
List<String> buildpacks;
List<String> bindings;
/**
* The name of the created image.
* @return the image name
@@ -183,6 +186,9 @@ public class Image {
request = request
.withBuildpacks(this.buildpacks.stream().map(BuildpackReference::of).collect(Collectors.toList()));
}
if (this.bindings != null && !this.bindings.isEmpty()) {
request = request.withBindings(this.bindings.stream().map(Binding::of).collect(Collectors.toList()));
}
return request;
}

View File

@@ -29,6 +29,7 @@ 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.PullPolicy;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.io.Owner;
import org.springframework.boot.buildpack.platform.io.TarArchive;
@@ -68,6 +69,7 @@ class ImageTests {
assertThat(request.isVerboseLogging()).isFalse();
assertThat(request.getPullPolicy()).isEqualTo(PullPolicy.ALWAYS);
assertThat(request.getBuildpacks()).isEmpty();
assertThat(request.getBindings()).isEmpty();
}
@Test
@@ -135,6 +137,15 @@ class ImageTests {
BuildpackReference.of("example/buildpack2@0.0.2"));
}
@Test
void getBuildRequestWhenHasBindingsUsesBindings() {
Image image = new Image();
image.bindings = Arrays.asList("host-src:container-dest:ro", "volume-name:container-dest:rw");
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
assertThat(request.getBindings()).containsExactly(Binding.of("host-src:container-dest:ro"),
Binding.of("volume-name:container-dest:rw"));
}
private Artifact createArtifact() {
return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile",
"jar", null, new DefaultArtifactHandler());