diff --git a/src/main/java/com/github/dockerjava/api/command/StartContainerCmd.java b/src/main/java/com/github/dockerjava/api/command/StartContainerCmd.java index ed16a727c..8535c8ed2 100644 --- a/src/main/java/com/github/dockerjava/api/command/StartContainerCmd.java +++ b/src/main/java/com/github/dockerjava/api/command/StartContainerCmd.java @@ -6,6 +6,7 @@ import com.github.dockerjava.api.model.Device; import com.github.dockerjava.api.model.Link; import com.github.dockerjava.api.model.LxcConf; +import com.github.dockerjava.api.model.PortBinding; import com.github.dockerjava.api.model.Ports; import com.github.dockerjava.api.model.RestartPolicy; @@ -53,8 +54,21 @@ public interface StartContainerCmd extends DockerCmd { public StartContainerCmd withLxcConf(LxcConf... lxcConf); + /** + * Add the port bindings that are contained in the given {@link Ports} + * object. + * + * @see #withPortBindings(PortBinding...) + */ public StartContainerCmd withPortBindings(Ports portBindings); + /** + * Add one or more {@link PortBinding}s. + * This corresponds to the --publish (-p) + * option of the docker run CLI command. + */ + public StartContainerCmd withPortBindings(PortBinding... portBindings); + public StartContainerCmd withPrivileged(boolean privileged); public StartContainerCmd withPublishAllPorts(boolean publishAllPorts); diff --git a/src/main/java/com/github/dockerjava/api/model/ExposedPort.java b/src/main/java/com/github/dockerjava/api/model/ExposedPort.java index d19bfbf05..30f7ce384 100644 --- a/src/main/java/com/github/dockerjava/api/model/ExposedPort.java +++ b/src/main/java/com/github/dockerjava/api/model/ExposedPort.java @@ -48,6 +48,16 @@ public ExposedPort(int port, InternetProtocol protocol) { this.protocol = protocol; } + /** + * Creates an {@link ExposedPort} for the given + * {@link #getPort() port number} and {@link InternetProtocol#DEFAULT}. + * + * @param port the {@link #getPort() port number} + */ + public ExposedPort(int port) { + this(port, InternetProtocol.DEFAULT); + } + /** * Creates an {@link ExposedPort} for the given parameters. * @@ -61,7 +71,8 @@ public ExposedPort(String scheme, int port) { this(port, InternetProtocol.valueOf(scheme)); } - /** @return the {@link InternetProtocol} */ + /** @return the {@link InternetProtocol} of the {@link #getPort() port} + * that the container exposes */ public InternetProtocol getProtocol() { return protocol; } @@ -75,7 +86,7 @@ public String getScheme() { return protocol.toString(); } - /** @return the port number */ + /** @return the port number that the container exposes */ public int getPort() { return port; } @@ -107,7 +118,14 @@ public static ExposedPort udp(int port) { public static ExposedPort parse(String serialized) throws IllegalArgumentException { try { String[] parts = serialized.split("/"); - return new ExposedPort(Integer.valueOf(parts[0]), InternetProtocol.parse(parts[1])); + switch (parts.length) { + case 1: + return new ExposedPort(Integer.valueOf(parts[0])); + case 2: + return new ExposedPort(Integer.valueOf(parts[0]), InternetProtocol.parse(parts[1])); + default: + throw new IllegalArgumentException(); + } } catch (Exception e) { throw new IllegalArgumentException("Error parsing ExposedPort '" + serialized + "'"); } diff --git a/src/main/java/com/github/dockerjava/api/model/PortBinding.java b/src/main/java/com/github/dockerjava/api/model/PortBinding.java new file mode 100644 index 000000000..df9085597 --- /dev/null +++ b/src/main/java/com/github/dockerjava/api/model/PortBinding.java @@ -0,0 +1,83 @@ +package com.github.dockerjava.api.model; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; + +import com.github.dockerjava.api.command.InspectContainerResponse.HostConfig; +import com.github.dockerjava.api.command.InspectContainerResponse.NetworkSettings; +import com.github.dockerjava.api.model.Ports.Binding; + +/** + * In a {@link PortBinding}, a network socket on the Docker host, expressed + * as a {@link Binding}, is bound to an {@link ExposedPort} of a container. + * A {@link PortBinding} corresponds to the --publish + * (-p) option of the docker run (and similar) + * CLI command for adding port bindings to a container. + *

+ * Note: This is an abstraction used for creating new port bindings. + * It is not to be confused with the abstraction used for querying existing + * port bindings from a container configuration in + * {@link NetworkSettings#getPorts()} and {@link HostConfig#getPortBindings()}. + * In that context, a Map<ExposedPort, Binding[]> is used. + */ +public class PortBinding { + private final Binding binding; + private final ExposedPort exposedPort; + + public PortBinding(Binding binding, ExposedPort exposedPort) { + this.binding = binding; + this.exposedPort = exposedPort; + } + + public Binding getBinding() { + return binding; + } + + public ExposedPort getExposedPort() { + return exposedPort; + } + + public static PortBinding parse(String serialized) throws IllegalArgumentException { + try { + String[] parts = StringUtils.splitByWholeSeparator(serialized, ":"); + switch (parts.length) { + case 3: + // 127.0.0.1:80:8080/tcp + return createFromSubstrings(parts[0] + ":" + parts[1], parts[2]); + case 2: + // 80:8080 // 127.0.0.1::8080 + return createFromSubstrings(parts[0], parts[1]); + case 1: + // 8080 + return createFromSubstrings("", parts[0]); + default: + throw new IllegalArgumentException(); + } + } catch (Exception e) { + throw new IllegalArgumentException("Error parsing PortBinding '" + + serialized + "'", e); + } + } + + private static PortBinding createFromSubstrings(String binding, String exposedPort) + throws IllegalArgumentException { + return new PortBinding(Binding.parse(binding), ExposedPort.parse(exposedPort)); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof PortBinding) { + PortBinding other = (PortBinding) obj; + return new EqualsBuilder().append(binding, other.getBinding()) + .append(exposedPort, other.getExposedPort()).isEquals(); + } else + return super.equals(obj); + } + + @Override + public int hashCode() { + return new HashCodeBuilder().append(binding).append(exposedPort).toHashCode(); + } + +} diff --git a/src/main/java/com/github/dockerjava/api/model/Ports.java b/src/main/java/com/github/dockerjava/api/model/Ports.java index a14b31036..d68175a5a 100644 --- a/src/main/java/com/github/dockerjava/api/model/Ports.java +++ b/src/main/java/com/github/dockerjava/api/model/Ports.java @@ -1,5 +1,7 @@ package com.github.dockerjava.api.model; +import static org.apache.commons.lang.StringUtils.isEmpty; + import java.io.IOException; import java.util.HashMap; import java.util.Iterator; @@ -8,7 +10,6 @@ import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.builder.EqualsBuilder; -import org.apache.commons.lang.builder.ToStringBuilder; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -28,6 +29,11 @@ /** * A container for port bindings, made available as a {@link Map} via its * {@link #getBindings()} method. + *

+ * Note: This is an abstraction used for querying existing port bindings from + * a container configuration. + * It is not to be confused with the {@link PortBinding} abstraction used for + * adding new port bindings to a container. * * @see HostConfig#getPortBindings() * @see NetworkSettings#getPorts() @@ -38,18 +44,42 @@ public class Ports { private final Map ports = new HashMap(); + /** + * Creates a {@link Ports} object with no {@link PortBinding}s. + * Use {@link #bind(ExposedPort, Binding)} or {@link #add(PortBinding...)} + * to add {@link PortBinding}s. + */ public Ports() { } + /** + * Creates a {@link Ports} object with an initial {@link PortBinding} for + * the specified {@link ExposedPort} and {@link Binding}. + * Use {@link #bind(ExposedPort, Binding)} or {@link #add(PortBinding...)} + * to add more {@link PortBinding}s. + */ public Ports(ExposedPort exposedPort, Binding host) { bind(exposedPort, host); } - public void bind(ExposedPort exposedPort, Binding host) { + /** + * Adds a new {@link PortBinding} for the specified {@link ExposedPort} and + * {@link Binding} to the current bindings. + */ + public void bind(ExposedPort exposedPort, Binding binding) { if (ports.containsKey(exposedPort)) { Binding[] bindings = ports.get(exposedPort); - ports.put(exposedPort, (Binding[]) ArrayUtils.add(bindings, host)); + ports.put(exposedPort, (Binding[]) ArrayUtils.add(bindings, binding)); } else { - ports.put(exposedPort, new Binding[]{host}); + ports.put(exposedPort, new Binding[]{binding}); + } + } + + /** + * Adds the specified {@link PortBinding}(s) to the list of {@link PortBinding}s. + */ + public void add(PortBinding... portBindings) { + for (PortBinding binding : portBindings) { + bind(binding.getExposedPort(), binding.getBinding()); } } @@ -59,6 +89,9 @@ public String toString(){ } /** + * Returns the port bindings in the format used by the Docker remote API, + * i.e. the {@link Binding}s grouped by {@link ExposedPort}. + * * @return the port bindings as a {@link Map} that contains one or more * {@link Binding}s per {@link ExposedPort}. */ @@ -66,53 +99,148 @@ public Map getBindings(){ return ports; } - public static Binding Binding(String hostIp, int hostPort) { + /** + * Creates a {@link Binding} for the given IP address and port number. + */ + public static Binding Binding(String hostIp, Integer hostPort) { return new Binding(hostIp, hostPort); } - public static Binding Binding(int hostPort) { + + /** + * Creates a {@link Binding} for the given port number, leaving the + * IP address undefined. + */ + public static Binding Binding(Integer hostPort) { return new Binding(hostPort); } /** - * The host part of a port binding. - * In a port binding a container port, expressed as an {@link ExposedPort}, - * is published as a port of the Docker host. + * A {@link Binding} represents a socket on the Docker host that is + * used in a {@link PortBinding}. + * It is characterized by an {@link #getHostIp() IP address} and a + * {@link #getHostPort() port number}. + * Both properties may be null in order to let Docker assign + * them dynamically/using defaults. * + * @see Ports#bind(ExposedPort, Binding) * @see ExposedPort */ public static class Binding { private final String hostIp; - private final int hostPort; + private final Integer hostPort; /** - * Creates the host part of a port binding. + * Creates a {@link Binding} for the given {@link #getHostIp() IP address} + * and {@link #getHostPort() port number}. * * @see Ports#bind(ExposedPort, Binding) * @see ExposedPort */ - public Binding(String hostIp, int hostPort) { - this.hostIp = hostIp; + public Binding(String hostIp, Integer hostPort) { + this.hostIp = isEmpty(hostIp) ? null : hostIp; this.hostPort = hostPort; } - public Binding(int hostPort) { - this("", hostPort); + /** + * Creates a {@link Binding} for the given {@link #getHostPort() port number}, + * leaving the {@link #getHostIp() IP address} undefined. + * + * @see Ports#bind(ExposedPort, Binding) + * @see ExposedPort + */ + public Binding(Integer hostPort) { + this(null, hostPort); } + /** + * Creates a {@link Binding} for the given {@link #getHostIp() IP address}, + * leaving the {@link #getHostPort() port number} undefined. + */ + public Binding(String hostIp) { + this(hostIp, null); + } + + /** + * Creates a {@link Binding} with both {@link #getHostIp() IP address} and + * {@link #getHostPort() port number} undefined. + */ + public Binding() { + this(null, null); + } + + /** + * @return the IP address on the Docker host. + * May be null, in which case Docker will bind the + * port to all interfaces (0.0.0.0). + */ public String getHostIp() { return hostIp; } - public int getHostPort() { + /** + * @return the port number on the Docker host. + * May be null, in which case Docker will dynamically + * assign a port. + */ + public Integer getHostPort() { return hostPort; } + /** + * Parses a textual host and port specification (as used by the Docker CLI) + * to a {@link Binding}. + *

+ * Legal syntax: IP|IP:port|port + * + * @param serialized serialized the specification, e.g. + * 127.0.0.1:80 + * @return a {@link Binding} matching the specification + * @throws IllegalArgumentException if the specification cannot be parsed + */ + public static Binding parse(String serialized) throws IllegalArgumentException { + try { + if (serialized.isEmpty()) { + return new Binding(); + } + + String[] parts = serialized.split(":"); + switch (parts.length) { + case 2: { + return new Binding(parts[0], Integer.valueOf(parts[1])); + } + case 1: { + return parts[0].contains(".") ? new Binding(parts[0]) + : new Binding(Integer.valueOf(parts[0])); + } + default: { + throw new IllegalArgumentException(); + } + } + } catch (Exception e) { + throw new IllegalArgumentException("Error parsing Binding '" + + serialized + "'"); + } + } + + /** + * Returns a string representation of this {@link Binding} suitable + * for inclusion in a JSON message. + * The format is [IP:]Port, like the argument in {@link #parse(String)}. + * + * @return a string representation of this {@link Binding} + */ @Override public String toString() { - return ToStringBuilder.reflectionToString(this); + if (isEmpty(hostIp)) { + return Integer.toString(hostPort); + } else if (hostPort == null) { + return hostIp; + } else { + return hostIp + ":" + hostPort; + } } @Override @@ -164,8 +292,8 @@ public void serialize(Ports portBindings, JsonGenerator jsonGen, jsonGen.writeStartArray(); for (Binding binding : entry.getValue()) { jsonGen.writeStartObject(); - jsonGen.writeStringField("HostIp", binding.getHostIp()); - jsonGen.writeStringField("HostPort", "" + binding.getHostPort()); + jsonGen.writeStringField("HostIp", binding.getHostIp() == null ? "" : binding.getHostIp()); + jsonGen.writeStringField("HostPort", binding.getHostPort() == null ? "" : binding.getHostPort().toString()); jsonGen.writeEndObject(); } jsonGen.writeEndArray(); diff --git a/src/main/java/com/github/dockerjava/core/command/StartContainerCmdImpl.java b/src/main/java/com/github/dockerjava/core/command/StartContainerCmdImpl.java index 9cf830833..74fbd6790 100644 --- a/src/main/java/com/github/dockerjava/core/command/StartContainerCmdImpl.java +++ b/src/main/java/com/github/dockerjava/core/command/StartContainerCmdImpl.java @@ -13,6 +13,7 @@ import com.github.dockerjava.api.model.Link; import com.github.dockerjava.api.model.Links; import com.github.dockerjava.api.model.LxcConf; +import com.github.dockerjava.api.model.PortBinding; import com.github.dockerjava.api.model.Ports; import com.github.dockerjava.api.model.RestartPolicy; import com.google.common.base.Preconditions; @@ -179,6 +180,16 @@ public StartContainerCmd withPortBindings(Ports portBindings) { return this; } + @Override + public StartContainerCmd withPortBindings(PortBinding... portBindings) { + Preconditions.checkNotNull(portBindings, "portBindings was not specified"); + if (this.portBindings == null) { + this.portBindings = new Ports(); + } + this.portBindings.add(portBindings); + return this; + } + @Override public StartContainerCmd withPrivileged(boolean privileged) { this.privileged = privileged; diff --git a/src/test/java/com/github/dockerjava/api/model/BindingTest.java b/src/test/java/com/github/dockerjava/api/model/BindingTest.java new file mode 100644 index 000000000..bd8488660 --- /dev/null +++ b/src/test/java/com/github/dockerjava/api/model/BindingTest.java @@ -0,0 +1,58 @@ +package com.github.dockerjava.api.model; + +import static org.testng.Assert.assertEquals; + +import org.testng.annotations.Test; + +import com.github.dockerjava.api.model.Ports.Binding; + +public class BindingTest { + + @Test + public void parseIpAndPort() { + assertEquals(Binding.parse("127.0.0.1:80"), Ports.Binding("127.0.0.1", 80)); + } + + @Test + public void parsePortOnly() { + assertEquals(Binding.parse("80"), Ports.Binding(null, 80)); + } + + @Test + public void parseIPOnly() { + assertEquals(Binding.parse("127.0.0.1"), Ports.Binding("127.0.0.1", null)); + } + + @Test + public void parseEmptyString() { + assertEquals(Binding.parse(""), Ports.Binding(null, null)); + } + + @Test(expectedExceptions = IllegalArgumentException.class, + expectedExceptionsMessageRegExp = "Error parsing Binding 'nonsense'") + public void parseInvalidInput() { + Binding.parse("nonsense"); + } + + @Test(expectedExceptions = IllegalArgumentException.class, + expectedExceptionsMessageRegExp = "Error parsing Binding 'null'") + public void parseNull() { + Binding.parse(null); + } + + @Test + public void toStringIpAndHost() { + assertEquals(Binding.parse("127.0.0.1:80").toString(), "127.0.0.1:80"); + } + + @Test + public void toStringPortOnly() { + assertEquals(Binding.parse("80").toString(), "80"); + } + + @Test + public void toStringIpOnly() { + assertEquals(Binding.parse("127.0.0.1").toString(), "127.0.0.1"); + } + +} diff --git a/src/test/java/com/github/dockerjava/api/model/ExposedPortTest.java b/src/test/java/com/github/dockerjava/api/model/ExposedPortTest.java index 052e44ff8..de1d23c21 100644 --- a/src/test/java/com/github/dockerjava/api/model/ExposedPortTest.java +++ b/src/test/java/com/github/dockerjava/api/model/ExposedPortTest.java @@ -1,5 +1,7 @@ package com.github.dockerjava.api.model; +import static com.github.dockerjava.api.model.InternetProtocol.DEFAULT; +import static com.github.dockerjava.api.model.InternetProtocol.TCP; import static org.testng.Assert.assertEquals; import org.testng.annotations.Test; @@ -7,9 +9,15 @@ public class ExposedPortTest { @Test - public void parse() { + public void parsePortAndProtocol() { ExposedPort exposedPort = ExposedPort.parse("80/tcp"); - assertEquals(exposedPort.getPort(), 80); + assertEquals(exposedPort, new ExposedPort(80, TCP)); + } + + @Test + public void parsePortOnly() { + ExposedPort exposedPort = ExposedPort.parse("80"); + assertEquals(exposedPort, new ExposedPort(80, DEFAULT)); } @Test(expectedExceptions = IllegalArgumentException.class, diff --git a/src/test/java/com/github/dockerjava/api/model/PortBindingTest.java b/src/test/java/com/github/dockerjava/api/model/PortBindingTest.java new file mode 100644 index 000000000..2aeb768f6 --- /dev/null +++ b/src/test/java/com/github/dockerjava/api/model/PortBindingTest.java @@ -0,0 +1,61 @@ +package com.github.dockerjava.api.model; + +import static org.testng.Assert.assertEquals; + +import org.testng.annotations.Test; + +import com.github.dockerjava.api.model.Ports.Binding; + +public class PortBindingTest { + + private static final ExposedPort TCP_8080 = ExposedPort.tcp(8080); + + @Test + public void fullDefinition() { + assertEquals(PortBinding.parse("127.0.0.1:80:8080/tcp"), + new PortBinding(new Binding("127.0.0.1", 80), TCP_8080)); + } + + @Test + public void noProtocol() { + assertEquals(PortBinding.parse("127.0.0.1:80:8080"), + new PortBinding(new Binding("127.0.0.1", 80), TCP_8080)); + } + + @Test + public void noHostIp() { + assertEquals(PortBinding.parse("80:8080/tcp"), + new PortBinding(new Binding(80), TCP_8080)); + } + + @Test + public void portsOnly() { + assertEquals(PortBinding.parse("80:8080"), + new PortBinding(new Binding(80), TCP_8080)); + } + + @Test + public void exposedPortOnly() { + assertEquals(PortBinding.parse("8080"), + new PortBinding(new Binding(), TCP_8080)); + } + + @Test + public void dynamicHostPort() { + assertEquals(PortBinding.parse("127.0.0.1::8080"), + new PortBinding(new Binding("127.0.0.1"), TCP_8080)); + } + + @Test(expectedExceptions = IllegalArgumentException.class, + expectedExceptionsMessageRegExp = "Error parsing PortBinding 'nonsense'") + public void parseInvalidInput() { + PortBinding.parse("nonsense"); + } + + @Test(expectedExceptions = IllegalArgumentException.class, + expectedExceptionsMessageRegExp = "Error parsing PortBinding 'null'") + public void parseNull() { + PortBinding.parse(null); + } + +} diff --git a/src/test/java/com/github/dockerjava/api/model/PortsTest.java b/src/test/java/com/github/dockerjava/api/model/Ports_SerializingTest.java similarity index 79% rename from src/test/java/com/github/dockerjava/api/model/PortsTest.java rename to src/test/java/com/github/dockerjava/api/model/Ports_SerializingTest.java index 6a6c5e44e..d295c881e 100644 --- a/src/test/java/com/github/dockerjava/api/model/PortsTest.java +++ b/src/test/java/com/github/dockerjava/api/model/Ports_SerializingTest.java @@ -9,7 +9,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.github.dockerjava.api.model.Ports.Binding; -public class PortsTest { +public class Ports_SerializingTest { private final ObjectMapper objectMapper = new ObjectMapper(); private final String jsonWithDoubleBindingForOnePort = "{\"80/tcp\":[{\"HostIp\":\"10.0.0.1\",\"HostPort\":\"80\"},{\"HostIp\":\"10.0.0.2\",\"HostPort\":\"80\"}]}"; @@ -34,4 +34,9 @@ public void serializingPortWithMultipleBindings() throws Exception { assertEquals(objectMapper.writeValueAsString(ports), jsonWithDoubleBindingForOnePort); } + @Test + public void serializingEmptyBinding() throws Exception { + Ports ports = new Ports(ExposedPort.tcp(80), new Binding(null, null)); + assertEquals(objectMapper.writeValueAsString(ports), "{\"80/tcp\":[{\"HostIp\":\"\",\"HostPort\":\"\"}]}"); + } } diff --git a/src/test/java/com/github/dockerjava/api/model/Ports_addBindingsTest.java b/src/test/java/com/github/dockerjava/api/model/Ports_addBindingsTest.java new file mode 100644 index 000000000..23abbdd2c --- /dev/null +++ b/src/test/java/com/github/dockerjava/api/model/Ports_addBindingsTest.java @@ -0,0 +1,57 @@ +package com.github.dockerjava.api.model; + +import static org.testng.Assert.assertEquals; + +import java.util.Map; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.github.dockerjava.api.model.Ports.Binding; + +/** + * As there may be several {@link Binding}s per {@link ExposedPort}, + * it makes a difference if you add {@link PortBinding}s for the + * same or different {@link ExposedPort}s to {@link Ports}. + * This test verifies that the Map in {@link Ports} is populated + * correctly in both cases. + */ +public class Ports_addBindingsTest { + private static final ExposedPort TCP_80 = ExposedPort.tcp(80); + private static final ExposedPort TCP_90 = ExposedPort.tcp(90); + private static final Binding BINDING_8080 = Ports.Binding(8080); + private static final Binding BINDING_9090 = Ports.Binding(9090); + + private Ports ports; + + @BeforeMethod + public void setup() { + ports = new Ports(); + } + + @Test + public void addTwoBindingsForDifferentExposedPorts() { + ports.add( + new PortBinding(BINDING_8080, TCP_80), + new PortBinding(BINDING_9090, TCP_90)); + + Map bindings = ports.getBindings(); + // two keys with one value each + assertEquals(bindings.size(), 2); + assertEquals(bindings.get(TCP_80), new Binding[] { BINDING_8080 }); + assertEquals(bindings.get(TCP_90), new Binding[] { BINDING_9090 }); + } + + @Test + public void addTwoBindingsForSameExposedPort() { + ports.add( + new PortBinding(BINDING_8080, TCP_80), + new PortBinding(BINDING_9090, TCP_80)); + + Map bindings = ports.getBindings(); + // one key with two values + assertEquals(bindings.size(), 1); + assertEquals(bindings.get(TCP_80), new Binding[] { BINDING_8080, BINDING_9090 }); + } + +}