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