Repository: docker-java/docker-java
Pull request #161 (2015-02-25 14:48:01)

Inspect exec command
Currently docker-java lacks [exec inspection](https://docs.docker.com/reference/api/docker_remote_api_v1.16/#exec-inspect) command which provides, among everything else, last exec exit code, which may be important for client apps.

This PR adds support for this API call. It also adds a list of execs to the container inspection response.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ead9068e..29c1ef3d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,6 @@ Change Log
 ===
 Latest SNAPSHOT
 ---
- * [#159](https://github.com/docker-java/docker-java/pull/159) Add missing Info fields 
  * [#156](https://github.com/docker-java/docker-java/pull/156) Add support for configuring ExtraHosts 
 
 
diff --git a/src/main/java/com/github/dockerjava/api/DockerClient.java b/src/main/java/com/github/dockerjava/api/DockerClient.java
index d905ef10..16e70b6c 100644
--- a/src/main/java/com/github/dockerjava/api/DockerClient.java
+++ b/src/main/java/com/github/dockerjava/api/DockerClient.java
@@ -78,6 +78,8 @@ public interface DockerClient extends Closeable {
 
     public ExecStartCmd execStartCmd(String containerId);
 
+    public InspectExecCmd inspectExecCmd(String execId);
+
 	public LogContainerCmd logContainerCmd(String containerId);
 
 	public CopyFileFromContainerCmd copyFileFromContainerCmd(
diff --git a/src/main/java/com/github/dockerjava/api/command/DockerCmdExecFactory.java b/src/main/java/com/github/dockerjava/api/command/DockerCmdExecFactory.java
index ca2bbcae..8b988074 100644
--- a/src/main/java/com/github/dockerjava/api/command/DockerCmdExecFactory.java
+++ b/src/main/java/com/github/dockerjava/api/command/DockerCmdExecFactory.java
@@ -51,6 +51,8 @@ public interface DockerCmdExecFactory extends Closeable {
 
     public ExecStartCmd.Exec createExecStartCmdExec();
 
+    public InspectExecCmd.Exec createInspectExecCmdExec();
+
 	public LogContainerCmd.Exec createLogContainerCmdExec();
 
 	public CopyFileFromContainerCmd.Exec createCopyFileFromContainerCmdExec();
diff --git a/src/main/java/com/github/dockerjava/api/command/InspectContainerResponse.java b/src/main/java/com/github/dockerjava/api/command/InspectContainerResponse.java
index 80d52634..c86e13c5 100644
--- a/src/main/java/com/github/dockerjava/api/command/InspectContainerResponse.java
+++ b/src/main/java/com/github/dockerjava/api/command/InspectContainerResponse.java
@@ -1,6 +1,7 @@
 package com.github.dockerjava.api.command;
 
 
+import java.util.List;
 import java.util.Map;
 
 import com.github.dockerjava.api.model.*;
@@ -66,6 +67,9 @@ public class InspectContainerResponse {
 	@JsonProperty("ResolvConfPath")
 	private String resolvConfPath;
 
+    @JsonProperty("ExecIDs")
+    private List<String> execIds;
+
 	@JsonProperty("State")
     private ContainerState state;
 
@@ -153,6 +157,10 @@ public class InspectContainerResponse {
 		return mountLabel;
 	}
 
+    public List<String> getExecIds() {
+        return execIds;
+    }
+
     @Override
     public String toString() {
         return ToStringBuilder.reflectionToString(this);
diff --git a/src/main/java/com/github/dockerjava/api/command/InspectExecCmd.java b/src/main/java/com/github/dockerjava/api/command/InspectExecCmd.java
new file mode 100644
index 00000000..13882b8f
--- /dev/null
+++ b/src/main/java/com/github/dockerjava/api/command/InspectExecCmd.java
@@ -0,0 +1,16 @@
+package com.github.dockerjava.api.command;
+
+import com.github.dockerjava.api.NotFoundException;
+
+public interface InspectExecCmd extends DockerCmd<InspectExecResponse> {
+    public String getExecId();
+    public InspectExecCmd withExecId(String execId);
+
+    /**
+     * @throws NotFoundException if no such exec has been found
+     */
+    @Override
+    public InspectExecResponse exec() throws NotFoundException;
+
+    public static interface Exec extends DockerCmdExec<InspectExecCmd, InspectExecResponse> {}
+}
diff --git a/src/main/java/com/github/dockerjava/api/command/InspectExecResponse.java b/src/main/java/com/github/dockerjava/api/command/InspectExecResponse.java
new file mode 100644
index 00000000..1532f7d5
--- /dev/null
+++ b/src/main/java/com/github/dockerjava/api/command/InspectExecResponse.java
@@ -0,0 +1,100 @@
+package com.github.dockerjava.api.command;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.commons.lang.builder.ToStringBuilder;
+
+import java.util.List;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class InspectExecResponse {
+    @JsonProperty("ID")
+    private String id;
+
+    @JsonProperty("OpenStdin")
+    private boolean openStdin;
+
+    @JsonProperty("OpenStderr")
+    private boolean openStderr;
+
+    @JsonProperty("OpenStdout")
+    private boolean openStdout;
+
+    @JsonProperty("Running")
+    private boolean running;
+
+    @JsonProperty("ExitCode")
+    private int exitCode;
+
+    public String getId() {
+        return id;
+    }
+
+    public boolean isOpenStdin() {
+        return openStdin;
+    }
+
+    public boolean isOpenStderr() {
+        return openStderr;
+    }
+
+    public boolean isOpenStdout() {
+        return openStdout;
+    }
+
+    public boolean isRunning() {
+        return running;
+    }
+
+    public int getExitCode() {
+        return exitCode;
+    }
+
+    @Override
+    public String toString() {
+        return ToStringBuilder.reflectionToString(this);
+    }
+
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public class ProcessConfig {
+        @JsonProperty("arguments")
+        private List<String> arguments;
+
+        @JsonProperty("entrypoint")
+        private String entryPoint;
+
+        @JsonProperty("privileged")
+        private boolean privileged;
+
+        @JsonProperty("tty")
+        private boolean tty;
+
+        @JsonProperty("user")
+        private String user;
+
+        public List<String> getArguments() {
+            return arguments;
+        }
+
+        public String getEntryPoint() {
+            return entryPoint;
+        }
+
+        public boolean isPrivileged() {
+            return privileged;
+        }
+
+        public boolean isTty() {
+            return tty;
+        }
+
+        public String getUser() {
+            return user;
+        }
+
+        @Override
+        public String toString() {
+            return ToStringBuilder.reflectionToString(this);
+        }
+    }
+}
diff --git a/src/main/java/com/github/dockerjava/api/model/Info.java b/src/main/java/com/github/dockerjava/api/model/Info.java
index fb63f2ff..7fe6ae79 100644
--- a/src/main/java/com/github/dockerjava/api/model/Info.java
+++ b/src/main/java/com/github/dockerjava/api/model/Info.java
@@ -25,9 +25,6 @@ public class Info {
 
 	@JsonProperty("Debug")
 	private boolean debug;
-	
-	@JsonProperty("DockerRootDir")
-	private String DockerRootDir;
 
 	@JsonProperty("Driver")
 	private String driver;
@@ -37,12 +34,9 @@ public class Info {
 
 	@JsonProperty("ExecutionDriver")
 	private String executionDriver;
-	
-	@JsonProperty("ID")
-	private String ID;
 
 	@JsonProperty("IPv4Forwarding")
-	private boolean IPv4Forwarding;
+	private String IPv4Forwarding;
 
 	@JsonProperty("Images")
 	private int images;
@@ -58,21 +52,9 @@ public class Info {
 
 	@JsonProperty("KernelVersion")
 	private String kernelVersion;
-	
-	@JsonProperty("Labels")
-	private String Labels;
 
 	@JsonProperty("MemoryLimit")
 	private boolean memoryLimit;
-	
-	@JsonProperty("MemTotal")
-	private long memTotal;
-		
-	@JsonProperty("Name")
-	private String name;
-	
-	@JsonProperty("NCPU")
-	private int NCPU;
 
 	@JsonProperty("NEventsListener")
 	private long nEventListener;
@@ -82,15 +64,12 @@ public class Info {
 
 	@JsonProperty("NGoroutines")
 	private int NGoroutines;
-	
-	@JsonProperty("OperatingSystem")
-	private String OperatingSystem;
 
 	@JsonProperty("Sockets")
 	private String[] sockets;
 
 	@JsonProperty("SwapLimit")
-	private boolean swapLimit;
+	private int swapLimit;
 
 	public boolean isDebug() {
 		return debug;
@@ -99,10 +78,6 @@ public class Info {
 	public int getContainers() {
 		return containers;
 	}
-	
-	public String getDockerRootDir() {
-		return DockerRootDir;
-	}
 
 	public String getDriver() {
 		return driver;
@@ -115,12 +90,8 @@ public class Info {
 	public int getImages() {
 		return images;
 	}
-	
-	public String getID() {
-		return ID;
-	}
 
-	public boolean getIPv4Forwarding() {
+	public String getIPv4Forwarding() {
 		return IPv4Forwarding;
 	}
 
@@ -139,10 +110,6 @@ public class Info {
 	public String getKernelVersion() {
 		return kernelVersion;
 	}
-	
-	public String getLabels() {
-		return Labels;
-	}
 
 	public String[] getSockets() {
 		return sockets;
@@ -155,18 +122,6 @@ public class Info {
 	public long getnEventListener() {
 		return nEventListener;
 	}
-	
-	public long getMemTotal() {
-		return memTotal;
-	}
-	
-	public String getName() {
-		return name;
-	}
-	
-	public int getNCPU() {
-		return NCPU;
-	}
 
 	public int getNFd() {
 		return NFd;
@@ -175,12 +130,8 @@ public class Info {
 	public int getNGoroutines() {
 		return NGoroutines;
 	}
-	
-	public String getOperatingSystem() {
-		return OperatingSystem;
-	}
 
-	public boolean getSwapLimit() {
+	public int getSwapLimit() {
 		return swapLimit;
 	}
 
diff --git a/src/main/java/com/github/dockerjava/core/DockerClientImpl.java b/src/main/java/com/github/dockerjava/core/DockerClientImpl.java
index dd9e566d..8f561dff 100644
--- a/src/main/java/com/github/dockerjava/core/DockerClientImpl.java
+++ b/src/main/java/com/github/dockerjava/core/DockerClientImpl.java
@@ -224,7 +224,12 @@ public class DockerClientImpl implements Closeable, DockerClient {
         return new ExecStartCmdImpl(getDockerCmdExecFactory().createExecStartCmdExec(), containerId);
     }
 
-	@Override
+    @Override
+    public InspectExecCmd inspectExecCmd(String execId) {
+        return new InspectExecCmdImpl(getDockerCmdExecFactory().createInspectExecCmdExec(), execId);
+    }
+
+    @Override
 	public LogContainerCmd logContainerCmd(String containerId) {
 		return new LogContainerCmdImpl(getDockerCmdExecFactory()
 				.createLogContainerCmdExec(), containerId);
diff --git a/src/main/java/com/github/dockerjava/core/command/InspectExecCmdImpl.java b/src/main/java/com/github/dockerjava/core/command/InspectExecCmdImpl.java
new file mode 100644
index 00000000..1fd72862
--- /dev/null
+++ b/src/main/java/com/github/dockerjava/core/command/InspectExecCmdImpl.java
@@ -0,0 +1,41 @@
+package com.github.dockerjava.core.command;
+
+import com.github.dockerjava.api.NotFoundException;
+import com.github.dockerjava.api.command.DockerCmdExec;
+import com.github.dockerjava.api.command.InspectExecCmd;
+import com.github.dockerjava.api.command.InspectExecResponse;
+import com.google.common.base.Preconditions;
+
+public class InspectExecCmdImpl extends AbstrDockerCmd<InspectExecCmd, InspectExecResponse> implements InspectExecCmd {
+    private String execId;
+
+    public InspectExecCmdImpl(InspectExecCmd.Exec execution, String execId) {
+        super(execution);
+        withExecId(execId);
+    }
+
+    @Override
+    public String getExecId() {
+        return execId;
+    }
+
+    @Override
+    public InspectExecCmd withExecId(String execId) {
+        Preconditions.checkNotNull(execId, "execId was not specified");
+        this.execId = execId;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "inspect " + execId;
+    }
+
+    /**
+     * @throws NotFoundException No such exec
+     */
+    @Override
+    public InspectExecResponse exec() throws NotFoundException {
+        return super.exec();
+    }
+}
diff --git a/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java b/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java
index 9df09ba7..f1f178dc 100644
--- a/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java
+++ b/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java
@@ -221,6 +221,11 @@ public class DockerCmdExecFactoryImpl implements DockerCmdExecFactory {
         return new ExecStartCmdExec(getBaseResource());
     }
 
+    @Override
+    public InspectExecCmd.Exec createInspectExecCmdExec() {
+        return new InspectExecCmdExec(getBaseResource());
+    }
+
     @Override
     public LogContainerCmd.Exec createLogContainerCmdExec() {
         return new LogContainerCmdExec(getBaseResource());
diff --git a/src/main/java/com/github/dockerjava/jaxrs/InspectExecCmdExec.java b/src/main/java/com/github/dockerjava/jaxrs/InspectExecCmdExec.java
new file mode 100644
index 00000000..b9ee33d6
--- /dev/null
+++ b/src/main/java/com/github/dockerjava/jaxrs/InspectExecCmdExec.java
@@ -0,0 +1,24 @@
+package com.github.dockerjava.jaxrs;
+
+import com.github.dockerjava.api.command.InspectExecCmd;
+import com.github.dockerjava.api.command.InspectExecResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+
+public class InspectExecCmdExec extends AbstrDockerCmdExec<InspectExecCmd, InspectExecResponse> implements InspectExecCmd.Exec {
+    private static final Logger LOGGER = LoggerFactory.getLogger(InspectExecCmdExec.class);
+
+    public InspectExecCmdExec(WebTarget baseResource) {
+        super(baseResource);
+    }
+
+    @Override
+    protected InspectExecResponse execute(InspectExecCmd command) {
+        WebTarget webResource = getBaseResource().path("/exec/{id}/json").resolveTemplate("id", command.getExecId());
+        LOGGER.debug("GET: {}", webResource);
+        return webResource.request().accept(MediaType.APPLICATION_JSON).get(InspectExecResponse.class);
+    }
+}


Test patch:
diff --git a/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java b/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java
index 1c9342dd..869a12fc 100644
--- a/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java
+++ b/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java
@@ -194,7 +194,12 @@ public class TestDockerCmdExecFactory implements DockerCmdExecFactory {
 		return delegate.createExecStartCmdExec();
 	}
 
-	@Override
+    @Override
+    public InspectExecCmd.Exec createInspectExecCmdExec() {
+        return delegate.createInspectExecCmdExec();
+    }
+
+    @Override
 	public LogContainerCmd.Exec createLogContainerCmdExec() {
 		return delegate.createLogContainerCmdExec();
 	}
diff --git a/src/test/java/com/github/dockerjava/core/command/InfoCmdImplTest.java b/src/test/java/com/github/dockerjava/core/command/InfoCmdImplTest.java
index 7170c12f..d1de7076 100644
--- a/src/test/java/com/github/dockerjava/core/command/InfoCmdImplTest.java
+++ b/src/test/java/com/github/dockerjava/core/command/InfoCmdImplTest.java
@@ -68,6 +68,5 @@ public class InfoCmdImplTest extends AbstractDockerClientTest {
 		assertTrue(dockerInfo.getImages() > 0);
 		assertTrue(dockerInfo.getNFd() > 0);
 		assertTrue(dockerInfo.getNGoroutines() > 0);
-		assertTrue(dockerInfo.getNCPU() > 0);
 	}
 }
diff --git a/src/test/java/com/github/dockerjava/core/command/InspectExecCmdImplTest.java b/src/test/java/com/github/dockerjava/core/command/InspectExecCmdImplTest.java
new file mode 100644
index 00000000..aacc51f6
--- /dev/null
+++ b/src/test/java/com/github/dockerjava/core/command/InspectExecCmdImplTest.java
@@ -0,0 +1,96 @@
+package com.github.dockerjava.core.command;
+
+import com.github.dockerjava.api.DockerException;
+import com.github.dockerjava.api.command.CreateContainerResponse;
+import com.github.dockerjava.api.command.ExecCreateCmdResponse;
+import com.github.dockerjava.api.command.InspectExecResponse;
+import com.github.dockerjava.client.AbstractDockerClientTest;
+import org.testng.ITestResult;
+import org.testng.annotations.*;
+
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.security.SecureRandom;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.isEmptyString;
+import static org.hamcrest.Matchers.not;
+
+@Test(groups = "integration")
+public class InspectExecCmdImplTest extends AbstractDockerClientTest {
+    @BeforeTest
+    public void beforeTest() throws DockerException {
+        super.beforeTest();
+    }
+
+    @AfterTest
+    public void afterTest() {
+        super.afterTest();
+    }
+
+    @BeforeMethod
+    public void beforeMethod(Method method) {
+        super.beforeMethod(method);
+    }
+
+    @AfterMethod
+    public void afterMethod(ITestResult result) {
+        super.afterMethod(result);
+    }
+
+    @Test
+    public void inspectExecTest() {
+        String containerName = "generated_" + new SecureRandom().nextInt();
+
+        CreateContainerResponse container = dockerClient
+                .createContainerCmd("busybox")
+                .withCmd("top")
+                .withName(containerName).exec();
+        LOG.info("Created container {}", container.toString());
+        assertThat(container.getId(), not(isEmptyString()));
+
+        dockerClient.startContainerCmd(container.getId()).exec();
+
+        ExecCreateCmdResponse touchFileCmdCreateResponse = dockerClient.execCreateCmd(container.getId())
+                .withAttachStdout()
+                .withAttachStderr()
+                .withCmd("touch", "/marker").exec();
+        LOG.info("Created exec {}", touchFileCmdCreateResponse.toString());
+        assertThat(touchFileCmdCreateResponse.getId(), not(isEmptyString()));
+        ExecCreateCmdResponse checkFileCmdCreateResponse = dockerClient.execCreateCmd(container.getId())
+                .withAttachStdout()
+                .withAttachStderr()
+                .withCmd("test", "-e", "/marker").exec();
+        LOG.info("Created exec {}", checkFileCmdCreateResponse.toString());
+        assertThat(checkFileCmdCreateResponse.getId(), not(isEmptyString()));
+
+        // Check that file does not exist
+        InputStream response1 = dockerClient.execStartCmd(container.getId())
+                .withExecId(checkFileCmdCreateResponse.getId())
+                .exec();
+        asString(response1);  // consume
+
+        InspectExecResponse first = dockerClient.inspectExecCmd(checkFileCmdCreateResponse.getId()).exec();
+        assertThat(first.getExitCode(), is(1));
+
+        // Create the file
+        InputStream response2 = dockerClient.execStartCmd(container.getId())
+                .withExecId(touchFileCmdCreateResponse.getId())
+                .exec();
+        asString(response2);
+
+        InspectExecResponse second = dockerClient.inspectExecCmd(touchFileCmdCreateResponse.getId()).exec();
+        assertThat(second.getExitCode(), is(0));
+
+        // Check that file does exist now
+        InputStream response3 = dockerClient.execStartCmd(container.getId())
+                .withExecId(checkFileCmdCreateResponse.getId())
+                .exec();
+        asString(response3);
+
+        InspectExecResponse third = dockerClient.inspectExecCmd(checkFileCmdCreateResponse.getId()).exec();
+        assertThat(third.getExitCode(), is(0));
+
+    }
+}
diff --git a/src/test/java/com/github/dockerjava/core/command/StartContainerCmdImplTest.java b/src/test/java/com/github/dockerjava/core/command/StartContainerCmdImplTest.java
index 37d805aa..22307b07 100644
--- a/src/test/java/com/github/dockerjava/core/command/StartContainerCmdImplTest.java
+++ b/src/test/java/com/github/dockerjava/core/command/StartContainerCmdImplTest.java
@@ -283,7 +283,7 @@ public class StartContainerCmdImplTest extends AbstractDockerClientTest {
 		}
 
 		CreateContainerResponse container2 = dockerClient
-				.createContainerCmd("busybox").withCmd("sleep", "9999")
+				.createContainerCmd("busybox").withCmd("true")
 				.withName("container2").exec();
 
 		LOG.info("Created container2 {}", container2.toString());