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 0000000000..7511f63547
--- /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 (POST /containers/(id)/exec
).
+ */
+@AutoValue
+public abstract class Exec {
+
+ public abstract String id();
+
+ @SerializedNames({ "Id"})
+ public static Exec create(String id) {
+ return new AutoValue_Exec(id);
+ }
+}
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 0000000000..3f6009bc5f
--- /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 cmd();
+
+ @SerializedNames({ "AttachStdout", "AttachStderr", "Cmd" })
+ private static ExecCreateParams create(boolean attachStdout, boolean attachStderr, List 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 cmd);
+
+ abstract List cmd();
+
+ abstract ExecCreateParams autoBuild();
+
+ public ExecCreateParams build() {
+ cmd(copyOf(cmd()));
+ return autoBuild();
+ }
+ }
+}
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 0000000000..d15bccfc13
--- /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 (
+ * GET /exec/(id)/json
).
+ */
+@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);
+ }
+}
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 0000000000..f8046f8abf
--- /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();
+ }
+}
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 8e5548ede7..5df5d04b95 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);
}
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 0000000000..dacd7b7681
--- /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
+ * null
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);
+ }
+
+}
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 0000000000..f8f5f9a150
--- /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 null
+ * @param truncated
+ * @throws ArrayIndexOutOfBoundsException
+ * if streamTypeId is not an index in {@link StdStreamType} enum.
+ * @throws NullPointerException
+ * if provided payload is null
+ */
+ 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;
+ }
+}
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 ce2bc9e1c9..8cdd76bf5e 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. 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();
}
-
}
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 4fe87caac6..dd5d965ac5 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. 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.
diff --git a/apis/docker/src/test/resources/exec.json b/apis/docker/src/test/resources/exec.json
new file mode 100644
index 0000000000..d5f626505c
--- /dev/null
+++ b/apis/docker/src/test/resources/exec.json
@@ -0,0 +1 @@
+{"Id":"dbf45d296388032ebb9872edb75847f6655a72b4e9ab0d99ae1c75589c4ca957"}
diff --git a/apis/docker/src/test/resources/exec.start b/apis/docker/src/test/resources/exec.start
new file mode 100644
index 0000000000..0d726243cc
Binary files /dev/null and b/apis/docker/src/test/resources/exec.start differ
diff --git a/apis/docker/src/test/resources/execInspect.json b/apis/docker/src/test/resources/execInspect.json
new file mode 100644
index 0000000000..a47cf49d09
--- /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/07695c5170a1cbf3402552202fd28d2beb81fce5958a244e9ad7c8cb0ded936e/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":""}}