Skip to content

Commit

Permalink
Add DockerHealthcheckWaitStrategy (#618)
Browse files Browse the repository at this point in the history
Also adds a isHealthy() method to the ContainerState interface.
  • Loading branch information
kiview authored Apr 3, 2018
1 parent 23478fa commit ac1d4af
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*
Expand All @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <a href="https://docs.docker.com/engine/reference/builder/#healthcheck">https://docs.docker.com/engine/reference/builder/#healthcheck</a>
*/
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");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/ash

echo sleeping
sleep 2
echo writing file
touch /testfile

while true; do sleep 1; done
8 changes: 8 additions & 0 deletions docs/usage/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down

0 comments on commit ac1d4af

Please sign in to comment.