Skip to content

Commit

Permalink
Merge pull request #267 from testcontainers/docker-in-docker
Browse files Browse the repository at this point in the history
Make it possible to run TestContainers inside a container
  • Loading branch information
rnorth committed Jan 22, 2017
2 parents db2c918 + e6d8be0 commit deab822
Show file tree
Hide file tree
Showing 14 changed files with 574 additions and 67 deletions.
Binary file added .mvn/wrapper/maven-wrapper.jar
Binary file not shown.
1 change: 1 addition & 0 deletions .mvn/wrapper/maven-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.9/apache-maven-3.3.9-bin.zip
13 changes: 11 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ before_install:
- docker pull mysql:5.6
- docker pull mysql:5.5
- docker pull postgres:latest
- docker pull selenium/standalone-chrome-debug:2.45.0
- docker pull selenium/standalone-firefox-debug:2.45.0
- docker pull selenium/standalone-chrome-debug:2.52.0
- docker pull selenium/standalone-firefox-debug:2.52.0
- docker pull richnorth/vnc-recorder:latest
- docker pull nginx:1.9.4
- docker pull dduportal/docker-compose:1.6.0
Expand All @@ -32,6 +32,15 @@ before_install:

script:
- mvn -B test
# Run Docker-in-Docker tests
- |
DOCKER_HOST=unix:///var/run/docker.sock DOCKER_TLS_VERIFY= docker run --rm \
-v "$HOME/.m2":/root/.m2/ \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$(pwd)":"$(pwd)" \
-w "$(pwd)" \
openjdk:8-jre \
./mvnw -B -pl core test -Dtest=*GenericContainerRuleTest
- mvn -B test -f shade-test/pom.xml

cache:
Expand Down
9 changes: 9 additions & 0 deletions circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ dependencies:
test:
override:
- mvn -B test
# Run Docker-in-Docker tests
- |
DOCKER_HOST=unix:///var/run/docker.sock DOCKER_TLS_VERIFY= docker run --rm \
-v "$HOME/.m2":/root/.m2/ \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$(pwd)":"$(pwd)" \
-w "$(pwd)" \
openjdk:8-jre \
./mvnw -B -pl core test -Dtest=*GenericContainerRuleTest
- mvn -B test -f shade-test/pom.xml
post:
- mkdir -p $CIRCLE_TEST_REPORTS/junit/
Expand Down
98 changes: 47 additions & 51 deletions core/src/main/java/org/testcontainers/DockerClientFactory.java
Original file line number Diff line number Diff line change
@@ -1,36 +1,35 @@
package org.testcontainers;

import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.CreateContainerResponse;
import com.github.dockerjava.api.command.CreateContainerCmd;
import com.github.dockerjava.api.exception.InternalServerErrorException;
import com.github.dockerjava.api.exception.NotFoundException;
import com.github.dockerjava.api.model.Frame;
import com.github.dockerjava.api.model.Image;
import com.github.dockerjava.api.model.Info;
import com.github.dockerjava.api.model.Version;
import com.github.dockerjava.core.command.LogContainerResultCallback;
import com.github.dockerjava.core.command.PullImageResultCallback;
import com.github.dockerjava.core.command.WaitContainerResultCallback;

import lombok.Synchronized;
import org.slf4j.Logger;
import lombok.extern.slf4j.Slf4j;
import org.testcontainers.dockerclient.*;

import java.util.List;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Consumer;

import static java.util.Arrays.asList;
import static org.slf4j.LoggerFactory.getLogger;

/**
* Singleton class that provides initialized Docker clients.
* <p>
* The correct client configuration to use will be determined on first use, and cached thereafter.
*/
@Slf4j
public class DockerClientFactory {

private static final String TINY_IMAGE = "alpine:3.2";
private static DockerClientFactory instance;
private static final Logger LOGGER = getLogger(DockerClientFactory.class);

// Cached client configuration
private DockerClientProviderStrategy strategy;
Expand Down Expand Up @@ -80,20 +79,29 @@ public DockerClient client() {
}

strategy = DockerClientProviderStrategy.getFirstValidStrategy(CONFIGURATION_STRATEGIES);

log.info("Docker host IP address is {}", strategy.getDockerHostIpAddress());
DockerClient client = strategy.getClient();

if (!preconditionsChecked) {
Info dockerInfo = client.infoCmd().exec();
Version version = client.versionCmd().exec();
activeApiVersion = version.getApiVersion();
activeExecutionDriver = dockerInfo.getExecutionDriver();
LOGGER.info("Connected to docker: \n" +
log.info("Connected to docker: \n" +
" Server Version: " + dockerInfo.getServerVersion() + "\n" +
" API Version: " + activeApiVersion + "\n" +
" Operating System: " + dockerInfo.getOperatingSystem() + "\n" +
" Total Memory: " + dockerInfo.getMemTotal() / (1024 * 1024) + " MB");

checkVersion(version.getVersion());

List<Image> images = client.listImagesCmd().exec();
// Pull the image we use to perform some checks
if (images.stream().noneMatch(it -> it.getRepoTags() != null && asList(it.getRepoTags()).contains(TINY_IMAGE))) {
client.pullImageCmd(TINY_IMAGE).exec(new PullImageResultCallback()).awaitSuccess();
}

checkDiskSpaceAndHandleExceptions(client);
preconditionsChecked = true;
}
Expand Down Expand Up @@ -121,7 +129,7 @@ private void checkDiskSpaceAndHandleExceptions(DockerClient client) {
} catch (NotEnoughDiskSpaceException e) {
throw e;
} catch (Exception e) {
LOGGER.warn("Encountered and ignored error while checking disk space", e);
log.warn("Encountered and ignored error while checking disk space", e);
}
}

Expand All @@ -130,44 +138,47 @@ private void checkDiskSpaceAndHandleExceptions(DockerClient client) {
* @param client an active Docker client
*/
private void checkDiskSpace(DockerClient client) {
DiskSpaceUsage df = runInsideDocker(client, cmd -> cmd.withCmd("df", "-P"), (dockerClient, id) -> {
String logResults = dockerClient.logContainerCmd(id)
.withStdOut(true)
.exec(new LogToStringContainerCallback())
.toString();

return parseAvailableDiskSpace(logResults);
});

log.info("Disk utilization in Docker environment is {} ({} )",
df.usedPercent.map(x -> x + "%").orElse("unknown"),
df.availableMB.map(x -> x + " MB available").orElse("unknown available"));

if (df.availableMB.orElseThrow(NotAbleToGetDiskSpaceUsageException::new) < 2048) {
log.error("Docker environment has less than 2GB free - execution is unlikely to succeed so will be aborted.");
throw new NotEnoughDiskSpaceException("Not enough disk space in Docker environment");
}
}

List<Image> images = client.listImagesCmd().exec();
if (!images.stream().anyMatch(it -> it.getRepoTags() != null && asList(it.getRepoTags()).contains("alpine:3.2"))) {
PullImageResultCallback callback = client.pullImageCmd("alpine:3.2").exec(new PullImageResultCallback());
callback.awaitSuccess();
public <T> T runInsideDocker(Consumer<CreateContainerCmd> createContainerCmdConsumer, BiFunction<DockerClient, String, T> block) {
if (strategy == null) {
client();
}
// We can't use client() here because it might create an infinite loop
return runInsideDocker(strategy.getClient(), createContainerCmdConsumer, block);
}

CreateContainerResponse createContainerResponse = client.createContainerCmd("alpine:3.2").withCmd("df", "-P").exec();
String id = createContainerResponse.getId();
private <T> T runInsideDocker(DockerClient client, Consumer<CreateContainerCmd> createContainerCmdConsumer, BiFunction<DockerClient, String, T> block) {
CreateContainerCmd createContainerCmd = client.createContainerCmd(TINY_IMAGE);
createContainerCmdConsumer.accept(createContainerCmd);
String id = createContainerCmd.exec().getId();

client.startContainerCmd(id).exec();

LogContainerResultCallback callback = client.logContainerCmd(id).withStdOut(true).exec(new LogContainerCallback());
try {

WaitContainerResultCallback waitCallback = new WaitContainerResultCallback();
client.waitContainerCmd(id).exec(waitCallback);
waitCallback.awaitStarted();

callback.awaitCompletion();
String logResults = callback.toString();

DiskSpaceUsage df = parseAvailableDiskSpace(logResults);
LOGGER.info("Disk utilization in Docker environment is {} ({} )",
df.usedPercent.map(x -> x.toString() + "%").orElse("unknown"),
df.availableMB.map(x -> x.toString() + " MB available").orElse("unknown available"));

if (df.availableMB.orElseThrow(NotAbleToGetDiskSpaceUsageException::new) < 2048) {
LOGGER.error("Docker environment has less than 2GB free - execution is unlikely to succeed so will be aborted.");
throw new NotEnoughDiskSpaceException("Not enough disk space in Docker environment");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
return block.apply(client, id);
} finally {
try {
client.removeContainerCmd(id).withRemoveVolumes(true).withForce(true).exec();
} catch (NotFoundException | InternalServerErrorException ignored) {

log.debug("", ignored);
}
}
}
Expand Down Expand Up @@ -231,18 +242,3 @@ private static class NotAbleToGetDiskSpaceUsageException extends RuntimeExceptio
}
}
}

class LogContainerCallback extends LogContainerResultCallback {
private final StringBuffer log = new StringBuffer();

@Override
public void onNext(Frame frame) {
log.append(new String(frame.getPayload()));
super.onNext(frame);
}

@Override
public String toString() {
return log.toString();
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,53 @@
package org.testcontainers.dockerclient;

import com.github.dockerjava.core.DockerClientConfig;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.testcontainers.DockerClientFactory;

import java.io.File;
import java.util.Optional;

@Slf4j
public class DockerClientConfigUtils {

// See https://github.com/docker/docker/blob/a9fa38b1edf30b23cae3eade0be48b3d4b1de14b/daemon/initlayer/setup_unix.go#L25
public static final boolean IN_A_CONTAINER = new File("/.dockerenv").exists();

@Getter(lazy = true)
private static final Optional<String> detectedDockerHostIp = Optional
.of(IN_A_CONTAINER)
.filter(it -> it)
.map(file -> DockerClientFactory.instance().runInsideDocker(
cmd -> cmd.withCmd("sh", "-c", "ip route|awk '/default/ { print $3 }'"),
(client, id) -> {
try {
return client.logContainerCmd(id)
.withStdOut(true)
.exec(new LogToStringContainerCallback())
.toString();
} catch (Exception e) {
log.warn("Can't parse the default gateway IP", e);
return null;
}
}
))
.map(StringUtils::trimToEmpty)
.filter(StringUtils::isNotBlank);

public static String getDockerHostIpAddress(DockerClientConfig config) {
switch (config.getDockerHost().getScheme()) {
case "http":
case "https":
case "tcp":
return config.getDockerHost().getHost();
case "unix":
return "localhost";
default:
return null;
}
return getDetectedDockerHostIp().orElseGet(() -> {
switch (config.getDockerHost().getScheme()) {
case "http":
case "https":
case "tcp":
return config.getDockerHost().getHost();
case "unix":
return "localhost";
default:
return null;
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ public void test() throws InvalidConfigurationException {
}

LOGGER.info("Found docker client settings from environment");
LOGGER.info("Docker host IP address is {}", DockerClientConfigUtils.getDockerHostIpAddress(config));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.testcontainers.dockerclient;

import com.github.dockerjava.api.model.Frame;
import com.github.dockerjava.core.command.LogContainerResultCallback;

public class LogToStringContainerCallback extends LogContainerResultCallback {
private final StringBuffer log = new StringBuffer();

@Override
public void onNext(Frame frame) {
log.append(new String(frame.getPayload()));
super.onNext(frame);
}

@Override
public String toString() {
try {
awaitCompletion();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return log.toString();
}
}
3 changes: 2 additions & 1 deletion docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* [Usage modes](usage.md#usage-modes)
* [Maven dependencies](usage.md#maven-dependencies)
* [Logging](usage.md#logging)
* [Docker in Docker](usage/dind.md)

## Generic containers

Expand Down Expand Up @@ -52,4 +53,4 @@
##
* [License](index.md#license)
* [Attributions](index.md#attributions)
* [Contributing](index.md#contributing)
* [Contributing](index.md#contributing)
3 changes: 2 additions & 1 deletion docs/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
| Linux - general | Docker v1.10 | |
| Linux - Travis CI | Docker v1.10 | See [example .travis.yml](https://raw.githubusercontent.com/testcontainers/testcontainers-java/master/.travis.yml) for baseline Travis CI configuration |
| Linux - Circle CI (LXC driver) | Docker v1.9.1 | The `exec` feature is not compatible with Circle CI. See [example circle.yml](../circle.yml) for baseline CircleCI configuration |
| Linux - Docker in Docker | Docker v1.12 | See [Docker-in-Docker](usage/dind.md) for the detailed configuration |
| Mac OS X - Docker Toolbox | Docker Machine v0.8.0 | |
| Mac OS X - Docker for Mac | 1.12.0 | *Support is best-efforts at present*. `getTestHostIpAddress()` is [not currently supported](https://github.com/testcontainers/testcontainers-java/issues/166) due to limitations in Docker for Mac. |
| Windows - Docker Toolbox | | *Support is limited at present and this is not currently tested on a regular basis*. |
| Windows - Docker for Windows Beta | | *Not currently supported*. |
| Windows - Docker for Windows Beta | | *Not currently supported*. |
3 changes: 2 additions & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Testcontainers will try to connect to a Docker daemon using the following strate
* `DOCKER_TLS_VERIFY=1`
* `DOCKER_CERT_PATH=~/.docker`
* If Docker Machine is installed, the docker machine environment for the *first* machine found. Docker Machine needs to be on the PATH for this to succeed.
* If you're going to run your tests inside a container, please read [Docker in Docker](usage/dind.md) first.

### Usage modes

Expand Down Expand Up @@ -97,4 +98,4 @@ should be included in your classpath to show a reasonable level of log output:
<logger name="com.github.dockerjava" level="WARN"/>
<logger name="org.zeroturnaround.exec" level="WARN"/>
</configuration>
```
```
Loading

0 comments on commit deab822

Please sign in to comment.