From 184ca99607d267693b5185bbfbf15aca6f461e39 Mon Sep 17 00:00:00 2001 From: Kevin Wittek Date: Thu, 22 Mar 2018 17:19:11 +0100 Subject: [PATCH 1/9] Add DockerHealthcheckWaitStrategy and isHealthy method to ContainerState --- CHANGELOG.md | 1 + .../containers/ContainerState.java | 13 +++++++ .../DockerHealthcheckWaitStrategy.java | 25 +++++++++++++ .../containers/wait/strategy/Wait.java | 9 +++++ .../DockerHealthcheckWaitStrategyTest.java | 36 +++++++++++++++++++ .../Dockerfile | 8 +++++ .../write_file_and_loop.sh | 8 +++++ 7 files changed, 100 insertions(+) create mode 100644 core/src/main/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategy.java create mode 100644 core/src/test/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategyTest.java create mode 100644 core/src/test/resources/health-wait-strategy-dockerfile/Dockerfile create mode 100755 core/src/test/resources/health-wait-strategy-dockerfile/write_file_and_loop.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index ef21e10f9cb..fe51554a54c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file. - Deprecated `WaitStrategy` and implementations in favour of classes with same names in `org.testcontainers.containers.strategy` ([\#600](https://github.com/testcontainers/testcontainers-java/pull/600)) - Added `ContainerState` interface representing the state of a started container ([\#600](https://github.com/testcontainers/testcontainers-java/pull/600)) - Added `WaitStrategyTarget` interface which is the target of the new `WaitStrategy` ([\#600](https://github.com/testcontainers/testcontainers-java/pull/600)) +- Added `DockerHealthcheckWaitStrategy` that is based on Docker's built-in [healthcheck](https://docs.docker.com/engine/reference/builder/#healthcheck). ## [1.6.0] - 2018-01-28 diff --git a/core/src/main/java/org/testcontainers/containers/ContainerState.java b/core/src/main/java/org/testcontainers/containers/ContainerState.java index 842a1cb6bde..a06b88603e4 100644 --- a/core/src/main/java/org/testcontainers/containers/ContainerState.java +++ b/core/src/main/java/org/testcontainers/containers/ContainerState.java @@ -15,6 +15,8 @@ public interface ContainerState { + String STATE_HEALTHY = "healthy"; + /** * Get the IP address that this container may be reached on (may not be the local machine). * @@ -35,6 +37,17 @@ default Boolean isRunning() { } } + /** + * @return has the container health state 'healthy'? + */ + default Boolean isHealthy() { + try { + return getContainerId() != null && DockerClientFactory.instance().client().inspectContainerCmd(getContainerId()).exec().getState().getHealth().getStatus().equals(STATE_HEALTHY); + } catch (DockerException e) { + return false; + } + } + /** * Get the actual mapped port for a first port exposed by the container. * diff --git a/core/src/main/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategy.java b/core/src/main/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategy.java new file mode 100644 index 00000000000..347153a151e --- /dev/null +++ b/core/src/main/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategy.java @@ -0,0 +1,25 @@ +package org.testcontainers.containers.wait.strategy; + +import org.rnorth.ducttape.TimeoutException; +import org.rnorth.ducttape.unreliables.Unreliables; +import org.testcontainers.containers.ContainerLaunchException; + +import java.util.concurrent.TimeUnit; + +/** + * Wait strategy leveraging Docker's built-in healthcheck mechanism. + * + * @see https://docs.docker.com/engine/reference/builder/#healthcheck + */ +public class DockerHealthcheckWaitStrategy extends AbstractWaitStrategy { + + @Override + protected void waitUntilReady() { + + try { + Unreliables.retryUntilTrue((int) startupTimeout.getSeconds(), TimeUnit.SECONDS, () -> waitStrategyTarget.isHealthy()); + } catch (TimeoutException e) { + throw new ContainerLaunchException("Timed out waiting for container to become healthy"); + } + } +} diff --git a/core/src/main/java/org/testcontainers/containers/wait/strategy/Wait.java b/core/src/main/java/org/testcontainers/containers/wait/strategy/Wait.java index 789927060b3..e839a11d91f 100644 --- a/core/src/main/java/org/testcontainers/containers/wait/strategy/Wait.java +++ b/core/src/main/java/org/testcontainers/containers/wait/strategy/Wait.java @@ -61,4 +61,13 @@ public static HttpWaitStrategy forHttps(String path) { public static LogMessageWaitStrategy forLogMessage(String regex, int times) { return new LogMessageWaitStrategy().withRegEx(regex).withTimes(times); } + + /** + * Convenience method to return a WaitStrategy leveraging Docker's built-in healthcheck. + * + * @return DockerHealthcheckWaitStrategy + */ + public static DockerHealthcheckWaitStrategy forHealthcheck() { + return new DockerHealthcheckWaitStrategy(); + } } diff --git a/core/src/test/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategyTest.java b/core/src/test/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategyTest.java new file mode 100644 index 00000000000..e63fb17bccf --- /dev/null +++ b/core/src/test/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategyTest.java @@ -0,0 +1,36 @@ +package org.testcontainers.containers.wait.strategy; + +import org.junit.Before; +import org.junit.Test; +import org.testcontainers.containers.ContainerLaunchException; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.images.builder.ImageFromDockerfile; + +import java.time.Duration; + +import static org.rnorth.visibleassertions.VisibleAssertions.assertThrows; + +public class DockerHealthcheckWaitStrategyTest { + + private GenericContainer container; + + @Before + public void setUp() { + // Using a Dockerfile here, since Dockerfile builder DSL doesn't support HEALTHCHECK + container = new GenericContainer(new ImageFromDockerfile() + .withFileFromClasspath("write_file_and_loop.sh", "health-wait-strategy-dockerfile/write_file_and_loop.sh") + .withFileFromClasspath("Dockerfile", "health-wait-strategy-dockerfile/Dockerfile")) + .waitingFor(new DockerHealthcheckWaitStrategy().withStartupTimeout(Duration.ofSeconds(3))); + } + + @Test + public void startsOnceHealthy() { + container.start(); + } + + @Test + public void containerStartFailsIfContainerIsUnhealthy() { + container.withCommand("ash"); + assertThrows("Container launch fails when unhealthy", ContainerLaunchException.class, container::start); + } +} diff --git a/core/src/test/resources/health-wait-strategy-dockerfile/Dockerfile b/core/src/test/resources/health-wait-strategy-dockerfile/Dockerfile new file mode 100644 index 00000000000..867860a1634 --- /dev/null +++ b/core/src/test/resources/health-wait-strategy-dockerfile/Dockerfile @@ -0,0 +1,8 @@ +FROM alpine:latest + +HEALTHCHECK --interval=1s CMD test -e /testfile + +ADD write_file_and_loop.sh write_file_and_loop.sh +RUN chmod +x write_file_and_loop.sh + +CMD ["/write_file_and_loop.sh"] diff --git a/core/src/test/resources/health-wait-strategy-dockerfile/write_file_and_loop.sh b/core/src/test/resources/health-wait-strategy-dockerfile/write_file_and_loop.sh new file mode 100755 index 00000000000..a102129394b --- /dev/null +++ b/core/src/test/resources/health-wait-strategy-dockerfile/write_file_and_loop.sh @@ -0,0 +1,8 @@ +#!/bin/ash + +echo sleeping +sleep 2 +echo writing file +touch /testfile + +while true; do sleep 1; done From 5dd62298c568904bd2987af9a31fc8dc938d5858 Mon Sep 17 00:00:00 2001 From: Kevin Wittek Date: Mon, 26 Mar 2018 20:25:52 +0200 Subject: [PATCH 2/9] Add PR link to changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe51554a54c..0f331139416 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ All notable changes to this project will be documented in this file. - Deprecated `WaitStrategy` and implementations in favour of classes with same names in `org.testcontainers.containers.strategy` ([\#600](https://github.com/testcontainers/testcontainers-java/pull/600)) - Added `ContainerState` interface representing the state of a started container ([\#600](https://github.com/testcontainers/testcontainers-java/pull/600)) - Added `WaitStrategyTarget` interface which is the target of the new `WaitStrategy` ([\#600](https://github.com/testcontainers/testcontainers-java/pull/600)) -- Added `DockerHealthcheckWaitStrategy` that is based on Docker's built-in [healthcheck](https://docs.docker.com/engine/reference/builder/#healthcheck). +- Added `DockerHealthcheckWaitStrategy` that is based on Docker's built-in [healthcheck](https://docs.docker.com/engine/reference/builder/#healthcheck) ([\#618](https://github.com/testcontainers/testcontainers-java/pull/618)). ## [1.6.0] - 2018-01-28 From 98ef3ddc18096de43fe3de7626235fb8ee849f6f Mon Sep 17 00:00:00 2001 From: Kevin Wittek Date: Mon, 26 Mar 2018 20:26:16 +0200 Subject: [PATCH 3/9] Use COPY instead of ADD in Dockerfile for healthstrategy check --- .../test/resources/health-wait-strategy-dockerfile/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/test/resources/health-wait-strategy-dockerfile/Dockerfile b/core/src/test/resources/health-wait-strategy-dockerfile/Dockerfile index 867860a1634..5b6ee6ad92d 100644 --- a/core/src/test/resources/health-wait-strategy-dockerfile/Dockerfile +++ b/core/src/test/resources/health-wait-strategy-dockerfile/Dockerfile @@ -2,7 +2,7 @@ FROM alpine:latest HEALTHCHECK --interval=1s CMD test -e /testfile -ADD write_file_and_loop.sh write_file_and_loop.sh +COPY write_file_and_loop.sh write_file_and_loop.sh RUN chmod +x write_file_and_loop.sh CMD ["/write_file_and_loop.sh"] From 5dc9bd1e770c1ab77bdb2ea9f56211e6c59ab76f Mon Sep 17 00:00:00 2001 From: Kevin Wittek Date: Mon, 26 Mar 2018 20:26:43 +0200 Subject: [PATCH 4/9] Use method reference in DockerHealthcheckWaitStrategy --- .../containers/wait/strategy/DockerHealthcheckWaitStrategy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategy.java b/core/src/main/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategy.java index 347153a151e..f2fa797327b 100644 --- a/core/src/main/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategy.java +++ b/core/src/main/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategy.java @@ -17,7 +17,7 @@ public class DockerHealthcheckWaitStrategy extends AbstractWaitStrategy { protected void waitUntilReady() { try { - Unreliables.retryUntilTrue((int) startupTimeout.getSeconds(), TimeUnit.SECONDS, () -> waitStrategyTarget.isHealthy()); + Unreliables.retryUntilTrue((int) startupTimeout.getSeconds(), TimeUnit.SECONDS, waitStrategyTarget::isHealthy); } catch (TimeoutException e) { throw new ContainerLaunchException("Timed out waiting for container to become healthy"); } From e92538f1d8cb1446763a86ae11ef8d80b10301a7 Mon Sep 17 00:00:00 2001 From: Kevin Wittek Date: Mon, 26 Mar 2018 20:27:11 +0200 Subject: [PATCH 5/9] Use long running command in DockerHealthcheckWaitStrategyTest --- .../wait/strategy/DockerHealthcheckWaitStrategyTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/test/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategyTest.java b/core/src/test/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategyTest.java index e63fb17bccf..796cbbe5251 100644 --- a/core/src/test/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategyTest.java +++ b/core/src/test/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategyTest.java @@ -30,7 +30,7 @@ public void startsOnceHealthy() { @Test public void containerStartFailsIfContainerIsUnhealthy() { - container.withCommand("ash"); + container.withCommand("tail", "-f", "/dev/null"); assertThrows("Container launch fails when unhealthy", ContainerLaunchException.class, container::start); } } From f03659a6bfbc06b35fd44840c9599da72b758dfa Mon Sep 17 00:00:00 2001 From: Kevin Wittek Date: Mon, 26 Mar 2018 20:28:18 +0200 Subject: [PATCH 6/9] Pin Alpine image for healthcheck test to 3.7 --- .../test/resources/health-wait-strategy-dockerfile/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/test/resources/health-wait-strategy-dockerfile/Dockerfile b/core/src/test/resources/health-wait-strategy-dockerfile/Dockerfile index 5b6ee6ad92d..ddb0f82bce7 100644 --- a/core/src/test/resources/health-wait-strategy-dockerfile/Dockerfile +++ b/core/src/test/resources/health-wait-strategy-dockerfile/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:latest +FROM alpine:3.7 HEALTHCHECK --interval=1s CMD test -e /testfile From c8fcbacdbbeaab659ee16bfb3d9f14d372536e4f Mon Sep 17 00:00:00 2001 From: Kevin Wittek Date: Mon, 26 Mar 2018 20:32:46 +0200 Subject: [PATCH 7/9] Refactor ContainerState --- .../containers/ContainerState.java | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/testcontainers/containers/ContainerState.java b/core/src/main/java/org/testcontainers/containers/ContainerState.java index a06b88603e4..a4e4473f25f 100644 --- a/core/src/main/java/org/testcontainers/containers/ContainerState.java +++ b/core/src/main/java/org/testcontainers/containers/ContainerState.java @@ -29,9 +29,14 @@ default String getContainerIpAddress() { /** * @return is the container currently running? */ - default Boolean isRunning() { + default boolean isRunning() { + if (getContainerId() == null) { + return false; + } + try { - return getContainerId() != null && DockerClientFactory.instance().client().inspectContainerCmd(getContainerId()).exec().getState().getRunning(); + Boolean running = getCurrentContainerInfo().getState().getRunning(); + return running != null && running; // avoid NPE when unboxing } catch (DockerException e) { return false; } @@ -40,14 +45,25 @@ default Boolean isRunning() { /** * @return has the container health state 'healthy'? */ - default Boolean isHealthy() { + default boolean isHealthy() { + if (getContainerId() == null) { + return false; + } + try { - return getContainerId() != null && DockerClientFactory.instance().client().inspectContainerCmd(getContainerId()).exec().getState().getHealth().getStatus().equals(STATE_HEALTHY); + InspectContainerResponse inspectContainerResponse = getCurrentContainerInfo(); + String healthStatus = inspectContainerResponse.getState().getHealth().getStatus(); + + return healthStatus.equals(STATE_HEALTHY); } catch (DockerException e) { return false; } } + default InspectContainerResponse getCurrentContainerInfo() { + return DockerClientFactory.instance().client().inspectContainerCmd(getContainerId()).exec(); + } + /** * Get the actual mapped port for a first port exposed by the container. * From 4de4253af24a7100d37d79917592d30c530ec76b Mon Sep 17 00:00:00 2001 From: Kevin Wittek Date: Mon, 26 Mar 2018 22:32:04 +0200 Subject: [PATCH 8/9] Small refactorings after review --- .../main/java/org/testcontainers/containers/ContainerState.java | 2 +- .../wait/strategy/DockerHealthcheckWaitStrategyTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/testcontainers/containers/ContainerState.java b/core/src/main/java/org/testcontainers/containers/ContainerState.java index a4e4473f25f..4fc46c64989 100644 --- a/core/src/main/java/org/testcontainers/containers/ContainerState.java +++ b/core/src/main/java/org/testcontainers/containers/ContainerState.java @@ -36,7 +36,7 @@ default boolean isRunning() { try { Boolean running = getCurrentContainerInfo().getState().getRunning(); - return running != null && running; // avoid NPE when unboxing + return Boolean.TRUE.equals(running); } catch (DockerException e) { return false; } diff --git a/core/src/test/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategyTest.java b/core/src/test/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategyTest.java index 796cbbe5251..d867233f5d7 100644 --- a/core/src/test/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategyTest.java +++ b/core/src/test/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategyTest.java @@ -20,7 +20,7 @@ public void setUp() { container = new GenericContainer(new ImageFromDockerfile() .withFileFromClasspath("write_file_and_loop.sh", "health-wait-strategy-dockerfile/write_file_and_loop.sh") .withFileFromClasspath("Dockerfile", "health-wait-strategy-dockerfile/Dockerfile")) - .waitingFor(new DockerHealthcheckWaitStrategy().withStartupTimeout(Duration.ofSeconds(3))); + .waitingFor(Wait.forHealthcheck().withStartupTimeout(Duration.ofSeconds(3))); } @Test From 79cc554463e1cffd07b072f4429dcbeaacbe60cd Mon Sep 17 00:00:00 2001 From: Kevin Wittek Date: Mon, 26 Mar 2018 22:32:42 +0200 Subject: [PATCH 9/9] Add documentation and example for healthcheck strategy --- docs/usage/options.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/usage/options.md b/docs/usage/options.md index 83211cc8f64..232cedb3780 100644 --- a/docs/usage/options.md +++ b/docs/usage/options.md @@ -109,6 +109,14 @@ public static GenericContainer elasticsearch = .usingTls()); ``` +If the used image supports Docker's [Healthcheck](https://docs.docker.com/engine/reference/builder/#healthcheck) feature, you can directly leverage the `healthy` state of the container as your wait condition: +```java +@ClassRule2.32.3 +public static GenericContainer container = + new GenericContainer("image-with-healthcheck:4.2") + .waitingFor(Wait.forHealthcheck()); +``` + For futher options, check out the `Wait` convenience class, or the various subclasses of `WaitStrategy`. If none of these options meet your requirements, you can create your own subclass of `AbstractWaitStrategy` with an appropriate wait mechanism in `waitUntilReady()`. The `GenericContainer.waitingFor()` method accepts any valid `WaitStrategy`.