From 602008c241469d6bc41d406d8504aa1fc6021a08 Mon Sep 17 00:00:00 2001 From: Nigel Magnay Date: Fri, 2 Jan 2015 11:30:10 +0000 Subject: [PATCH 1/4] There are two different event streamed JSON objects that come back from commands. Create representations for them. Signed-off-by: Nigel Magnay --- .../dockerjava/api/model/EventStreamItem.java | 62 +++++++++++++++++++ .../api/model/PushEventStreamItem.java | 58 +++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 src/main/java/com/github/dockerjava/api/model/EventStreamItem.java create mode 100644 src/main/java/com/github/dockerjava/api/model/PushEventStreamItem.java diff --git a/src/main/java/com/github/dockerjava/api/model/EventStreamItem.java b/src/main/java/com/github/dockerjava/api/model/EventStreamItem.java new file mode 100644 index 0000000000..98c61a8622 --- /dev/null +++ b/src/main/java/com/github/dockerjava/api/model/EventStreamItem.java @@ -0,0 +1,62 @@ +package com.github.dockerjava.api.model; + +import com.google.common.base.Objects; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; + +/** + * Represents an event stream + */ +@JsonIgnoreProperties(ignoreUnknown=true) +public class EventStreamItem implements Serializable { + + @JsonProperty("stream") + private String stream; + + // {"error":"Error...", "errorDetail":{"code": 123, "message": "Error..."}} + @JsonProperty("error") + private String error; + + @JsonProperty("errorDetail") + private ErrorDetail errorDetail; + + public String getStream() { + return stream; + } + + public String getError() { + return error; + } + + public ErrorDetail getErrorDetail() { + return errorDetail; + } + + @JsonIgnoreProperties(ignoreUnknown=true) + public static class ErrorDetail implements Serializable { + @JsonProperty("code") + String code; + @JsonProperty("message") + String message; + + @Override + public String toString() { + return Objects.toStringHelper(this) + .add("code", code) + .add("message", message) + .toString(); + } + } + + @Override + public String toString() { + return Objects.toStringHelper(this) + .add("stream", stream) + .add("error", error) + .add("errorDetail", errorDetail) + .toString(); + } +} diff --git a/src/main/java/com/github/dockerjava/api/model/PushEventStreamItem.java b/src/main/java/com/github/dockerjava/api/model/PushEventStreamItem.java new file mode 100644 index 0000000000..df7f59f50d --- /dev/null +++ b/src/main/java/com/github/dockerjava/api/model/PushEventStreamItem.java @@ -0,0 +1,58 @@ +package com.github.dockerjava.api.model; + +import com.google.common.base.Objects; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; + +/** + * Represents an item returned from push + */ +@JsonIgnoreProperties(ignoreUnknown=true) +public class PushEventStreamItem implements Serializable { + + @JsonProperty("status") + private String status; + + @JsonProperty("progress") + private String progress; + + @JsonProperty("progressDetail") + private ProgressDetail progressDetail; + + + public String getStatus() { + return status; + } + + public String getProgress() { + return progress; + } + + public ProgressDetail getProgressDetail() { + return progressDetail; + } + + @JsonIgnoreProperties(ignoreUnknown=true) + public static class ProgressDetail implements Serializable { + @JsonProperty("current") + int current; + + + @Override + public String toString() { + return "current " + current; + } + } + + @Override + public String toString() { + return Objects.toStringHelper(this) + .add("status", status) + .add("progress", progress) + .add("progressDetail", progressDetail) + .toString(); + } +} From 8140c4707886246480eb5e330c53c85e84ebe0d7 Mon Sep 17 00:00:00 2001 From: Nigel Magnay Date: Fri, 2 Jan 2015 11:35:36 +0000 Subject: [PATCH 2/4] Make BuildImage and PushImage return an object that can be iterated over in a typed fashion so that clients can read out the responses. Use Iterable so the response does not have to be 'complete' before consuming. Derive from InputStream to maintain backwards compatibility and for clients that don't want to parse the response. Signed-off-by: Nigel Magnay --- .../dockerjava/api/command/BuildImageCmd.java | 11 +++- .../dockerjava/api/command/PushImageCmd.java | 13 ++-- .../core/command/BuildImageCmdImpl.java | 3 +- .../core/command/PushImageCmdImpl.java | 6 +- .../dockerjava/jaxrs/BuildImageCmdExec.java | 49 ++++++++++++--- .../dockerjava/jaxrs/PushImageCmdExec.java | 63 +++++++++++++++---- .../core/TestDockerCmdExecFactory.java | 9 +-- 7 files changed, 119 insertions(+), 35 deletions(-) diff --git a/src/main/java/com/github/dockerjava/api/command/BuildImageCmd.java b/src/main/java/com/github/dockerjava/api/command/BuildImageCmd.java index 7c2deb63a5..8a1e621150 100644 --- a/src/main/java/com/github/dockerjava/api/command/BuildImageCmd.java +++ b/src/main/java/com/github/dockerjava/api/command/BuildImageCmd.java @@ -1,5 +1,9 @@ package com.github.dockerjava.api.command; +import com.github.dockerjava.api.model.EventStreamItem; +import com.github.dockerjava.api.model.PushEventStreamItem; + +import java.io.IOException; import java.io.InputStream; /** @@ -9,7 +13,7 @@ * TODO: http://docs.docker.com/reference/builder/#dockerignore * */ -public interface BuildImageCmd extends DockerCmd{ +public interface BuildImageCmd extends DockerCmd{ public BuildImageCmd withTag(String tag); @@ -37,7 +41,10 @@ public interface BuildImageCmd extends DockerCmd{ public BuildImageCmd withQuiet(boolean quiet); - public static interface Exec extends DockerCmdExec { + public static interface Exec extends DockerCmdExec { } + public static abstract class Response extends InputStream { + public abstract Iterable getItems() throws IOException; + } } \ No newline at end of file diff --git a/src/main/java/com/github/dockerjava/api/command/PushImageCmd.java b/src/main/java/com/github/dockerjava/api/command/PushImageCmd.java index f50beb01ef..80e16c6d95 100644 --- a/src/main/java/com/github/dockerjava/api/command/PushImageCmd.java +++ b/src/main/java/com/github/dockerjava/api/command/PushImageCmd.java @@ -1,16 +1,18 @@ package com.github.dockerjava.api.command; +import java.io.IOException; import java.io.InputStream; import com.github.dockerjava.api.NotFoundException; import com.github.dockerjava.api.model.AuthConfig; +import com.github.dockerjava.api.model.PushEventStreamItem; /** * Push the latest image to the repository. * * @param name The name, e.g. "alexec/busybox" or just "busybox" if you want to default. Not null. */ -public interface PushImageCmd extends DockerCmd{ +public interface PushImageCmd extends DockerCmd{ public String getName(); @@ -33,10 +35,13 @@ public interface PushImageCmd extends DockerCmd{ /** * @throws NotFoundException No such image */ - @Override - public InputStream exec() throws NotFoundException; + public Response exec() throws NotFoundException; - public static interface Exec extends DockerCmdExec { + public static interface Exec extends DockerCmdExec { + } + + public static abstract class Response extends InputStream { + public abstract Iterable getItems() throws IOException; } } \ No newline at end of file diff --git a/src/main/java/com/github/dockerjava/core/command/BuildImageCmdImpl.java b/src/main/java/com/github/dockerjava/core/command/BuildImageCmdImpl.java index 0bdb6db36e..468dac7e7b 100644 --- a/src/main/java/com/github/dockerjava/core/command/BuildImageCmdImpl.java +++ b/src/main/java/com/github/dockerjava/core/command/BuildImageCmdImpl.java @@ -32,8 +32,7 @@ * Build an image from Dockerfile. * */ -public class BuildImageCmdImpl extends - AbstrDockerCmd implements BuildImageCmd { +public class BuildImageCmdImpl extends AbstrDockerCmd implements BuildImageCmd { private static final Pattern ADD_OR_COPY_PATTERN = Pattern .compile("^(ADD|COPY)\\s+(.*)\\s+(.*)$"); diff --git a/src/main/java/com/github/dockerjava/core/command/PushImageCmdImpl.java b/src/main/java/com/github/dockerjava/core/command/PushImageCmdImpl.java index 137c008a3b..1914f91dd9 100644 --- a/src/main/java/com/github/dockerjava/core/command/PushImageCmdImpl.java +++ b/src/main/java/com/github/dockerjava/core/command/PushImageCmdImpl.java @@ -2,8 +2,6 @@ import static jersey.repackaged.com.google.common.base.Preconditions.checkNotNull; -import java.io.InputStream; - import com.github.dockerjava.api.NotFoundException; import com.github.dockerjava.api.command.PushImageCmd; @@ -12,7 +10,7 @@ * * @param name The name, e.g. "alexec/busybox" or just "busybox" if you want to default. Not null. */ -public class PushImageCmdImpl extends AbstrAuthCfgDockerCmd implements PushImageCmd { +public class PushImageCmdImpl extends AbstrAuthCfgDockerCmd implements PushImageCmd { private String name; private String tag; @@ -63,7 +61,7 @@ public String toString() { * @throws NotFoundException No such image */ @Override - public InputStream exec() throws NotFoundException { + public Response exec() throws NotFoundException { return super.exec(); } } diff --git a/src/main/java/com/github/dockerjava/jaxrs/BuildImageCmdExec.java b/src/main/java/com/github/dockerjava/jaxrs/BuildImageCmdExec.java index 85c5f18c22..2b5d9a57ad 100644 --- a/src/main/java/com/github/dockerjava/jaxrs/BuildImageCmdExec.java +++ b/src/main/java/com/github/dockerjava/jaxrs/BuildImageCmdExec.java @@ -1,8 +1,12 @@ package com.github.dockerjava.jaxrs; +import com.google.common.collect.ImmutableList; + import static javax.ws.rs.client.Entity.entity; +import java.io.IOException; import java.io.InputStream; +import java.util.Iterator; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.MediaType; @@ -13,9 +17,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; import com.github.dockerjava.api.command.BuildImageCmd; +import com.github.dockerjava.api.command.PushImageCmd; +import com.github.dockerjava.api.model.EventStreamItem; +import com.github.dockerjava.api.model.PushEventStreamItem; -public class BuildImageCmdExec extends AbstrDockerCmdExec implements BuildImageCmd.Exec { +public class BuildImageCmdExec extends AbstrDockerCmdExec implements BuildImageCmd.Exec { private static final Logger LOGGER = LoggerFactory .getLogger(BuildImageCmdExec.class); @@ -25,7 +34,7 @@ public BuildImageCmdExec(WebTarget baseResource) { } @Override - protected InputStream execute(BuildImageCmd command) { + protected ResponseImpl execute(BuildImageCmd command) { WebTarget webResource = getBaseResource().path("/build"); if(command.getTag() != null) { @@ -45,12 +54,38 @@ protected InputStream execute(BuildImageCmd command) { webResource.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.CHUNKED); webResource.property(ClientProperties.CHUNKED_ENCODING_SIZE, 1024*1024); - LOGGER.debug("POST: {}", webResource); - return webResource - .request() - .accept(MediaType.TEXT_PLAIN) - .post(entity(command.getTarInputStream(), "application/tar"), Response.class).readEntity(InputStream.class); + + LOGGER.debug("POST: {}", webResource); + InputStream is = webResource + .request() + .accept(MediaType.TEXT_PLAIN) + .post(entity(command.getTarInputStream(), "application/tar"), Response.class).readEntity(InputStream.class); + + return new ResponseImpl(is); } + public static class ResponseImpl extends BuildImageCmd.Response { + + private final InputStream proxy; + + public ResponseImpl(InputStream proxy) { + this.proxy = proxy; + } + + @Override + public Iterable getItems() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + // we'll be reading instances of MyBean + ObjectReader reader = mapper.reader(EventStreamItem.class); + // and then do other configuration, if any, and read: + Iterator items = reader.readValues(proxy); + + return ImmutableList.copyOf(items); + } + @Override + public int read() throws IOException { + return proxy.read(); + } + } } diff --git a/src/main/java/com/github/dockerjava/jaxrs/PushImageCmdExec.java b/src/main/java/com/github/dockerjava/jaxrs/PushImageCmdExec.java index b970d42fc8..838c9b89a6 100644 --- a/src/main/java/com/github/dockerjava/jaxrs/PushImageCmdExec.java +++ b/src/main/java/com/github/dockerjava/jaxrs/PushImageCmdExec.java @@ -1,6 +1,12 @@ package com.github.dockerjava.jaxrs; +import com.google.common.collect.ImmutableList; + +import static javax.ws.rs.client.Entity.entity; + +import java.io.IOException; import java.io.InputStream; +import java.util.Iterator; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.MediaType; @@ -8,37 +14,70 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; import com.github.dockerjava.api.command.PushImageCmd; +import com.github.dockerjava.api.command.PushImageCmd.Response; import com.github.dockerjava.api.model.AuthConfig; +import com.github.dockerjava.api.model.PushEventStreamItem; -public class PushImageCmdExec extends - AbstrDockerCmdExec implements - PushImageCmd.Exec { +public class PushImageCmdExec extends AbstrDockerCmdExec implements PushImageCmd.Exec { + private static final Logger LOGGER = LoggerFactory .getLogger(PushImageCmdExec.class); - + public PushImageCmdExec(WebTarget baseResource) { super(baseResource); } @Override - protected InputStream execute(PushImageCmd command) { - WebTarget webResource = getBaseResource().path( - "/images/" + name(command) + "/push").queryParam("tag", - command.getTag()); + protected ResponseImpl execute(PushImageCmd command) { + WebTarget webResource = getBaseResource().path("/images/" + name(command) + "/push") + .queryParam("tag", command.getTag()); final String registryAuth = registryAuth(command.getAuthConfig()); LOGGER.trace("POST: {}", webResource); - return webResource.request().header("X-Registry-Auth", registryAuth) - .accept(MediaType.APPLICATION_JSON).post(null) - .readEntity(InputStream.class); - } + InputStream is = webResource + .request() + .header("X-Registry-Auth", registryAuth) + .accept(MediaType.APPLICATION_JSON) + .post( + entity(Response.class, MediaType.APPLICATION_JSON)).readEntity( + InputStream.class); + return new ResponseImpl(is); + } + private String name(PushImageCmd command) { String name = command.getName(); AuthConfig authConfig = command.getAuthConfig(); return name.contains("/") ? name : authConfig.getUsername(); } + + public static class ResponseImpl extends Response { + + private final InputStream proxy; + + ResponseImpl(InputStream proxy) { + this.proxy = proxy; + } + + @Override + public Iterable getItems() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + // we'll be reading instances of MyBean + ObjectReader reader = mapper.reader(PushEventStreamItem.class); + // and then do other configuration, if any, and read: + Iterator items = reader.readValues(proxy); + + return ImmutableList.copyOf(items); + } + + @Override + public int read() throws IOException { + return proxy.read(); + } + } } diff --git a/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java b/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java index c63667a0d2..4fcf26b6ab 100644 --- a/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java +++ b/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java @@ -8,6 +8,7 @@ import com.github.dockerjava.api.command.*; import com.github.dockerjava.api.command.AuthCmd.Exec; +import com.github.dockerjava.jaxrs.BuildImageCmdExec; /** * Special {@link DockerCmdExecFactory} implementation that collects container and image creations @@ -92,18 +93,18 @@ public Void exec(RemoveImageCmd command) { public BuildImageCmd.Exec createBuildImageCmdExec() { return new BuildImageCmd.Exec() { @Override - public InputStream exec(BuildImageCmd command) { + public BuildImageCmd.Response exec(BuildImageCmd command) { // can't detect image id here so tagging it String tag = command.getTag(); if(tag == null || "".equals(tag.trim())) { tag = "" + new SecureRandom().nextInt(Integer.MAX_VALUE); command.withTag(tag); } - InputStream inputStream = delegate.createBuildImageCmdExec().exec(command); + InputStream inputStream = delegate.createBuildImageCmdExec().exec(command); imageNames.add(tag); - return inputStream; + return new BuildImageCmdExec.ResponseImpl(inputStream); } - }; + }; } @Override From f18697161e744463be0d033095b450eaf2598cf7 Mon Sep 17 00:00:00 2001 From: Nigel Magnay Date: Tue, 3 Feb 2015 13:58:09 +0000 Subject: [PATCH 3/4] Repoint to shaded, imported jersey.repackaged guava (!) Signed-off-by: Nigel Magnay --- .../java/com/github/dockerjava/api/model/EventStreamItem.java | 4 +++- .../com/github/dockerjava/api/model/PushEventStreamItem.java | 4 ++-- .../java/com/github/dockerjava/jaxrs/BuildImageCmdExec.java | 4 +++- .../java/com/github/dockerjava/jaxrs/PushImageCmdExec.java | 4 +++- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/github/dockerjava/api/model/EventStreamItem.java b/src/main/java/com/github/dockerjava/api/model/EventStreamItem.java index 98c61a8622..6e6ce035a3 100644 --- a/src/main/java/com/github/dockerjava/api/model/EventStreamItem.java +++ b/src/main/java/com/github/dockerjava/api/model/EventStreamItem.java @@ -1,12 +1,14 @@ package com.github.dockerjava.api.model; -import com.google.common.base.Objects; + import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.io.Serializable; +import jersey.repackaged.com.google.common.base.Objects; + /** * Represents an event stream */ diff --git a/src/main/java/com/github/dockerjava/api/model/PushEventStreamItem.java b/src/main/java/com/github/dockerjava/api/model/PushEventStreamItem.java index df7f59f50d..f5e845b29a 100644 --- a/src/main/java/com/github/dockerjava/api/model/PushEventStreamItem.java +++ b/src/main/java/com/github/dockerjava/api/model/PushEventStreamItem.java @@ -1,12 +1,12 @@ package com.github.dockerjava.api.model; -import com.google.common.base.Objects; - import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.io.Serializable; +import jersey.repackaged.com.google.common.base.Objects; + /** * Represents an item returned from push */ diff --git a/src/main/java/com/github/dockerjava/jaxrs/BuildImageCmdExec.java b/src/main/java/com/github/dockerjava/jaxrs/BuildImageCmdExec.java index 2b5d9a57ad..faf3d9f23f 100644 --- a/src/main/java/com/github/dockerjava/jaxrs/BuildImageCmdExec.java +++ b/src/main/java/com/github/dockerjava/jaxrs/BuildImageCmdExec.java @@ -1,6 +1,6 @@ package com.github.dockerjava.jaxrs; -import com.google.common.collect.ImmutableList; + import static javax.ws.rs.client.Entity.entity; @@ -24,6 +24,8 @@ import com.github.dockerjava.api.model.EventStreamItem; import com.github.dockerjava.api.model.PushEventStreamItem; +import jersey.repackaged.com.google.common.collect.ImmutableList; + public class BuildImageCmdExec extends AbstrDockerCmdExec implements BuildImageCmd.Exec { private static final Logger LOGGER = LoggerFactory diff --git a/src/main/java/com/github/dockerjava/jaxrs/PushImageCmdExec.java b/src/main/java/com/github/dockerjava/jaxrs/PushImageCmdExec.java index 838c9b89a6..152f31a3b7 100644 --- a/src/main/java/com/github/dockerjava/jaxrs/PushImageCmdExec.java +++ b/src/main/java/com/github/dockerjava/jaxrs/PushImageCmdExec.java @@ -1,6 +1,6 @@ package com.github.dockerjava.jaxrs; -import com.google.common.collect.ImmutableList; + import static javax.ws.rs.client.Entity.entity; @@ -21,6 +21,8 @@ import com.github.dockerjava.api.model.AuthConfig; import com.github.dockerjava.api.model.PushEventStreamItem; +// Shaded, but imported +import jersey.repackaged.com.google.common.collect.ImmutableList; public class PushImageCmdExec extends AbstrDockerCmdExec implements PushImageCmd.Exec { From b4d30f68f09587dbbd38dc4c2b98068f6e7dc57c Mon Sep 17 00:00:00 2001 From: Nigel Magnay Date: Tue, 3 Feb 2015 14:30:56 +0000 Subject: [PATCH 4/4] Update test to show typed API. Signed-off-by: Nigel Magnay --- .../core/command/BuildImageCmdImplTest.java | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/github/dockerjava/core/command/BuildImageCmdImplTest.java b/src/test/java/com/github/dockerjava/core/command/BuildImageCmdImplTest.java index 3692f56d14..8be2f8f8ea 100644 --- a/src/test/java/com/github/dockerjava/core/command/BuildImageCmdImplTest.java +++ b/src/test/java/com/github/dockerjava/core/command/BuildImageCmdImplTest.java @@ -27,8 +27,10 @@ import com.github.dockerjava.api.command.CreateContainerResponse; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.command.InspectImageResponse; +import com.github.dockerjava.api.model.EventStreamItem; import com.github.dockerjava.client.AbstractDockerClientTest; + @Test(groups = "integration") public class BuildImageCmdImplTest extends AbstractDockerClientTest { @@ -170,18 +172,24 @@ public void testDockerIgnore() throws DockerException, } @Test - public void testNetCatDockerfileBuilder() throws InterruptedException { + public void testNetCatDockerfileBuilder() throws InterruptedException, IOException { File baseDir = new File(Thread.currentThread().getContextClassLoader() .getResource("netcat").getFile()); - InputStream response = dockerClient.buildImageCmd(baseDir).withNoCache().exec(); + Iterable response = dockerClient.buildImageCmd(baseDir).withNoCache().exec().getItems(); - String fullLog = asString(response); + String imageId = null; + + for(EventStreamItem item : response) { + String text = item.getStream(); + if( text.startsWith("Successfully built ")) { + imageId = StringUtils.substringBetween(text, + "Successfully built ", "\n").trim(); + } + } - assertThat(fullLog, containsString("Successfully built")); + assertNotNull(imageId, "Not successful in build"); - String imageId = StringUtils.substringBetween(fullLog, - "Successfully built ", "\\n\"}").trim(); InspectImageResponse inspectImageResponse = dockerClient .inspectImageCmd(imageId).exec();