diff --git a/pom.xml b/pom.xml index 99a6c603e..d8f1fbac6 100644 --- a/pom.xml +++ b/pom.xml @@ -63,7 +63,7 @@ 1.7.5 1.3.9 0.3 - 11.0.1 + 18.0 1.0.1 diff --git a/src/main/java/com/github/dockerjava/api/DockerClient.java b/src/main/java/com/github/dockerjava/api/DockerClient.java index 15e66bef8..52323942f 100644 --- a/src/main/java/com/github/dockerjava/api/DockerClient.java +++ b/src/main/java/com/github/dockerjava/api/DockerClient.java @@ -10,6 +10,8 @@ import com.github.dockerjava.api.command.CopyFileFromContainerCmd; import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.command.CreateImageCmd; +import com.github.dockerjava.api.command.EventCallback; +import com.github.dockerjava.api.command.EventsCmd; import com.github.dockerjava.api.command.InfoCmd; import com.github.dockerjava.api.command.InspectContainerCmd; import com.github.dockerjava.api.command.InspectImageCmd; @@ -114,6 +116,8 @@ public CopyFileFromContainerCmd copyFileFromContainerCmd( public UnpauseContainerCmd unpauseContainerCmd(String containerId); + public EventsCmd eventsCmd(EventCallback eventCallback); + public void close() throws IOException; } \ No newline at end of file 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 6534bb547..24c0a4650 100644 --- a/src/main/java/com/github/dockerjava/api/command/DockerCmdExecFactory.java +++ b/src/main/java/com/github/dockerjava/api/command/DockerCmdExecFactory.java @@ -69,6 +69,8 @@ public interface DockerCmdExecFactory extends Closeable { public UnpauseContainerCmd.Exec createUnpauseContainerCmdExec(); + public EventsCmd.Exec createEventsCmdExec(); + public void close() throws IOException; } \ No newline at end of file diff --git a/src/main/java/com/github/dockerjava/api/command/EventCallback.java b/src/main/java/com/github/dockerjava/api/command/EventCallback.java new file mode 100644 index 000000000..009cc5998 --- /dev/null +++ b/src/main/java/com/github/dockerjava/api/command/EventCallback.java @@ -0,0 +1,10 @@ +package com.github.dockerjava.api.command; + +import com.github.dockerjava.api.model.Event; + +/** + * Event callback + */ +public interface EventCallback { + public void onEvent(Event event); +} diff --git a/src/main/java/com/github/dockerjava/api/command/EventsCmd.java b/src/main/java/com/github/dockerjava/api/command/EventsCmd.java new file mode 100644 index 000000000..cfdb23a64 --- /dev/null +++ b/src/main/java/com/github/dockerjava/api/command/EventsCmd.java @@ -0,0 +1,27 @@ +package com.github.dockerjava.api.command; + +import java.util.concurrent.ExecutorService; + + +/** + * Get events + * + * @param since - Show all events created since timestamp + * @param until - Stream events until this timestamp + */ +public interface EventsCmd extends DockerCmd { + public EventsCmd withSince(String since); + + public EventsCmd withUntil(String until); + + public String getSince(); + + public String getUntil(); + + public EventCallback getEventCallback(); + + public EventsCmd withEventCallback(EventCallback eventCallback); + + public static interface Exec extends DockerCmdExec { + } +} diff --git a/src/main/java/com/github/dockerjava/api/model/Event.java b/src/main/java/com/github/dockerjava/api/model/Event.java new file mode 100644 index 000000000..674cb66de --- /dev/null +++ b/src/main/java/com/github/dockerjava/api/model/Event.java @@ -0,0 +1,37 @@ +package com.github.dockerjava.api.model; + +import org.apache.commons.lang.builder.ToStringBuilder; + +/** + * Representation of a Docker event. + */ +public class Event { + private String status; + + private String id; + + private String from; + + private long time; + + public String getStatus() { + return status; + } + + public String getId() { + return id; + } + + public String getFrom() { + return from; + } + + public long getTime() { + return time; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this); + } +} diff --git a/src/main/java/com/github/dockerjava/api/model/EventNotifier.java b/src/main/java/com/github/dockerjava/api/model/EventNotifier.java new file mode 100644 index 000000000..39d42931e --- /dev/null +++ b/src/main/java/com/github/dockerjava/api/model/EventNotifier.java @@ -0,0 +1,52 @@ +package com.github.dockerjava.api.model; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.dockerjava.api.command.EventCallback; +import com.google.common.base.Preconditions; + +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; +import java.io.InputStream; +import java.util.concurrent.Callable; + +/** + * EventNotifier API + */ +public class EventNotifier implements Callable { + private static final JsonFactory JSON_FACTORY = new JsonFactory(); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private final EventCallback eventCallback; + private final WebTarget webTarget; + + private EventNotifier(EventCallback eventCallback, WebTarget webTarget) { + this.eventCallback = eventCallback; + this.webTarget = webTarget; + } + + public static EventNotifier create(EventCallback eventCallback, WebTarget webTarget) { + Preconditions.checkNotNull(eventCallback, "An EventCallback must be provided"); + Preconditions.checkNotNull(webTarget, "An WebTarget must be provided"); + return new EventNotifier(eventCallback, webTarget); + } + + @Override + public Void call() throws Exception { + Response response = webTarget.request().get(Response.class); + InputStream inputStream = response.readEntity(InputStream.class); + try { + JsonParser jp = JSON_FACTORY.createParser(inputStream); + while (jp.nextToken() != JsonToken.END_OBJECT && !jp.isClosed()) { + eventCallback.onEvent(OBJECT_MAPPER.readValue(jp, Event.class)); + } + } finally { + if (response != null) { + response.close(); + } + } + return null; + } +} 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 f71b75c93..7fe6ae798 100644 --- a/src/main/java/com/github/dockerjava/api/model/Info.java +++ b/src/main/java/com/github/dockerjava/api/model/Info.java @@ -10,7 +10,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -/**Ø +/** * * @author Konstantin Pelykh (kpelykh@gmail.com) * diff --git a/src/main/java/com/github/dockerjava/core/DockerClientImpl.java b/src/main/java/com/github/dockerjava/core/DockerClientImpl.java index baa5b5da5..5dfb4c13f 100644 --- a/src/main/java/com/github/dockerjava/core/DockerClientImpl.java +++ b/src/main/java/com/github/dockerjava/core/DockerClientImpl.java @@ -252,6 +252,11 @@ public UnpauseContainerCmd unpauseContainerCmd(String containerId) { return new UnpauseContainerCmdImpl(getDockerCmdExecFactory().createUnpauseContainerCmdExec(), containerId); } + @Override + public EventsCmd eventsCmd(EventCallback eventCallback) { + return new EventsCmdImpl(getDockerCmdExecFactory().createEventsCmdExec(), eventCallback); + } + @Override public void close() throws IOException { getDockerCmdExecFactory().close(); diff --git a/src/main/java/com/github/dockerjava/core/command/EventsCmdImpl.java b/src/main/java/com/github/dockerjava/core/command/EventsCmdImpl.java new file mode 100644 index 000000000..ac55de714 --- /dev/null +++ b/src/main/java/com/github/dockerjava/core/command/EventsCmdImpl.java @@ -0,0 +1,67 @@ +package com.github.dockerjava.core.command; + +import java.util.concurrent.ExecutorService; + +import com.github.dockerjava.api.command.EventCallback; +import com.github.dockerjava.api.command.EventsCmd; + +/** + * Stream docker events + */ +public class EventsCmdImpl extends AbstrDockerCmd implements EventsCmd { + + private String since; + private String until; + private EventCallback eventCallback; + + public EventsCmdImpl(EventsCmd.Exec exec, EventCallback eventCallback) { + super(exec); + withEventCallback(eventCallback); + } + + @Override + public EventsCmd withSince(String since) { + this.since = since; + return this; + } + + @Override + public EventsCmd withUntil(String until) { + this.until = until; + return this; + } + + @Override + public EventsCmd withEventCallback(EventCallback eventCallback) { + this.eventCallback = eventCallback; + return this; + } + + @Override + public String getSince() { + return since; + } + + @Override + public String getUntil() { + return until; + } + + @Override + public EventCallback getEventCallback() { + return eventCallback; + } + + @Override + public ExecutorService exec() { + return super.exec(); + } + + @Override + public String toString() { + return new StringBuilder("events") + .append(since != null ? " --since=" + since : "") + .append(until != null ? " --until=" + until : "") + .toString(); + } +} diff --git a/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java b/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java index 8ba7edd5f..cb59f200b 100644 --- a/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java +++ b/src/main/java/com/github/dockerjava/jaxrs/DockerCmdExecFactoryImpl.java @@ -7,6 +7,7 @@ import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.WebTarget; +import com.github.dockerjava.api.command.EventsCmd; import org.glassfish.jersey.client.ClientConfig; import org.glassfish.jersey.client.ClientProperties; @@ -241,7 +242,12 @@ public PauseContainerCmd.Exec createPauseContainerCmdExec() { public UnpauseContainerCmd.Exec createUnpauseContainerCmdExec() { return new UnpauseContainerCmdExec(baseResource); } - + + @Override + public EventsCmd.Exec createEventsCmdExec() { + return new EventsCmdExec(getBaseResource()); + } + @Override public void close() throws IOException { Preconditions.checkNotNull(client, "Factory not initialized. You probably forgot to call init()!"); diff --git a/src/main/java/com/github/dockerjava/jaxrs/EventsCmdExec.java b/src/main/java/com/github/dockerjava/jaxrs/EventsCmdExec.java new file mode 100644 index 000000000..63db6772a --- /dev/null +++ b/src/main/java/com/github/dockerjava/jaxrs/EventsCmdExec.java @@ -0,0 +1,80 @@ +package com.github.dockerjava.jaxrs; + +import java.io.InputStream; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.dockerjava.api.command.EventCallback; +import com.github.dockerjava.api.command.EventsCmd; +import com.github.dockerjava.api.model.Event; +import com.github.dockerjava.api.model.EventNotifier; +import com.google.common.base.Preconditions; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +public class EventsCmdExec extends AbstrDockerCmdExec implements EventsCmd.Exec { + private static final Logger LOGGER = LoggerFactory.getLogger(EventsCmdExec.class); + + public EventsCmdExec(WebTarget baseResource) { + super(baseResource); + } + + @Override + protected ExecutorService execute(EventsCmd command) { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + + WebTarget webResource = getBaseResource().path("/events") + .queryParam("since", command.getSince()) + .queryParam("until", command.getUntil()); + + LOGGER.trace("GET: {}", webResource); + EventNotifier eventNotifier = EventNotifier.create(command.getEventCallback(), webResource); + executorService.submit(eventNotifier); + return executorService; + } + + private static class EventNotifier implements Callable { + private static final JsonFactory JSON_FACTORY = new JsonFactory(); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private final EventCallback eventCallback; + private final WebTarget webTarget; + + private EventNotifier(EventCallback eventCallback, WebTarget webTarget) { + this.eventCallback = eventCallback; + this.webTarget = webTarget; + } + + public static EventNotifier create(EventCallback eventCallback, WebTarget webTarget) { + Preconditions.checkNotNull(eventCallback, "An EventCallback must be provided"); + Preconditions.checkNotNull(webTarget, "An WebTarget must be provided"); + return new EventNotifier(eventCallback, webTarget); + } + + @Override + public Void call() throws Exception { + Response response = webTarget.request().get(Response.class); + InputStream inputStream = response.readEntity(InputStream.class); + try { + JsonParser jp = JSON_FACTORY.createParser(inputStream); + while (jp.nextToken() != JsonToken.END_OBJECT && !jp.isClosed()) { + eventCallback.onEvent(OBJECT_MAPPER.readValue(jp, Event.class)); + } + } finally { + if (response != null) { + response.close(); + } + } + return null; + } + } +} diff --git a/src/test/java/com/github/dockerjava/core/command/EventsCmdImplTest.java b/src/test/java/com/github/dockerjava/core/command/EventsCmdImplTest.java new file mode 100644 index 000000000..6d3651c07 --- /dev/null +++ b/src/test/java/com/github/dockerjava/core/command/EventsCmdImplTest.java @@ -0,0 +1,116 @@ +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.EventCallback; +import com.github.dockerjava.api.command.EventsCmd; +import com.github.dockerjava.api.model.Event; +import com.github.dockerjava.client.AbstractDockerClientTest; + +import org.testng.ITestResult; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +public class EventsCmdImplTest extends AbstractDockerClientTest { + + private static int KNOWN_NUM_EVENTS = 4; + + private static String getEpochTime() { + return String.valueOf(System.currentTimeMillis() / 1000); + } + + @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 testEventStreamTimeBound() throws InterruptedException, IOException { + // Don't include other tests events + TimeUnit.SECONDS.sleep(1); + + String startTime = getEpochTime(); + int expectedEvents = generateEvents(); + String endTime = getEpochTime(); + + CountDownLatch countDownLatch = new CountDownLatch(expectedEvents); + EventCallback eventCallback = new EventCallbackTest(countDownLatch); + + EventsCmd eventsCmd = dockerClient.eventsCmd(eventCallback).withSince(startTime).withUntil(endTime); + ExecutorService executorService = eventsCmd.exec(); + + boolean zeroCount = countDownLatch.await(5, TimeUnit.SECONDS); + + executorService.shutdown(); + assertTrue(zeroCount, "Expected 4 events, [create, start, die, stop]"); + } + + @Test + public void testEventStreaming() throws InterruptedException, IOException { + // Don't include other tests events + TimeUnit.SECONDS.sleep(1); + + CountDownLatch countDownLatch = new CountDownLatch(KNOWN_NUM_EVENTS); + EventCallback eventCallback = new EventCallbackTest(countDownLatch); + + EventsCmd eventsCmd = dockerClient.eventsCmd(eventCallback).withSince(getEpochTime()); + ExecutorService executorService = eventsCmd.exec(); + + generateEvents(); + + boolean zeroCount = countDownLatch.await(5, TimeUnit.SECONDS); + executorService.shutdown(); + assertTrue(zeroCount, "Expected 4 events, [create, start, die, stop]"); + } + + /** + * This method generates {#link KNOWN_NUM_EVENTS} events + */ + private int generateEvents() { + String testImage = "busybox"; + asString(dockerClient.pullImageCmd(testImage).exec()); + CreateContainerResponse container = dockerClient + .createContainerCmd(testImage).withCmd("echo").exec(); + dockerClient.startContainerCmd(container.getId()).exec(); + dockerClient.stopContainerCmd(container.getId()).exec(); + return KNOWN_NUM_EVENTS; + } + + private class EventCallbackTest implements EventCallback { + private final CountDownLatch countDownLatch; + + public EventCallbackTest(CountDownLatch countDownLatch) { + this.countDownLatch = countDownLatch; + } + + @Override + public void onEvent(Event event) { + LOG.info("Received event #{}: {}", countDownLatch.getCount(), event); + countDownLatch.countDown(); + } + } +}