You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jclouds.apache.org by na...@apache.org on 2016/08/30 09:35:38 UTC

[14/50] jclouds git commit: [JCLOUDS-1007] Implemented Docker Exec support in MiscApi

[JCLOUDS-1007] Implemented Docker Exec support in MiscApi


Project: http://git-wip-us.apache.org/repos/asf/jclouds/repo
Commit: http://git-wip-us.apache.org/repos/asf/jclouds/commit/f6ad2cc3
Tree: http://git-wip-us.apache.org/repos/asf/jclouds/tree/f6ad2cc3
Diff: http://git-wip-us.apache.org/repos/asf/jclouds/diff/f6ad2cc3

Branch: refs/heads/master
Commit: f6ad2cc3803332595bf6af8a08f669c2e66a0e19
Parents: d7b3f5d
Author: Josef Cacek <jo...@gmail.com>
Authored: Mon Sep 28 23:08:00 2015 +0200
Committer: Ignasi Barrera <na...@apache.org>
Committed: Fri Oct 2 16:29:50 2015 +0200

----------------------------------------------------------------------
 .../java/org/jclouds/docker/domain/Exec.java    |  35 ++++++
 .../jclouds/docker/domain/ExecCreateParams.java |  72 ++++++++++++
 .../org/jclouds/docker/domain/ExecInspect.java  |  40 +++++++
 .../jclouds/docker/domain/ExecStartParams.java  |  47 ++++++++
 .../org/jclouds/docker/features/MiscApi.java    |  55 +++++++++
 .../jclouds/docker/util/DockerInputStream.java  |  73 ++++++++++++
 .../org/jclouds/docker/util/StdStreamData.java  |  87 +++++++++++++++
 .../docker/features/MiscApiLiveTest.java        | 111 ++++++++++++++++++-
 .../docker/features/MiscApiMockTest.java        |  93 ++++++++++++++--
 apis/docker/src/test/resources/exec.json        |   1 +
 apis/docker/src/test/resources/exec.start       | Bin 0 -> 29 bytes
 apis/docker/src/test/resources/execInspect.json |   1 +
 12 files changed, 604 insertions(+), 11 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/f6ad2cc3/apis/docker/src/main/java/org/jclouds/docker/domain/Exec.java
----------------------------------------------------------------------
diff --git a/apis/docker/src/main/java/org/jclouds/docker/domain/Exec.java b/apis/docker/src/main/java/org/jclouds/docker/domain/Exec.java
new file mode 100644
index 0000000..7511f63
--- /dev/null
+++ b/apis/docker/src/main/java/org/jclouds/docker/domain/Exec.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.docker.domain;
+
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * Represents a response from Exec Create call (<code>POST /containers/(id)/exec</code>).
+ */
+@AutoValue
+public abstract class Exec {
+
+   public abstract String id();
+
+   @SerializedNames({ "Id"})
+   public static Exec create(String id) {
+      return new AutoValue_Exec(id);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f6ad2cc3/apis/docker/src/main/java/org/jclouds/docker/domain/ExecCreateParams.java
----------------------------------------------------------------------
diff --git a/apis/docker/src/main/java/org/jclouds/docker/domain/ExecCreateParams.java b/apis/docker/src/main/java/org/jclouds/docker/domain/ExecCreateParams.java
new file mode 100644
index 0000000..3f6009b
--- /dev/null
+++ b/apis/docker/src/main/java/org/jclouds/docker/domain/ExecCreateParams.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.docker.domain;
+
+import static org.jclouds.docker.internal.NullSafeCopies.copyOf;
+
+import java.util.List;
+
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * Json Parameters (some of them) of Exec Create call.
+ */
+@AutoValue
+public abstract class ExecCreateParams {
+
+   public abstract boolean attachStdout();
+
+   public abstract boolean attachStderr();
+
+   public abstract List<String> cmd();
+
+   @SerializedNames({ "AttachStdout", "AttachStderr", "Cmd" })
+   private static ExecCreateParams create(boolean attachStdout, boolean attachStderr, List<String> cmd) {
+      return builder().attachStdout(attachStdout).attachStderr(attachStderr).cmd(cmd).build();
+   }
+
+   /**
+    * Creates builder for {@link ExecCreateParams}, it sets
+    * {@link #attachStderr()} and {@link #attachStdout()} to true as a default.
+    *
+    * @return new {@link ExecCreateParams.Builder} instance
+    */
+   public static Builder builder() {
+      return new AutoValue_ExecCreateParams.Builder().attachStderr(true).attachStdout(true);
+   }
+
+   @AutoValue.Builder
+   public abstract static class Builder {
+
+      public abstract Builder attachStdout(boolean b);
+
+      public abstract Builder attachStderr(boolean b);
+
+      public abstract Builder cmd(List<String> cmd);
+
+      abstract List<String> cmd();
+
+      abstract ExecCreateParams autoBuild();
+
+      public ExecCreateParams build() {
+         cmd(copyOf(cmd()));
+         return autoBuild();
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f6ad2cc3/apis/docker/src/main/java/org/jclouds/docker/domain/ExecInspect.java
----------------------------------------------------------------------
diff --git a/apis/docker/src/main/java/org/jclouds/docker/domain/ExecInspect.java b/apis/docker/src/main/java/org/jclouds/docker/domain/ExecInspect.java
new file mode 100644
index 0000000..d15bccf
--- /dev/null
+++ b/apis/docker/src/main/java/org/jclouds/docker/domain/ExecInspect.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.docker.domain;
+
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * Represents a response (part of it) from Exec Inspect call (
+ * <code>GET /exec/(id)/json</code>).
+ */
+@AutoValue
+public abstract class ExecInspect {
+
+   public abstract String id();
+
+   public abstract boolean running();
+
+   public abstract int exitCode();
+
+   @SerializedNames({ "ID", "Running", "ExitCode"})
+   public static ExecInspect create(String id, boolean running, int exitCode) {
+      return new AutoValue_ExecInspect(id, running, exitCode);
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f6ad2cc3/apis/docker/src/main/java/org/jclouds/docker/domain/ExecStartParams.java
----------------------------------------------------------------------
diff --git a/apis/docker/src/main/java/org/jclouds/docker/domain/ExecStartParams.java b/apis/docker/src/main/java/org/jclouds/docker/domain/ExecStartParams.java
new file mode 100644
index 0000000..f8046f8
--- /dev/null
+++ b/apis/docker/src/main/java/org/jclouds/docker/domain/ExecStartParams.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.docker.domain;
+
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * Json Parameter(s) (some of them) of Exec Start call.
+ */
+@AutoValue
+public abstract class ExecStartParams {
+
+   public abstract boolean detach();
+
+   @SerializedNames({ "Detach" })
+   public static ExecStartParams create(boolean detach) {
+      return builder().detach(detach).build();
+   }
+
+   public static Builder builder() {
+      return new AutoValue_ExecStartParams.Builder().detach(false);
+   }
+
+   @AutoValue.Builder
+   public abstract static class Builder {
+
+      public abstract Builder detach(boolean b);
+
+      public abstract ExecStartParams build();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f6ad2cc3/apis/docker/src/main/java/org/jclouds/docker/features/MiscApi.java
----------------------------------------------------------------------
diff --git a/apis/docker/src/main/java/org/jclouds/docker/features/MiscApi.java b/apis/docker/src/main/java/org/jclouds/docker/features/MiscApi.java
index 8e5548e..5df5d04 100644
--- a/apis/docker/src/main/java/org/jclouds/docker/features/MiscApi.java
+++ b/apis/docker/src/main/java/org/jclouds/docker/features/MiscApi.java
@@ -23,13 +23,21 @@ import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
 import javax.ws.rs.core.MediaType;
 
+import org.jclouds.docker.domain.Exec;
+import org.jclouds.docker.domain.ExecCreateParams;
+import org.jclouds.docker.domain.ExecInspect;
+import org.jclouds.docker.domain.ExecStartParams;
 import org.jclouds.docker.domain.Info;
 import org.jclouds.docker.domain.Version;
 import org.jclouds.docker.options.BuildOptions;
+import org.jclouds.docker.util.DockerInputStream;
 import org.jclouds.io.Payload;
+import org.jclouds.rest.annotations.BinderParam;
 import org.jclouds.rest.annotations.Headers;
+import org.jclouds.rest.binders.BindToJsonPayload;
 
 @Consumes(MediaType.APPLICATION_JSON)
 @Path("/v{jclouds.api-version}")
@@ -82,4 +90,51 @@ public interface MiscApi {
    @Headers(keys = { "Content-Type", "Connection" }, values = { "application/tar", "close" })
    InputStream build(Payload inputStream, BuildOptions options);
 
+   /**
+    * Sets up an exec instance in a running container with given Id.
+    *
+    * @param containerId
+    *           container Id
+    * @param execCreateParams
+    *           exec parameters
+    * @return an instance which holds exec identifier
+    */
+   @Named("container:exec")
+   @POST
+   @Path("/containers/{id}/exec")
+   Exec execCreate(@PathParam("id") String containerId,
+         @BinderParam(BindToJsonPayload.class) ExecCreateParams execCreateParams);
+
+   /**
+    * Starts a previously set up exec instance id. If
+    * {@link ExecStartParams#detach()} is true, this API returns after starting
+    * the exec command. Otherwise, this API sets up an interactive session with
+    * the exec command.
+    *
+    * @param execId
+    *           exec instance id
+    * @param execStartParams
+    *           start parameters
+    * @return raw docker stream which can be wrapped to
+    *         {@link DockerInputStream}
+    * @see #execCreate(String, ExecCreateParams)
+    * @see DockerInputStream
+    */
+   @Named("exec:start")
+   @POST
+   @Path("/exec/{id}/start")
+   InputStream execStart(@PathParam("id") String execId,
+         @BinderParam(BindToJsonPayload.class) ExecStartParams execStartParams);
+
+   /**
+    * Returns low-level information about the exec command id.
+    * 
+    * @param execId
+    *           exec instance id
+    * @return details about exec instance
+    */
+   @Named("exec:inspect")
+   @GET
+   @Path("/exec/{id}/json")
+   ExecInspect execInspect(@PathParam("id") String execId);
 }

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f6ad2cc3/apis/docker/src/main/java/org/jclouds/docker/util/DockerInputStream.java
----------------------------------------------------------------------
diff --git a/apis/docker/src/main/java/org/jclouds/docker/util/DockerInputStream.java b/apis/docker/src/main/java/org/jclouds/docker/util/DockerInputStream.java
new file mode 100644
index 0000000..dacd7b7
--- /dev/null
+++ b/apis/docker/src/main/java/org/jclouds/docker/util/DockerInputStream.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jclouds.docker.util;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Extension to {@link DataInputStream} which adds method
+ * {@link #readStdStreamData()} to allow read multiplexed standard streams.
+ */
+public final class DockerInputStream extends DataInputStream {
+
+   /**
+    * Ctor from superclass.
+    *
+    * @param in
+    * @see DataInputStream#DataInputStream(InputStream)
+    */
+   public DockerInputStream(InputStream in) {
+      super(in);
+   }
+
+   /**
+    * @return {@link StdStreamData} instance read from the input stream or
+    *         <code>null</code> if we reached end of the stream.
+    * @throws IOException
+    */
+   public StdStreamData readStdStreamData() throws IOException {
+      byte[] header = new byte[8];
+      // try to read first byte from the message header - just to check if we
+      // are at the end
+      // of stream
+      if (-1 == read(header, 0, 1)) {
+         return null;
+      }
+      // read the rest of the header
+      readFully(header, 1, 7);
+      // decode size as an unsigned int
+      long size = (long) (header[4] & 0xFF) << 24 | (header[5] & 0xFF) << 16 | (header[6] & 0xFF) << 8
+            | (header[7] & 0xFF);
+
+      byte[] payload;
+      // The size from the header is an unsigned int so it can happen the byte
+      // array has not a sufficient size and we'll have to truncate the frame
+      payload = new byte[(int) Math.min(Integer.MAX_VALUE, size)];
+      readFully(payload);
+      boolean truncated = false;
+      if (size > Integer.MAX_VALUE) {
+         truncated = true;
+         // skip the rest
+         readFully(new byte[(int) (size - Integer.MAX_VALUE)]);
+      }
+      return new StdStreamData(header[0], payload, truncated);
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f6ad2cc3/apis/docker/src/main/java/org/jclouds/docker/util/StdStreamData.java
----------------------------------------------------------------------
diff --git a/apis/docker/src/main/java/org/jclouds/docker/util/StdStreamData.java b/apis/docker/src/main/java/org/jclouds/docker/util/StdStreamData.java
new file mode 100644
index 0000000..f8f5f9a
--- /dev/null
+++ b/apis/docker/src/main/java/org/jclouds/docker/util/StdStreamData.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jclouds.docker.util;
+
+import java.util.Arrays;
+
+/**
+ * Representation of single message from docker-raw-stream. It holds stream
+ * type, data (payload) and flag which says if the payload was truncated. The
+ * truncation can occur when the frame size is greater than
+ * {@link Integer#MAX_VALUE}.
+ */
+public final class StdStreamData {
+
+   private final StdStreamType type;
+   private final byte[] payload;
+   private final boolean truncated;
+
+   /**
+    * Ctor.
+    *
+    * @param streamTypeId
+    *           standard stream type (0=stdIn, 1=stdOut, 2=stdErr)
+    * @param payload
+    *           message data - must not be <code>null</code>
+    * @param truncated
+    * @throws ArrayIndexOutOfBoundsException
+    *            if streamTypeId is not an index in {@link StdStreamType} enum.
+    * @throws NullPointerException
+    *            if provided payload is <code>null</code>
+    */
+   StdStreamData(byte streamTypeId, byte[] payload, boolean truncated)
+         throws ArrayIndexOutOfBoundsException, NullPointerException {
+      this.type = StdStreamType.values()[streamTypeId];
+      this.payload = Arrays.copyOf(payload, payload.length);
+      this.truncated = truncated;
+   }
+
+   /**
+    * Type of stream.
+    *
+    * @return
+    */
+   public StdStreamType getType() {
+      return type;
+   }
+
+   /**
+    * Data from this message.
+    *
+    * @return payload.
+    */
+   public byte[] getPayload() {
+      return payload;
+   }
+
+   /**
+    * Flag which says if the payload was truncated (because of size).
+    *
+    * @return true if truncated
+    */
+   public boolean isTruncated() {
+      return truncated;
+   }
+
+   /**
+    * Standard streams enum. The order of entries is important!
+    */
+   public static enum StdStreamType {
+      IN, OUT, ERR;
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f6ad2cc3/apis/docker/src/test/java/org/jclouds/docker/features/MiscApiLiveTest.java
----------------------------------------------------------------------
diff --git a/apis/docker/src/test/java/org/jclouds/docker/features/MiscApiLiveTest.java b/apis/docker/src/test/java/org/jclouds/docker/features/MiscApiLiveTest.java
index ce2bc9e..8cdd76b 100644
--- a/apis/docker/src/test/java/org/jclouds/docker/features/MiscApiLiveTest.java
+++ b/apis/docker/src/test/java/org/jclouds/docker/features/MiscApiLiveTest.java
@@ -16,23 +16,79 @@
  */
 package org.jclouds.docker.features;
 
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URISyntaxException;
 
+import org.assertj.core.api.Fail;
 import org.jclouds.docker.compute.BaseDockerApiLiveTest;
+import org.jclouds.docker.domain.Config;
+import org.jclouds.docker.domain.Container;
+import org.jclouds.docker.domain.Exec;
+import org.jclouds.docker.domain.ExecCreateParams;
+import org.jclouds.docker.domain.ExecInspect;
+import org.jclouds.docker.domain.ExecStartParams;
+import org.jclouds.docker.domain.Image;
 import org.jclouds.docker.options.BuildOptions;
+import org.jclouds.docker.options.CreateImageOptions;
+import org.jclouds.docker.options.RemoveContainerOptions;
+import org.jclouds.docker.util.DockerInputStream;
+import org.jclouds.docker.util.StdStreamData;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
 import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 
 @Test(groups = "live", testName = "MiscApiLiveTest", singleThreaded = true)
 public class MiscApiLiveTest extends BaseDockerApiLiveTest {
 
+   protected static final String BUSYBOX_IMAGE_TAG = "busybox:ubuntu-12.04";
+
    private static String imageId;
 
+   private Container container = null;
+   private Image image = null;
+   private Exec exec = null;
+
+   @BeforeClass
+   protected void init() {
+      if (api.getImageApi().inspectImage(BUSYBOX_IMAGE_TAG) == null) {
+         CreateImageOptions options = CreateImageOptions.Builder.fromImage(BUSYBOX_IMAGE_TAG);
+         InputStream createImageStream = api.getImageApi().createImage(options);
+         consumeStream(createImageStream);
+      }
+      image = api.getImageApi().inspectImage(BUSYBOX_IMAGE_TAG);
+      assertNotNull(image);
+      Config containerConfig = Config.builder().image(image.id())
+            .cmd(ImmutableList.of("/bin/sh", "-c", "touch hello; while true; do echo hello world; sleep 1; done"))
+            .build();
+      container = api.getContainerApi().createContainer("miscApiTest", containerConfig);
+      assertNotNull(container);
+      api.getContainerApi().startContainer(container.id());
+      assertTrue(api.getContainerApi().inspectContainer(container.id()).state().running());
+   }
+
+   @AfterClass
+   protected void tearDown() {
+      if (container != null) {
+         if (api.getContainerApi().inspectContainer(container.id()) != null) {
+            api.getContainerApi().removeContainer(container.id(), RemoveContainerOptions.Builder.force(true));
+         }
+      }
+      if (image != null) {
+         api.getImageApi().deleteImage(BUSYBOX_IMAGE_TAG);
+      }
+   }
+
    @Test
    public void testVersion() {
       assertNotNull(api().getVersion().apiVersion());
@@ -61,9 +117,62 @@ public class MiscApiLiveTest extends BaseDockerApiLiveTest {
       assertNotNull(imageId);
    }
 
+   @Test
+   public void testExecCreate() {
+      exec = api().execCreate(container.id(),
+            ExecCreateParams.builder()
+                  .cmd(ImmutableList.<String> of("/bin/sh", "-c",
+                        "echo -n Standard >&1 && echo -n Error >&2 && exit 2"))
+                  .attachStderr(true).attachStdout(true).build());
+      assertNotNull(exec);
+      assertNotNull(exec.id());
+   }
+
+   @Test(dependsOnMethods = "testExecCreate")
+   public void testExecStart() throws IOException {
+      final ExecStartParams startParams = ExecStartParams.builder().detach(false).build();
+      DockerInputStream inputStream = null;
+      try {
+         inputStream = new DockerInputStream(api().execStart(exec.id(), startParams));
+         assertNotNull(inputStream);
+         ByteArrayOutputStream baosOut = new ByteArrayOutputStream();
+         ByteArrayOutputStream baosErr = new ByteArrayOutputStream();
+         StdStreamData data = null;
+         while (null != (data = inputStream.readStdStreamData())) {
+            assertFalse(data.isTruncated());
+            switch (data.getType()) {
+               case OUT:
+                  baosOut.write(data.getPayload());
+                  break;
+               case ERR:
+                  baosErr.write(data.getPayload());
+                  break;
+               default:
+                  Fail.fail("Unexpected Stream type");
+                  break;
+            }
+         }
+         assertEquals(baosOut.toString(), "Standard");
+         assertEquals(baosErr.toString(), "Error");
+      } finally {
+         if (inputStream != null) {
+            inputStream.close();
+         }
+      }
+   }
+
+   @Test(dependsOnMethods = "testExecStart")
+   public void testExecInspect() throws IOException {
+      ExecInspect execInspect = api().execInspect(exec.id());
+      assertNotNull(execInspect);
+      assertEquals(execInspect.id(), exec.id());
+      assertEquals(execInspect.running(), false);
+      assertEquals(execInspect.exitCode(), 2);
+   }
+
+   
    private MiscApi api() {
       return api.getMiscApi();
    }
 
-
 }

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f6ad2cc3/apis/docker/src/test/java/org/jclouds/docker/features/MiscApiMockTest.java
----------------------------------------------------------------------
diff --git a/apis/docker/src/test/java/org/jclouds/docker/features/MiscApiMockTest.java b/apis/docker/src/test/java/org/jclouds/docker/features/MiscApiMockTest.java
index 4fe87ca..dd5d965 100644
--- a/apis/docker/src/test/java/org/jclouds/docker/features/MiscApiMockTest.java
+++ b/apis/docker/src/test/java/org/jclouds/docker/features/MiscApiMockTest.java
@@ -16,25 +16,40 @@
  */
 package org.jclouds.docker.features;
 
-import com.squareup.okhttp.mockwebserver.MockResponse;
-import com.squareup.okhttp.mockwebserver.MockWebServer;
-import com.squareup.okhttp.mockwebserver.RecordedRequest;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.jclouds.docker.compute.BaseDockerApiLiveTest.tarredDockerfile;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import javax.ws.rs.core.HttpHeaders;
 
 import org.jclouds.docker.DockerApi;
+import org.jclouds.docker.config.DockerParserModule;
+import org.jclouds.docker.domain.Exec;
+import org.jclouds.docker.domain.ExecCreateParams;
+import org.jclouds.docker.domain.ExecInspect;
+import org.jclouds.docker.domain.ExecStartParams;
 import org.jclouds.docker.internal.BaseDockerMockTest;
 import org.jclouds.docker.parse.InfoParseTest;
 import org.jclouds.docker.parse.VersionParseTest;
+import org.jclouds.docker.util.DockerInputStream;
+import org.jclouds.docker.util.StdStreamData;
+import org.jclouds.docker.util.StdStreamData.StdStreamType;
 import org.jclouds.io.Payload;
 import org.jclouds.io.Payloads;
 import org.testng.annotations.Test;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.jclouds.docker.compute.BaseDockerApiLiveTest.tarredDockerfile;
-import static org.testng.Assert.assertEquals;
-import java.io.File;
-import java.io.FileInputStream;
-
-import javax.ws.rs.core.HttpHeaders;
+import com.google.common.collect.ImmutableList;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.RecordedRequest;
 
 /**
  * Mock tests for the {@link org.jclouds.docker.features.MiscApi} class.
@@ -92,6 +107,64 @@ public class MiscApiMockTest extends BaseDockerMockTest {
       }
    }
 
+
+   public void testExecCreate() throws Exception {
+      MockWebServer server = mockWebServer(new MockResponse().setBody(payloadFromResource("/exec.json")));
+      MiscApi api = api(DockerApi.class, server.getUrl("/").toString(), new DockerParserModule()).getMiscApi();
+      try {
+         final String containerId = "a40d212a0a379de00426a1da2a8fd3fd20d5f74fd7c2dd42f6c93a6b1b0e6974";
+         final ExecCreateParams execParams = ExecCreateParams.builder()
+               .cmd(ImmutableList.<String> of("/bin/sh", "-c", "echo -n Standard >&1 && echo -n Error >&2"))
+               .attachStderr(true).attachStdout(true).build();
+         final Exec expectedExec = Exec.create("dbf45d296388032ebb9872edb75847f6655a72b4e9ab0d99ae1c75589c4ca957");
+         assertEquals(api.execCreate(containerId, execParams), expectedExec);
+         assertSent(server, "POST", "/containers/" + containerId + "/exec");
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   public void testExecStart() throws Exception {
+      MockWebServer server = mockWebServer(new MockResponse().setBody(payloadFromResource("/exec.start")));
+      MiscApi api = api(DockerApi.class, server.getUrl("/").toString(), new DockerParserModule()).getMiscApi();
+      DockerInputStream dis = null;
+      try {
+         final String execId = "dbf45d296388032ebb9872edb75847f6655a72b4e9ab0d99ae1c75589c4ca957";
+         final ExecStartParams startParams = ExecStartParams.builder().detach(false).build();
+         dis = new DockerInputStream(api.execStart(execId, startParams));
+
+         final StdStreamData msg1 = dis.readStdStreamData();
+         assertFalse(msg1.isTruncated());
+         assertEquals(msg1.getPayload(), "Standard".getBytes(StandardCharsets.UTF_8));
+         assertEquals(msg1.getType(), StdStreamType.OUT);
+
+         final StdStreamData msg2 = dis.readStdStreamData();
+         assertFalse(msg2.isTruncated());
+         assertEquals(msg2.getPayload(), "Error".getBytes(StandardCharsets.UTF_8));
+         assertEquals(msg2.getType(), StdStreamType.ERR);
+
+         assertNull(dis.readStdStreamData());
+         assertSent(server, "POST", "/exec/" + execId + "/start");
+      } finally {
+         if (dis != null) {
+            dis.close();
+         }
+         server.shutdown();
+      }
+   }
+
+   public void testExecInspect() throws IOException, InterruptedException {
+      MockWebServer server = mockWebServer(new MockResponse().setBody(payloadFromResource("/execInspect.json")));
+      MiscApi api = api(DockerApi.class, server.getUrl("/").toString(), new DockerParserModule()).getMiscApi();
+      final String expectedExecId = "fda1cf8064863fc0667c691c69793fdb7d0bd4a1fabb8250536abe5203e4208a";
+      ExecInspect execInspect = api.execInspect(expectedExecId);
+      assertNotNull(execInspect);
+      assertEquals(execInspect.id(), expectedExecId);
+      assertEquals(execInspect.running(), false);
+      assertEquals(execInspect.exitCode(), 2);
+      assertSent(server, "GET", "/exec/" + expectedExecId + "/json");
+   }
+
    /**
     * Asserts that correct values of HTTP headers are used in Docker build REST
     * API calls.

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f6ad2cc3/apis/docker/src/test/resources/exec.json
----------------------------------------------------------------------
diff --git a/apis/docker/src/test/resources/exec.json b/apis/docker/src/test/resources/exec.json
new file mode 100644
index 0000000..d5f6265
--- /dev/null
+++ b/apis/docker/src/test/resources/exec.json
@@ -0,0 +1 @@
+{"Id":"dbf45d296388032ebb9872edb75847f6655a72b4e9ab0d99ae1c75589c4ca957"}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f6ad2cc3/apis/docker/src/test/resources/exec.start
----------------------------------------------------------------------
diff --git a/apis/docker/src/test/resources/exec.start b/apis/docker/src/test/resources/exec.start
new file mode 100644
index 0000000..0d72624
Binary files /dev/null and b/apis/docker/src/test/resources/exec.start differ

http://git-wip-us.apache.org/repos/asf/jclouds/blob/f6ad2cc3/apis/docker/src/test/resources/execInspect.json
----------------------------------------------------------------------
diff --git a/apis/docker/src/test/resources/execInspect.json b/apis/docker/src/test/resources/execInspect.json
new file mode 100644
index 0000000..a47cf49
--- /dev/null
+++ b/apis/docker/src/test/resources/execInspect.json
@@ -0,0 +1 @@
+{"ID":"fda1cf8064863fc0667c691c69793fdb7d0bd4a1fabb8250536abe5203e4208a","Running":false,"ExitCode":2,"ProcessConfig":{"privileged":false,"user":"","tty":false,"entrypoint":"/bin/sh","arguments":["-c","echo -n Standard \u003e\u00261 \u0026\u0026 echo -n Error \u003e\u00262 \u0026\u0026 exit 2"]},"OpenStdin":false,"OpenStderr":true,"OpenStdout":true,"Container":{"State":{"Running":true,"Paused":false,"Restarting":false,"OOMKilled":false,"Dead":false,"Pid":1561,"ExitCode":0,"Error":"","StartedAt":"2015-09-29T12:36:21.011469908Z","FinishedAt":"0001-01-01T00:00:00Z"},"ID":"07695c5170a1cbf3402552202fd28d2beb81fce5958a244e9ad7c8cb0ded936e","Created":"2015-09-29T12:36:19.829846377Z","Path":"/bin/sh","Args":["-c","touch hello; while true; do echo hello world; sleep 1; done"],"Config":{"Hostname":"07695c5170a1","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"PortSpecs":null,"ExposedPorts":null,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null
 ,"Cmd":["/bin/sh","-c","touch hello; while true; do echo hello world; sleep 1; done"],"Image":"ff8f955d1fed83a6239675b9a767fc9502db9934922a2f69ac6bf4cad8a7d7be","Volumes":null,"VolumeDriver":"","WorkingDir":"","Entrypoint":null,"NetworkDisabled":false,"MacAddress":"","OnBuild":null,"Labels":{},"Init":""},"Image":"ff8f955d1fed83a6239675b9a767fc9502db9934922a2f69ac6bf4cad8a7d7be","NetworkSettings":{"Bridge":"","EndpointID":"78d38a6f4d63966b16ca824a533b70f0a3a00490586688a188f67ec0bb07e68f","Gateway":"172.17.42.1","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"HairpinMode":false,"IPAddress":"172.17.0.3","IPPrefixLen":16,"IPv6Gateway":"","LinkLocalIPv6Address":"","LinkLocalIPv6PrefixLen":0,"MacAddress":"02:42:ac:11:00:03","NetworkID":"880f733909fa4ad45c8fbc95b80c83e22a7b46f8cd7fb23dea62b0996ef1c8c6","PortMapping":null,"Ports":{},"SandboxKey":"/var/run/docker/netns/07695c5170a1","SecondaryIPAddresses":null,"SecondaryIPv6Addresses":null},"ResolvConfPath":"/var/lib/docker/containers/07695c
 5170a1cbf3402552202fd28d2beb81fce5958a244e9ad7c8cb0ded936e/resolv.conf","HostnamePath":"/var/lib/docker/containers/07695c5170a1cbf3402552202fd28d2beb81fce5958a244e9ad7c8cb0ded936e/hostname","HostsPath":"/var/lib/docker/containers/07695c5170a1cbf3402552202fd28d2beb81fce5958a244e9ad7c8cb0ded936e/hosts","LogPath":"/var/lib/docker/containers/07695c5170a1cbf3402552202fd28d2beb81fce5958a244e9ad7c8cb0ded936e/07695c5170a1cbf3402552202fd28d2beb81fce5958a244e9ad7c8cb0ded936e-json.log","Name":"/miscApiTest","Driver":"devicemapper","ExecDriver":"native-0.2","MountLabel":"","ProcessLabel":"","RestartCount":0,"UpdateDns":false,"MountPoints":{},"Volumes":{},"VolumesRW":{},"Init":"","AppArmorProfile":""}}