Skip to content

Commit

Permalink
Merge pull request #155 from palantir/native.health.checks
Browse files Browse the repository at this point in the history
Support native docker healthchecks
  • Loading branch information
alicederyn authored Feb 21, 2017
2 parents b6d206c + f648997 commit 19d88e6
Show file tree
Hide file tree
Showing 24 changed files with 401 additions and 154 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Docker Compose JUnit Rule
=========================

This is a small library for executing JUnit tests that interact with Docker Compose managed containers. It supports the following:
This is a library for executing JUnit tests that interact with Docker Compose managed containers. It supports the following:

- Starting containers defined in a docker-compose.yml before tests and tearing them down afterwards
- Waiting for services to become available before running tests
Expand Down Expand Up @@ -55,7 +55,7 @@ public class MyIntegrationTest {
}
```

This will cause the containers defined in `src/test/resources/docker-compose.yml` to be started by Docker Compose before the test executes and then the containers will be killed and removed (along with associated volumes) once the test has finished executing.
This will cause the containers defined in `src/test/resources/docker-compose.yml` to be started by Docker Compose before the test executes and then the containers will be killed and removed (along with associated volumes) once the test has finished executing. If the containers have healthchecks specified, either [in the docker image](https://docs.docker.com/engine/reference/builder/#/healthcheck) or [in the docker-compose config](https://docs.docker.com/compose/compose-file/#/healthcheck), the test will wait for them to become healthy.

The `docker-compose.yml` file is referenced using the path given, relative to the working directory of the test. It will not be copied elsewhere and so references to shared directories and other resources for your containers can be made using path relative to this file as normal. If you wish to manually run the Docker containers for debugging the tests simply run `docker-compose up` in the same directory as the `docker-compose.yml`.

Expand All @@ -70,7 +70,7 @@ To run the tests from your IDE you will need to add the environment variables gi
Waiting for a service to be available
-------------------------------------

To wait for services to be available before executing tests use the following methods on the `DockerComposeRule` object:
To wait for services to be available before executing tests, either add health checks to the configuration, or use the following methods on the `DockerComposeRule` object:

```java
public class MyEndToEndTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

import static java.util.stream.Collectors.toList;

import java.io.IOException;
import java.util.List;
import java.util.Set;
import org.immutables.value.Value;

@Value.Immutable
Expand All @@ -25,4 +27,8 @@ public List<Container> containers(List<String> containerNames) {
.collect(toList());
}

public Set<Container> allContainers() throws IOException, InterruptedException {
return containerCache().containers();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.google.common.base.Suppliers;
import com.google.common.base.Throwables;
import com.palantir.docker.compose.connection.waiting.SuccessOrFailure;
import com.palantir.docker.compose.execution.Docker;
import com.palantir.docker.compose.execution.DockerCompose;
import java.io.IOException;
import java.util.List;
Expand All @@ -29,13 +30,15 @@
public class Container {

private final String containerName;
private final DockerCompose dockerComposeProcess;
private final Docker docker;
private final DockerCompose dockerCompose;

private final Supplier<Ports> portMappings = Suppliers.memoize(this::getDockerPorts);

public Container(String containerName, DockerCompose dockerComposeProcess) {
public Container(String containerName, Docker docker, DockerCompose dockerCompose) {
this.containerName = containerName;
this.dockerComposeProcess = dockerComposeProcess;
this.docker = docker;
this.dockerCompose = dockerCompose;
}

public String getContainerName() {
Expand Down Expand Up @@ -90,28 +93,36 @@ public DockerPort port(int internalPort) {
}

public void start() throws IOException, InterruptedException {
dockerComposeProcess.start(this);
dockerCompose.start(this);
}

public void stop() throws IOException, InterruptedException {
dockerComposeProcess.stop(this);
dockerCompose.stop(this);
}

public void kill() throws IOException, InterruptedException {
dockerComposeProcess.kill(this);
dockerCompose.kill(this);
}

public State state() throws IOException, InterruptedException {
return dockerComposeProcess.state(containerName);
String id = dockerCompose.id(this).orElse(null);
if (id == null) {
return State.DOWN;
}
return docker.state(id);
}

public void up() throws IOException, InterruptedException {
dockerComposeProcess.up(this);
dockerCompose.up(this);
}

public Ports ports() {
return portMappings.get();
}

private Ports getDockerPorts() {
try {
return dockerComposeProcess.ports(containerName);
return dockerCompose.ports(containerName);
} catch (IOException | InterruptedException e) {
throw Throwables.propagate(e);
}
Expand All @@ -136,7 +147,7 @@ public int hashCode() {

@Override
public String toString() {
return "Container{containerName='" + containerName + "}";
return "Container{containerName='" + containerName + "'}";
}

public SuccessOrFailure areAllPortsOpen() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,33 @@
*/
package com.palantir.docker.compose.connection;

import static java.util.stream.Collectors.toSet;

import com.palantir.docker.compose.execution.Docker;
import com.palantir.docker.compose.execution.DockerCompose;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class ContainerCache {

private final Map<String, Container> containers = new HashMap<>();
private final Docker docker;
private final DockerCompose dockerCompose;

public ContainerCache(DockerCompose dockerCompose) {
public ContainerCache(Docker docker, DockerCompose dockerCompose) {
this.docker = docker;
this.dockerCompose = dockerCompose;
}

public Container container(String containerName) {
containers.putIfAbsent(containerName, dockerCompose.container(containerName));
containers.putIfAbsent(containerName, new Container(containerName, docker, dockerCompose));
return containers.get(containerName);
}

public Set<Container> containers() throws IOException, InterruptedException {
return dockerCompose.services().stream().map(this::container).collect(toSet());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,21 @@
*/
package com.palantir.docker.compose.connection;

import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public enum State {
Up, Exit;
DOWN, PAUSED, UNHEALTHY, HEALTHY;

/** Returns true if the container is up, unpaused and healthy. */
public boolean isHealthy() {
return this == HEALTHY;
}

private static final Pattern STATE_PATTERN = Pattern.compile("(Up|Exit)");
private static final int STATE_INDEX = 1;
/** Returns true if the container is up but not necessarily unpaused or healthy. */
public boolean isUp() {
return this != DOWN;
}

public static State parseFromDockerComposePs(String psOutput) {
Preconditions.checkArgument(!Strings.isNullOrEmpty(psOutput), "No container found");
Matcher matcher = STATE_PATTERN.matcher(psOutput);
Preconditions.checkState(matcher.find(), "Could not parse status: %s", psOutput);
String matchedStatus = matcher.group(STATE_INDEX);
return valueOf(matchedStatus);
/** Returns true if the container is paused. */
public boolean isPaused() {
return this == PAUSED;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,15 @@
*/
package com.palantir.docker.compose.connection.waiting;

import static java.util.stream.Collectors.joining;

import com.palantir.docker.compose.connection.Cluster;
import com.palantir.docker.compose.connection.Container;
import com.palantir.docker.compose.connection.State;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;

@FunctionalInterface
Expand All @@ -37,5 +43,31 @@ static <T> ClusterHealthCheck transformingHealthCheck(Function<Cluster, T> trans
};
}

SuccessOrFailure isClusterHealthy(Cluster cluster);
/**
* Returns a check that the native "healthcheck" status of the docker containers is not unhealthy.
*
* <p>Does not wait for DOWN or PAUSED containers, or containers with no healthcheck defined.
*/
static ClusterHealthCheck nativeHealthChecks() {
return cluster -> {
Set<String> unhealthyContainers = new LinkedHashSet<>();
try {
for (Container container : cluster.allContainers()) {
State state = container.state();
if (state == State.UNHEALTHY) {
unhealthyContainers.add(container.getContainerName());
}
}
if (!unhealthyContainers.isEmpty()) {
return SuccessOrFailure.failure(
"The following containers are not healthy: " + unhealthyContainers.stream().collect(joining(", ")));
}
return SuccessOrFailure.success();
} catch (IOException e) {
return SuccessOrFailure.fromException(e);
}
};
}

SuccessOrFailure isClusterHealthy(Cluster cluster) throws InterruptedException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@
import com.palantir.docker.compose.connection.ContainerNames;
import com.palantir.docker.compose.connection.DockerMachine;
import com.palantir.docker.compose.connection.Ports;
import com.palantir.docker.compose.connection.State;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import org.apache.commons.io.IOUtils;
import org.joda.time.Duration;
import org.slf4j.Logger;
Expand Down Expand Up @@ -165,14 +165,18 @@ public List<ContainerName> ps() throws IOException, InterruptedException {
}

@Override
public List<String> services() throws IOException, InterruptedException {
String servicesOutput = command.execute(Command.throwingOnError(), "config", "--services");
return Arrays.asList(servicesOutput.split("\n"));
public Optional<String> id(Container container) throws IOException, InterruptedException {
String id = command.execute(Command.throwingOnError(), "ps", "-q", container.getContainerName());
if (id.isEmpty()) {
return Optional.empty();
}
return Optional.of(id);
}

@Override
public Container container(String containerName) {
return new Container(containerName, this);
public List<String> services() throws IOException, InterruptedException {
String servicesOutput = command.execute(Command.throwingOnError(), "config", "--services");
return Arrays.asList(servicesOutput.split("\n"));
}

/**
Expand Down Expand Up @@ -204,11 +208,6 @@ public Ports ports(String service) throws IOException, InterruptedException {
return Ports.parseFromDockerComposePs(psOutput(service), dockerMachine.getIp());
}

@Override
public State state(String service) throws IOException, InterruptedException {
return State.parseFromDockerComposePs(psOutput(service));
}

private static ErrorHandler swallowingDownCommandDoesNotExist() {
return (exitCode, output, commandName, commands) -> {
if (downCommandWasPresent(output)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
import com.palantir.docker.compose.connection.Container;
import com.palantir.docker.compose.connection.ContainerName;
import com.palantir.docker.compose.connection.Ports;
import com.palantir.docker.compose.connection.State;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Optional;

abstract class DelegatingDockerCompose implements DockerCompose {
private final DockerCompose dockerCompose;
Expand Down Expand Up @@ -98,13 +98,13 @@ public List<ContainerName> ps() throws IOException, InterruptedException {
}

@Override
public List<String> services() throws IOException, InterruptedException {
return dockerCompose.services();
public Optional<String> id(Container container) throws IOException, InterruptedException {
return dockerCompose.id(container);
}

@Override
public Container container(String containerName) {
return dockerCompose.container(containerName);
public List<String> services() throws IOException, InterruptedException {
return dockerCompose.services();
}

@Override
Expand All @@ -117,11 +117,6 @@ public Ports ports(String service) throws IOException, InterruptedException {
return dockerCompose.ports(service);
}

@Override
public State state(String service) throws IOException, InterruptedException {
return dockerCompose.state(service);
}

protected final DockerCompose getDockerCompose() {
return dockerCompose;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,52 @@
*/
package com.palantir.docker.compose.execution;

import static com.google.common.base.Preconditions.checkState;

import com.github.zafarkhaja.semver.Version;
import com.google.common.collect.ObjectArrays;
import com.palantir.docker.compose.connection.DockerMachine;
import com.palantir.docker.compose.connection.State;
import java.io.IOException;
import java.util.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Docker {

private static final Logger log = LoggerFactory.getLogger(Docker.class);

private static final Pattern VERSION_PATTERN = Pattern.compile("Docker version (\\d+\\.\\d+\\.\\d+).*");
private static final String HEALTH_STATUS_FORMAT =
"--format="
+ "{{if not .State.Running}}DOWN"
+ "{{else if .State.Paused}}PAUSED"
+ "{{else if index .State \"Health\"}}"
+ "{{if eq .State.Health.Status \"healthy\"}}HEALTHY"
+ "{{else}}UNHEALTHY{{end}}"
+ "{{else}}HEALTHY{{end}}";

public static Version version() throws IOException, InterruptedException {
Command command = new Command(
DockerExecutable.builder().dockerConfiguration(DockerMachine.localMachine().build()).build(), log::trace);
String versionString = command.execute(Command.throwingOnError(), "-v");
Matcher matcher = VERSION_PATTERN.matcher(versionString);
checkState(matcher.matches(), "Unexpected output of docker -v: %s", versionString);
return Version.valueOf(matcher.group(1));
}

private final Command command;

public Docker(DockerExecutable rawExecutable) {
this.command = new Command(rawExecutable, log::debug);
this.command = new Command(rawExecutable, log::trace);
}

public State state(String containerId) throws IOException, InterruptedException {
String stateString = command.execute(
Command.throwingOnError(), "inspect", HEALTH_STATUS_FORMAT, containerId);
return State.valueOf(stateString);
}

public void rm(Collection<String> containerNames) throws IOException, InterruptedException {
Expand Down
Loading

0 comments on commit 19d88e6

Please sign in to comment.