diff --git a/README.md b/README.md index 039657807..d5052e6b9 100644 --- a/README.md +++ b/README.md @@ -51,23 +51,23 @@ For secure tls (https) communication: DOCKER_CERT_PATH=/Users/marcus/.docker/machine/machines/docker-1.11.2 ### Latest release version -Supports a subset of the Docker Remote API [v1.23](https://github.com/docker/docker/blob/master/docs/reference/api/docker_remote_api_v1.23.md), Docker Server version 1.11.x +Supports a subset of the Docker Remote API [v1.23](https://github.com/docker/docker/blob/master/docs/api/v1.23.md), Docker Server version 1.11.x com.github.docker-java docker-java - 3.0.3 + 3.0.6 ### Latest development version -Supports a subset of the Docker Remote API [v1.23](https://github.com/docker/docker/blob/master/docs/reference/api/docker_remote_api_v1.23.md), Docker Server version 1.11.x +Supports a subset of the Docker Remote API [v1.23](https://github.com/docker/docker/blob/master/docs/api/v1.23.md), Docker Server version 1.11.x You can find the latest development version including javadoc and source files on [Sonatypes OSS repository](https://oss.sonatype.org/content/groups/public/com/github/docker-java/docker-java/). com.github.docker-java docker-java - 3.0.4-SNAPSHOT + 3.0.7-SNAPSHOT diff --git a/src/main/java/com/github/dockerjava/api/model/Bind.java b/src/main/java/com/github/dockerjava/api/model/Bind.java index 0995c87ac..9a7ebf18d 100644 --- a/src/main/java/com/github/dockerjava/api/model/Bind.java +++ b/src/main/java/com/github/dockerjava/api/model/Bind.java @@ -18,24 +18,48 @@ public class Bind implements Serializable { private AccessMode accessMode; + /** + * @since {@link com.github.dockerjava.core.RemoteApiVersion#VERSION_1_23} + */ + private Boolean noCopy; + /** * @since {@link com.github.dockerjava.core.RemoteApiVersion#VERSION_1_17} */ private SELContext secMode; + /** + * @since {@link com.github.dockerjava.core.RemoteApiVersion#VERSION_1_22} + */ + private PropagationMode propagationMode; + public Bind(String path, Volume volume) { this(path, volume, AccessMode.DEFAULT, SELContext.DEFAULT); } + public Bind(String path, Volume volume, Boolean noCopy) { + this(path, volume, AccessMode.DEFAULT, SELContext.DEFAULT, noCopy); + } + public Bind(String path, Volume volume, AccessMode accessMode) { this(path, volume, accessMode, SELContext.DEFAULT); } public Bind(String path, Volume volume, AccessMode accessMode, SELContext secMode) { + this(path, volume, accessMode, secMode, null); + } + + public Bind(String path, Volume volume, AccessMode accessMode, SELContext secMode, Boolean noCopy) { + this(path, volume, accessMode, secMode, noCopy, PropagationMode.DEFAULT_MODE); + } + + public Bind(String path, Volume volume, AccessMode accessMode, SELContext secMode, Boolean noCopy, PropagationMode propagationMode) { this.path = path; this.volume = volume; this.accessMode = accessMode; this.secMode = secMode; + this.noCopy = noCopy; + this.propagationMode = propagationMode; } public String getPath() { @@ -54,6 +78,14 @@ public SELContext getSecMode() { return secMode; } + public Boolean getNoCopy() { + return noCopy; + } + + public PropagationMode getPropagationMode() { + return propagationMode; + } + /** * Parses a bind mount specification to a {@link Bind}. * @@ -74,15 +106,25 @@ public static Bind parse(String serialized) { String[] flags = parts[2].split(","); AccessMode accessMode = AccessMode.DEFAULT; SELContext seMode = SELContext.DEFAULT; + Boolean nocopy = null; + PropagationMode propagationMode = PropagationMode.DEFAULT_MODE; for (String p : flags) { if (p.length() == 2) { accessMode = AccessMode.valueOf(p.toLowerCase()); + } else if ("nocopy".equals(p)) { + nocopy = true; + } else if (PropagationMode.SHARED.toString().equals(p)) { + propagationMode = PropagationMode.SHARED; + } else if (PropagationMode.SLAVE.toString().equals(p)) { + propagationMode = PropagationMode.SLAVE; + } else if (PropagationMode.PRIVATE.toString().equals(p)) { + propagationMode = PropagationMode.PRIVATE; } else { seMode = SELContext.fromString(p); } } - return new Bind(parts[0], new Volume(parts[1]), accessMode, seMode); + return new Bind(parts[0], new Volume(parts[1]), accessMode, seMode, nocopy, propagationMode); } default: { throw new IllegalArgumentException(); @@ -102,6 +144,8 @@ public boolean equals(Object obj) { .append(volume, other.getVolume()) .append(accessMode, other.getAccessMode()) .append(secMode, other.getSecMode()) + .append(noCopy, other.getNoCopy()) + .append(propagationMode, other.getPropagationMode()) .isEquals(); } else { return super.equals(obj); @@ -115,6 +159,8 @@ public int hashCode() { .append(volume) .append(accessMode) .append(secMode) + .append(noCopy) + .append(propagationMode) .toHashCode(); } @@ -127,10 +173,12 @@ public int hashCode() { */ @Override public String toString() { - return String.format("%s:%s:%s%s", + return String.format("%s:%s:%s%s%s%s", path, volume.getPath(), accessMode.toString(), - secMode != SELContext.none ? "," + secMode.toString() : ""); + secMode != SELContext.none ? "," + secMode.toString() : "", + noCopy != null ? ",nocopy" : "", + propagationMode != PropagationMode.DEFAULT_MODE ? "," + propagationMode.toString() : ""); } } diff --git a/src/main/java/com/github/dockerjava/api/model/PropagationMode.java b/src/main/java/com/github/dockerjava/api/model/PropagationMode.java new file mode 100644 index 000000000..9be7d6e43 --- /dev/null +++ b/src/main/java/com/github/dockerjava/api/model/PropagationMode.java @@ -0,0 +1,50 @@ +package com.github.dockerjava.api.model; + +/** + * The propagation mode of a file system or file: shared, slave or private. + * + * @see https://github.com/docker/docker/pull/17034 + * @since 1.22 + */ +public enum PropagationMode { + /** default */ + DEFAULT(""), + + /** shared */ + SHARED("shared"), + + /** slave */ + SLAVE("slave"), + + /** private */ + PRIVATE("private"); + + /** + * The default {@link PropagationMode}: {@link #DEFAULT} + */ + public static final PropagationMode DEFAULT_MODE = DEFAULT; + + private String value; + + PropagationMode(String v) { + value = v; + } + + @Override + public String toString() { + return value; + } + + public static PropagationMode fromString(String v) { + switch (v) { + case "shared": + return SHARED; + case "slave": + return SLAVE; + case "private": + return PRIVATE; + default: + return DEFAULT; + } + } +} diff --git a/src/main/java/com/github/dockerjava/netty/NettyDockerCmdExecFactory.java b/src/main/java/com/github/dockerjava/netty/NettyDockerCmdExecFactory.java index cead92e92..c91d4af21 100644 --- a/src/main/java/com/github/dockerjava/netty/NettyDockerCmdExecFactory.java +++ b/src/main/java/com/github/dockerjava/netty/NettyDockerCmdExecFactory.java @@ -108,6 +108,9 @@ import com.github.dockerjava.netty.exec.RenameContainerCmdExec; import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelConfig; +import io.netty.channel.ChannelFactory; import io.netty.channel.ChannelInitializer; import io.netty.channel.EventLoopGroup; import io.netty.channel.epoll.EpollDomainSocketChannel; @@ -178,6 +181,8 @@ public DuplexChannel getChannel() { } }; + private Integer connectTimeout = null; + @Override public void init(DockerClientConfig dockerClientConfig) { checkNotNull(dockerClientConfig, "config was not specified"); @@ -218,7 +223,15 @@ private class UnixDomainSocketInitializer implements NettyInitializer { @Override public EventLoopGroup init(Bootstrap bootstrap, DockerClientConfig dockerClientConfig) { EventLoopGroup epollEventLoopGroup = new EpollEventLoopGroup(0, new DefaultThreadFactory(threadPrefix)); - bootstrap.group(epollEventLoopGroup).channel(EpollDomainSocketChannel.class) + + ChannelFactory factory = new ChannelFactory() { + @Override + public EpollDomainSocketChannel newChannel() { + return configure(new EpollDomainSocketChannel()); + } + }; + + bootstrap.group(epollEventLoopGroup).channelFactory(factory) .handler(new ChannelInitializer() { @Override protected void initChannel(final UnixChannel channel) throws Exception { @@ -245,7 +258,14 @@ public EventLoopGroup init(Bootstrap bootstrap, final DockerClientConfig dockerC Security.addProvider(new BouncyCastleProvider()); - bootstrap.group(nioEventLoopGroup).channel(NioSocketChannel.class) + ChannelFactory factory = new ChannelFactory() { + @Override + public NioSocketChannel newChannel() { + return configure(new NioSocketChannel()); + } + }; + + bootstrap.group(nioEventLoopGroup).channelFactory(factory) .handler(new ChannelInitializer() { @Override protected void initChannel(final SocketChannel channel) throws Exception { @@ -580,6 +600,24 @@ public void close() throws IOException { eventLoopGroup.shutdownGracefully(); } + /** + * Configure connection timeout in milliseconds + */ + public NettyDockerCmdExecFactory withConnectTimeout(Integer connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + private T configure(T channel) { + ChannelConfig channelConfig = channel.config(); + + if (connectTimeout != null) { + channelConfig.setConnectTimeoutMillis(connectTimeout); + } + + return channel; + } + private WebTarget getBaseResource() { return new WebTarget(channelProvider); } diff --git a/src/test/java/com/github/dockerjava/api/model/BindTest.java b/src/test/java/com/github/dockerjava/api/model/BindTest.java index 3204ee716..2e6fd8353 100644 --- a/src/test/java/com/github/dockerjava/api/model/BindTest.java +++ b/src/test/java/com/github/dockerjava/api/model/BindTest.java @@ -3,6 +3,7 @@ import static com.github.dockerjava.api.model.AccessMode.ro; import static com.github.dockerjava.api.model.AccessMode.rw; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.Is.is; import org.testng.annotations.Test; @@ -16,6 +17,8 @@ public void parseUsingDefaultAccessMode() { assertThat(bind.getVolume().getPath(), is("/container")); assertThat(bind.getAccessMode(), is(AccessMode.DEFAULT)); assertThat(bind.getSecMode(), is(SELContext.none)); + assertThat(bind.getNoCopy(), nullValue()); + assertThat(bind.getPropagationMode(), is(PropagationMode.DEFAULT_MODE)); } @Test @@ -25,6 +28,52 @@ public void parseReadWrite() { assertThat(bind.getVolume().getPath(), is("/container")); assertThat(bind.getAccessMode(), is(rw)); assertThat(bind.getSecMode(), is(SELContext.none)); + assertThat(bind.getNoCopy(), nullValue()); + assertThat(bind.getPropagationMode(), is(PropagationMode.DEFAULT_MODE)); + } + + @Test + public void parseReadWriteNoCopy() { + Bind bind = Bind.parse("/host:/container:rw,nocopy"); + assertThat(bind.getPath(), is("/host")); + assertThat(bind.getVolume().getPath(), is("/container")); + assertThat(bind.getAccessMode(), is(rw)); + assertThat(bind.getSecMode(), is(SELContext.none)); + assertThat(bind.getNoCopy(), is(true)); + assertThat(bind.getPropagationMode(), is(PropagationMode.DEFAULT_MODE)); + } + + @Test + public void parseReadWriteShared() { + Bind bind = Bind.parse("/host:/container:rw,shared"); + assertThat(bind.getPath(), is("/host")); + assertThat(bind.getVolume().getPath(), is("/container")); + assertThat(bind.getAccessMode(), is(rw)); + assertThat(bind.getSecMode(), is(SELContext.none)); + assertThat(bind.getNoCopy(), nullValue()); + assertThat(bind.getPropagationMode(), is(PropagationMode.SHARED)); + } + + @Test + public void parseReadWriteSlave() { + Bind bind = Bind.parse("/host:/container:rw,slave"); + assertThat(bind.getPath(), is("/host")); + assertThat(bind.getVolume().getPath(), is("/container")); + assertThat(bind.getAccessMode(), is(rw)); + assertThat(bind.getSecMode(), is(SELContext.none)); + assertThat(bind.getNoCopy(), nullValue()); + assertThat(bind.getPropagationMode(), is(PropagationMode.SLAVE)); + } + + @Test + public void parseReadWritePrivate() { + Bind bind = Bind.parse("/host:/container:rw,private"); + assertThat(bind.getPath(), is("/host")); + assertThat(bind.getVolume().getPath(), is("/container")); + assertThat(bind.getAccessMode(), is(rw)); + assertThat(bind.getSecMode(), is(SELContext.none)); + assertThat(bind.getNoCopy(), nullValue()); + assertThat(bind.getPropagationMode(), is(PropagationMode.PRIVATE)); } @Test @@ -34,6 +83,8 @@ public void parseReadOnly() { assertThat(bind.getVolume().getPath(), is("/container")); assertThat(bind.getAccessMode(), is(ro)); assertThat(bind.getSecMode(), is(SELContext.none)); + assertThat(bind.getNoCopy(), nullValue()); + assertThat(bind.getPropagationMode(), is(PropagationMode.DEFAULT_MODE)); } @Test @@ -43,12 +94,16 @@ public void parseSELOnly() { assertThat(bind.getVolume().getPath(), is("/container")); assertThat(bind.getAccessMode(), is(AccessMode.DEFAULT)); assertThat(bind.getSecMode(), is(SELContext.single)); + assertThat(bind.getNoCopy(), nullValue()); + assertThat(bind.getPropagationMode(), is(PropagationMode.DEFAULT_MODE)); bind = Bind.parse("/host:/container:z"); assertThat(bind.getPath(), is("/host")); assertThat(bind.getVolume().getPath(), is("/container")); assertThat(bind.getAccessMode(), is(AccessMode.DEFAULT)); assertThat(bind.getSecMode(), is(SELContext.shared)); + assertThat(bind.getNoCopy(), nullValue()); + assertThat(bind.getPropagationMode(), is(PropagationMode.DEFAULT_MODE)); } @Test @@ -58,6 +113,8 @@ public void parseReadWriteSEL() { assertThat(bind.getVolume().getPath(), is("/container")); assertThat(bind.getAccessMode(), is(rw)); assertThat(bind.getSecMode(), is(SELContext.single)); + assertThat(bind.getNoCopy(), nullValue()); + assertThat(bind.getPropagationMode(), is(PropagationMode.DEFAULT_MODE)); } @Test @@ -67,6 +124,8 @@ public void parseReadOnlySEL() { assertThat(bind.getVolume().getPath(), is("/container")); assertThat(bind.getAccessMode(), is(ro)); assertThat(bind.getSecMode(), is(SELContext.shared)); + assertThat(bind.getNoCopy(), nullValue()); + assertThat(bind.getPropagationMode(), is(PropagationMode.DEFAULT_MODE)); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Error parsing Bind.*") @@ -94,6 +153,26 @@ public void toStringReadWrite() { assertThat(Bind.parse("/host:/container:rw").toString(), is("/host:/container:rw")); } + @Test + public void toStringReadWriteNoCopy() { + assertThat(Bind.parse("/host:/container:rw,nocopy").toString(), is("/host:/container:rw,nocopy")); + } + + @Test + public void toStringReadWriteShared() { + assertThat(Bind.parse("/host:/container:rw,shared").toString(), is("/host:/container:rw,shared")); + } + + @Test + public void toStringReadWriteSlave() { + assertThat(Bind.parse("/host:/container:rw,slave").toString(), is("/host:/container:rw,slave")); + } + + @Test + public void toStringReadWritePrivate() { + assertThat(Bind.parse("/host:/container:rw,private").toString(), is("/host:/container:rw,private")); + } + @Test public void toStringDefaultAccessMode() { assertThat(Bind.parse("/host:/container").toString(), is("/host:/container:rw")); diff --git a/src/test/java/com/github/dockerjava/netty/exec/CreateContainerCmdExecTest.java b/src/test/java/com/github/dockerjava/netty/exec/CreateContainerCmdExecTest.java index 3694ca7fb..aa97b8839 100644 --- a/src/test/java/com/github/dockerjava/netty/exec/CreateContainerCmdExecTest.java +++ b/src/test/java/com/github/dockerjava/netty/exec/CreateContainerCmdExecTest.java @@ -2,6 +2,7 @@ import com.github.dockerjava.api.command.CreateContainerResponse; import com.github.dockerjava.api.command.CreateNetworkResponse; +import com.github.dockerjava.api.command.CreateVolumeResponse; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.exception.ConflictException; import com.github.dockerjava.api.exception.DockerException; @@ -15,6 +16,7 @@ import com.github.dockerjava.api.model.Network; import com.github.dockerjava.api.model.Ports; import com.github.dockerjava.api.model.Ports.Binding; +import com.github.dockerjava.core.RemoteApiVersion; import com.github.dockerjava.api.model.RestartPolicy; import com.github.dockerjava.api.model.Ulimit; import com.github.dockerjava.api.model.Volume; @@ -23,6 +25,7 @@ import org.apache.commons.io.FileUtils; import org.testng.ITestResult; +import org.testng.SkipException; import org.testng.annotations.AfterMethod; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeMethod; @@ -40,6 +43,7 @@ import static com.github.dockerjava.api.model.Capability.MKNOD; import static com.github.dockerjava.api.model.Capability.NET_ADMIN; +import static com.github.dockerjava.utils.TestUtils.getVersion; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -138,6 +142,39 @@ public void createContainerWithReadOnlyVolume() throws DockerException { // assertFalse(inspectContainerResponse.getMounts().get(0).getRW()); } + @Test + public void createContainerWithNoCopyVolumes() throws DockerException { + final RemoteApiVersion apiVersion = getVersion(dockerClient); + + if (!apiVersion.isGreaterOrEqual(RemoteApiVersion.VERSION_1_23)) { + throw new SkipException("API version should be >= 1.23"); + } + + Volume volume1 = new Volume("/opt/webapp1"); + String container1Name = UUID.randomUUID().toString(); + + CreateVolumeResponse volumeResponse = dockerClient.createVolumeCmd().withName("webapp1").exec(); + assertThat(volumeResponse.getName(), equalTo("webapp1")); + assertThat(volumeResponse.getDriver(), equalTo("local")); + assertThat(volumeResponse.getMountpoint(), containsString("/webapp1/")); + + Bind bind1 = new Bind("webapp1", volume1, true); + + CreateContainerResponse container1 = dockerClient.createContainerCmd("busybox").withCmd("sleep", "9999") + .withName(container1Name) + .withBinds(bind1).exec(); + LOG.info("Created container1 {}", container1.toString()); + + InspectContainerResponse inspectContainerResponse1 = dockerClient.inspectContainerCmd(container1.getId()).exec(); + + assertThat(Arrays.asList(inspectContainerResponse1.getHostConfig().getBinds()), contains(bind1)); + assertThat(inspectContainerResponse1, mountedVolumes(contains(volume1))); + + assertThat(inspectContainerResponse1.getMounts().get(0).getDestination(), equalTo(volume1)); + assertThat(inspectContainerResponse1.getMounts().get(0).getMode(), equalTo("rw,nocopy")); + assertThat(inspectContainerResponse1.getMounts().get(0).getRW(), equalTo(true)); + } + @Test public void createContainerWithVolumesFrom() throws DockerException {