Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DockerHealthcheckWaitStrategy #618

Merged
merged 9 commits into from
Apr 3, 2018
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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 running != null && running; // avoid NPE when unboxing
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Boolean.TRUE.equals(running) ;)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thx, I think it's obvious that I'm not used to working with boxed primitives :D

} 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should add HEALTHCHECK support to the DSL?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitly, but another PR I assume?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, up to you

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)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to use Wait.forHealthcheck() here? It seems like the preferable style, so might be good to use it anywhere people might look for examples.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I'll change it.

}

@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