diff --git a/CHANGELOG.md b/CHANGELOG.md index ef21e10f9cb..0f331139416 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) ([\#618](https://github.com/testcontainers/testcontainers-java/pull/618)). ## [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..4fc46c64989 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). * @@ -27,14 +29,41 @@ default String getContainerIpAddress() { /** * @return is the container currently running? */ - default Boolean isRunning() { + default boolean isRunning() { + if (getContainerId() == null) { + return false; + } + + try { + Boolean running = getCurrentContainerInfo().getState().getRunning(); + return Boolean.TRUE.equals(running); + } catch (DockerException e) { + return false; + } + } + + /** + * @return has the container health state 'healthy'? + */ + default boolean isHealthy() { + if (getContainerId() == null) { + return false; + } + try { - return getContainerId() != null && DockerClientFactory.instance().client().inspectContainerCmd(getContainerId()).exec().getState().getRunning(); + 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. * 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..f2fa797327b --- /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..d867233f5d7 --- /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(Wait.forHealthcheck().withStartupTimeout(Duration.ofSeconds(3))); + } + + @Test + public void startsOnceHealthy() { + container.start(); + } + + @Test + public void containerStartFailsIfContainerIsUnhealthy() { + container.withCommand("tail", "-f", "/dev/null"); + 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..ddb0f82bce7 --- /dev/null +++ b/core/src/test/resources/health-wait-strategy-dockerfile/Dockerfile @@ -0,0 +1,8 @@ +FROM alpine:3.7 + +HEALTHCHECK --interval=1s CMD test -e /testfile + +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"] 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 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`.