Add network option for image building

This commit adds configuration to the Maven and Gradle plugins to
allow specifying the network mode to be provided to the image
building goal and task.

See gh-27486
This commit is contained in:
Jeroen Meijer 2021-07-24 15:47:01 +02:00 committed by Scott Frederick
parent e737388f5c
commit 8e6d03b221
17 changed files with 195 additions and 15 deletions

View File

@ -36,6 +36,7 @@ import org.springframework.util.Assert;
* @author Phillip Webb
* @author Scott Frederick
* @author Andrey Shlykov
* @author Jeroen Meijer
* @since 2.3.0
*/
public class BuildRequest {
@ -68,6 +69,8 @@ public class BuildRequest {
private final List<Binding> bindings;
private final String network;
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent) {
Assert.notNull(name, "Name must not be null");
Assert.notNull(applicationContent, "ApplicationContent must not be null");
@ -83,12 +86,13 @@ public class BuildRequest {
this.creator = Creator.withVersion("");
this.buildpacks = Collections.emptyList();
this.bindings = Collections.emptyList();
this.network = 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) {
List<Binding> bindings, String network) {
this.name = name;
this.applicationContent = applicationContent;
this.builder = builder;
@ -101,6 +105,7 @@ public class BuildRequest {
this.publish = publish;
this.buildpacks = buildpacks;
this.bindings = bindings;
this.network = network;
}
/**
@ -112,7 +117,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.buildpacks, this.bindings, this.network);
}
/**
@ -123,7 +128,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.buildpacks, this.bindings, this.network);
}
/**
@ -134,7 +139,8 @@ 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.bindings);
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network);
}
/**
@ -150,7 +156,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.buildpacks, this.bindings, this.network);
}
/**
@ -164,7 +170,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.bindings);
this.publish, this.buildpacks, this.bindings, this.network);
}
/**
@ -174,7 +180,8 @@ 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);
cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network);
}
/**
@ -184,7 +191,8 @@ 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.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network);
}
/**
@ -194,7 +202,8 @@ 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.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network);
}
/**
@ -204,7 +213,8 @@ 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.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks, this.bindings,
this.network);
}
/**
@ -227,7 +237,8 @@ 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.bindings);
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks, this.bindings,
this.network);
}
/**
@ -250,7 +261,14 @@ public class BuildRequest {
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);
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, bindings,
this.network);
}
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);
}
/**
@ -353,6 +371,10 @@ public class BuildRequest {
return this.bindings;
}
public String getNetwork() {
return this.network;
}
/**
* Factory method to create a new {@link BuildRequest} from a JAR file.
* @param jarFile the source jar file

View File

@ -38,6 +38,7 @@ import org.springframework.util.Assert;
*
* @author Phillip Webb
* @author Scott Frederick
* @author Jeroen Meijer
*/
class Lifecycle implements Closeable {
@ -147,6 +148,7 @@ class Lifecycle implements Closeable {
this.request.getBindings().forEach(phase::withBinding);
}
phase.withEnv(PLATFORM_API_VERSION_KEY, this.platformVersion.toString());
phase.withNetworkMode(this.request.getNetwork());
return phase;
}

View File

@ -31,6 +31,7 @@ import org.springframework.util.StringUtils;
*
* @author Phillip Webb
* @author Scott Frederick
* @author Jeroen Meijer
*/
class Phase {
@ -48,6 +49,8 @@ class Phase {
private final Map<String, String> env = new LinkedHashMap<>();
private String networkMode;
/**
* Create a new {@link Phase} instance.
* @param name the name of the phase
@ -101,6 +104,10 @@ class Phase {
this.env.put(name, value);
}
void withNetworkMode(String networkMode) {
this.networkMode = networkMode;
}
/**
* Return the name of the phase.
* @return the phase name
@ -127,6 +134,7 @@ class Phase {
update.withLabel("author", "spring-boot");
this.bindings.forEach(update::withBinding);
this.env.forEach(update::withEnv);
update.withNetworkMode(this.networkMode);
}
}

View File

@ -40,6 +40,7 @@ import org.springframework.util.StringUtils;
*
* @author Phillip Webb
* @author Scott Frederick
* @author Jeroen Meijer
* @since 2.3.0
*/
public class ContainerConfig {
@ -47,7 +48,7 @@ public class ContainerConfig {
private final String json;
ContainerConfig(String user, ImageReference image, String command, List<String> args, Map<String, String> labels,
List<Binding> bindings, Map<String, String> env) throws IOException {
List<Binding> bindings, Map<String, String> env, String networkMode) throws IOException {
Assert.notNull(image, "Image must not be null");
Assert.hasText(command, "Command must not be empty");
ObjectMapper objectMapper = SharedObjectMapper.get();
@ -64,6 +65,9 @@ public class ContainerConfig {
ObjectNode labelsNode = node.putObject("Labels");
labels.forEach(labelsNode::put);
ObjectNode hostConfigNode = node.putObject("HostConfig");
if (networkMode != null) {
hostConfigNode.put("NetworkMode", networkMode);
}
ArrayNode bindsNode = hostConfigNode.putArray("Binds");
bindings.forEach((binding) -> bindsNode.add(binding.toString()));
this.json = objectMapper.writeValueAsString(node);
@ -114,6 +118,8 @@ public class ContainerConfig {
private final Map<String, String> env = new LinkedHashMap<>();
private String networkMode;
Update(ImageReference image) {
this.image = image;
}
@ -122,7 +128,7 @@ public class ContainerConfig {
update.accept(this);
try {
return new ContainerConfig(this.user, this.image, this.command, this.args, this.labels, this.bindings,
this.env);
this.env, this.networkMode);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
@ -182,6 +188,10 @@ public class ContainerConfig {
this.env.put(name, value);
}
public void withNetworkMode(String networkMode) {
this.networkMode = networkMode;
}
}
}

View File

@ -45,6 +45,7 @@ import static org.assertj.core.api.Assertions.entry;
*
* @author Phillip Webb
* @author Scott Frederick
* @author Jeroen Meijer
*/
public class BuildRequestTests {
@ -199,6 +200,12 @@ public class BuildRequestTests {
.withMessage("Bindings must not be null");
}
@Test
void withNetworkUpdatesNetwork() throws IOException {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")).withNetwork("test");
assertThat(request.getNetwork()).isEqualTo("test");
}
private void hasExpectedJarContent(TarArchive archive) {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

View File

@ -62,6 +62,7 @@ import static org.mockito.Mockito.verify;
*
* @author Phillip Webb
* @author Scott Frederick
* @author Jeroen Meijer
*/
class LifecycleTests {
@ -188,6 +189,17 @@ class LifecycleTests {
verify(this.docker.volume()).delete(VolumeName.of("pack-app-aaaaaaaaaa"), true);
}
@Test
void executeWithNetworkExecutesPhases() 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().withNetwork("test");
createLifecycle(request).execute();
assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-network.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

@ -32,6 +32,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
*
* @author Phillip Webb
* @author Scott Frederick
* @author Jeroen Meijer
*/
class PhaseTests {
@ -56,6 +57,7 @@ class PhaseTests {
phase.apply(update);
verify(update).withCommand("/cnb/lifecycle/test", NO_ARGS);
verify(update).withLabel("author", "spring-boot");
verify(update).withNetworkMode(null);
verifyNoMoreInteractions(update);
}
@ -69,6 +71,7 @@ class PhaseTests {
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");
verify(update).withNetworkMode(null);
verifyNoMoreInteractions(update);
}
@ -80,6 +83,7 @@ class PhaseTests {
phase.apply(update);
verify(update).withCommand("/cnb/lifecycle/test", "-log-level", "debug");
verify(update).withLabel("author", "spring-boot");
verify(update).withNetworkMode(null);
verifyNoMoreInteractions(update);
}
@ -91,6 +95,7 @@ class PhaseTests {
phase.apply(update);
verify(update).withCommand("/cnb/lifecycle/test");
verify(update).withLabel("author", "spring-boot");
verify(update).withNetworkMode(null);
verifyNoMoreInteractions(update);
}
@ -102,6 +107,7 @@ class PhaseTests {
phase.apply(update);
verify(update).withCommand("/cnb/lifecycle/test", "a", "b", "c");
verify(update).withLabel("author", "spring-boot");
verify(update).withNetworkMode(null);
verifyNoMoreInteractions(update);
}
@ -115,6 +121,7 @@ class PhaseTests {
verify(update).withCommand("/cnb/lifecycle/test");
verify(update).withLabel("author", "spring-boot");
verify(update).withBinding(Binding.from(volumeName, "/test"));
verify(update).withNetworkMode(null);
verifyNoMoreInteractions(update);
}
@ -129,6 +136,19 @@ class PhaseTests {
verify(update).withLabel("author", "spring-boot");
verify(update).withEnv("name1", "value1");
verify(update).withEnv("name2", "value2");
verify(update).withNetworkMode(null);
verifyNoMoreInteractions(update);
}
@Test
void applyWhenWithNetworkModeUpdatesConfigurationWithNetworkMode() {
Phase phase = new Phase("test", true);
phase.withNetworkMode("test");
Update update = mock(Update.class);
phase.apply(update);
verify(update).withCommand("/cnb/lifecycle/test");
verify(update).withNetworkMode("test");
verify(update).withLabel("author", "spring-boot");
verifyNoMoreInteractions(update);
}

View File

@ -32,6 +32,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*
* @author Phillip Webb
* @author Scott Frederick
* @author Jeroen Meijer
*/
class ContainerConfigTests extends AbstractJsonTests {
@ -59,6 +60,7 @@ class ContainerConfigTests extends AbstractJsonTests {
update.withBinding(Binding.from("bind-source", "bind-dest"));
update.withEnv("name1", "value1");
update.withEnv("name2", "value2");
update.withNetworkMode("test");
});
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
containerConfig.writeTo(outputStream);

View File

@ -0,0 +1,13 @@
{
"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" : {
"NetworkMode" : "test",
"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" ]
}
}

View File

@ -16,6 +16,7 @@
"HostConfig": {
"Binds": [
"bind-source:bind-dest"
]
],
"NetworkMode": "test"
}
}

View File

@ -169,6 +169,14 @@ Where `<options>` can contain:
| `--publishImage`
| Whether to publish the generated image to a Docker registry.
| `false`
| `network`
| `--network`
| The network the build container will connect to. The value supplied for this option will be passed
unvalidated as `HostConfig.NetworkMode` to the configuration which creates the build container,
see https://docs.docker.com/engine/api/v1.41/#operation/ContainerCreate[Docker's engine API].
Using this option is similar to running `docker build --network ...`.
|
|===
NOTE: The plugin detects the target Java compatibility of the project using the JavaPlugin's `targetCompatibility` property.

View File

@ -58,6 +58,7 @@ import org.springframework.util.StringUtils;
*
* @author Andy Wilkinson
* @author Scott Frederick
* @author Jeroen Meijer
* @since 2.3.0
*/
public class BootBuildImage extends DefaultTask {
@ -92,6 +93,8 @@ public class BootBuildImage extends DefaultTask {
private final ListProperty<String> bindings;
private String network;
private final DockerSpec docker = new DockerSpec();
public BootBuildImage() {
@ -376,6 +379,25 @@ public class BootBuildImage extends DefaultTask {
this.bindings.addAll(bindings);
}
/**
* Returns the network the build container will connect to.
* @return the network
*/
@Input
@Optional
public String getNetwork() {
return this.network;
}
/**
* Sets the network the build container will connect to.
* @param network the network
*/
@Option(option = "network", description = "Connect detect and build containers to network")
public void setNetwork(String network) {
this.network = network;
}
/**
* Returns the Docker configuration the builder will use.
* @return docker configuration.
@ -438,6 +460,7 @@ public class BootBuildImage extends DefaultTask {
request = customizePublish(request);
request = customizeBuildpacks(request);
request = customizeBindings(request);
request = request.withNetwork(this.network);
return request;
}

View File

@ -42,6 +42,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
* @author Andy Wilkinson
* @author Scott Frederick
* @author Andrey Shlykov
* @author Jeroen Meijer
*/
class BootBuildImageTests {
@ -278,4 +279,10 @@ class BootBuildImageTests {
.containsExactly(Binding.of("host-src:container-dest:ro"), Binding.of("volume-name:container-dest:rw"));
}
@Test
void whenNetworkIsConfiguredThenRequestHasNetwork() {
this.buildImage.setNetwork("test");
assertThat(this.buildImage.createRequest().getNetwork()).isEqualTo("test");
}
}

View File

@ -176,6 +176,14 @@ Where `<options>` can contain:
(`spring-boot.build-image.publish`)
| Whether to publish the generated image to a Docker registry.
| `false`
| `network` +
(`spring-boot.build-image.network`)
| The network the build container will connect to. The value supplied for this option will be passed
unvalidated as `HostConfig.NetworkMode` to the configuration which creates the build container,
see https://docs.docker.com/engine/api/v1.41/#operation/ContainerCreate[Docker's engine API].
Using this option is similar to running `docker build --network ...`.
|
|===
NOTE: The plugin detects the target Java compatibility of the project using the compiler's plugin configuration or the `maven.compiler.target` property.

View File

@ -59,6 +59,7 @@ import org.springframework.util.StringUtils;
*
* @author Phillip Webb
* @author Scott Frederick
* @author Jeroen Meijer
* @since 2.3.0
*/
@Mojo(name = "build-image", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true,
@ -153,6 +154,13 @@ public class BuildImageMojo extends AbstractPackagerMojo {
@Parameter(property = "spring-boot.build-image.publish", readonly = true)
Boolean publish;
/**
* Alias for {@link Image#network} to support configuration via command-line property.
* @since 2.6.0
*/
@Parameter(property = "spring-boot.build-image.network", readonly = true)
String network;
/**
* Docker configuration options.
* @since 2.4.0
@ -248,6 +256,9 @@ public class BuildImageMojo extends AbstractPackagerMojo {
if (image.publish == null && this.publish != null) {
image.setPublish(this.publish);
}
if (image.network == null && this.network != null) {
image.setNetwork(this.network);
}
if (image.publish != null && image.publish && publishRegistryNotConfigured()) {
throw new MojoExecutionException("Publishing an image requires docker.publishRegistry to be configured");
}

View File

@ -39,6 +39,7 @@ import org.springframework.util.StringUtils;
*
* @author Phillip Webb
* @author Scott Frederick
* @author Jeroen Meijer
* @since 2.3.0
*/
public class Image {
@ -63,6 +64,8 @@ public class Image {
List<String> bindings;
String network;
/**
* The name of the created image.
* @return the image name
@ -151,6 +154,18 @@ public class Image {
this.publish = publish;
}
/**
* Returns the network the build container will connect to.
* @return the network
*/
public String getNetwork() {
return this.network;
}
public void setNetwork(String network) {
this.network = network;
}
BuildRequest getBuildRequest(Artifact artifact, Function<Owner, TarArchive> applicationContent) {
return customize(BuildRequest.of(getOrDeduceName(artifact), applicationContent));
}
@ -190,6 +205,7 @@ public class Image {
if (!CollectionUtils.isEmpty(this.bindings)) {
request = request.withBindings(this.bindings.stream().map(Binding::of).collect(Collectors.toList()));
}
request = request.withNetwork(this.network);
return request;
}

View File

@ -41,6 +41,7 @@ import static org.assertj.core.api.Assertions.entry;
*
* @author Phillip Webb
* @author Scott Frederick
* @author Jeroen Meijer
*/
class ImageTests {
@ -70,6 +71,7 @@ class ImageTests {
assertThat(request.getPullPolicy()).isEqualTo(PullPolicy.ALWAYS);
assertThat(request.getBuildpacks()).isEmpty();
assertThat(request.getBindings()).isEmpty();
assertThat(request.getNetwork()).isNull();
}
@Test
@ -146,6 +148,14 @@ class ImageTests {
Binding.of("volume-name:container-dest:rw"));
}
@Test
void getBuildRequestWhenNetworkUsesNetwork() {
Image image = new Image();
image.network = "test";
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
assertThat(request.getNetwork()).isEqualTo("test");
}
private Artifact createArtifact() {
return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile",
"jar", null, new DefaultArtifactHandler());