Skip to content

Commit

Permalink
Make it possible to run TestContainers inside a container. Fixes test…
Browse files Browse the repository at this point in the history
  • Loading branch information
bsideup authored and rnorth committed Jan 21, 2017
1 parent db2c918 commit 398a616
Show file tree
Hide file tree
Showing 12 changed files with 559 additions and 65 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
14 changes: 12 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,16 @@ before_install:

script:
- mvn -B test
# Run Docker-in-Docker tests
# Yes, we use selenium image, but we use it anyway in BrowserContainer's tests
- >
docker run --rm
-v "$HOME/.m2":/root/.m2/
-v /var/run/docker.sock:/var/run/docker.sock
-v "$(pwd)":/app
-w /app
selenium/standalone-chrome-debug:2.52.0
./mvnw -B -pl core test -Dtest=*Docker
- mvn -B test -f shade-test/pom.xml

cache:
Expand Down
10 changes: 10 additions & 0 deletions circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ dependencies:
test:
override:
- mvn -B test
# Run Docker-in-Docker tests
# Yes, we use selenium image, but we use it anyway in BrowserContainer's tests
- >
docker run --rm
-v "$HOME/.m2":/root/.m2/
-v /var/run/docker.sock:/var/run/docker.sock
-v "$(pwd)":/app
-w /app
selenium/standalone-chrome-debug:2.52.0
./mvnw -B -pl core test -Dtest=*Docker
- 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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.testcontainers.dockerclient;

import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.testcontainers.containers.GenericContainer;

import static org.junit.Assume.assumeTrue;
import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals;
import static org.testcontainers.junit.GenericContainerRuleTest.getReaderForContainerPort80;

public class DockerInDockerTest {

@ClassRule
public static TestRule assumption = new TestWatcher() {
@Override
public Statement apply(Statement base, Description description) {
assumeTrue("We're inside a container", DockerClientConfigUtils.IN_A_CONTAINER);
return super.apply(base, description);
}
};

@Rule
public GenericContainer container = new GenericContainer("alpine:3.2")
.withExposedPorts(80)
.withCommand("/bin/sh", "-c", "while true; do echo \"hello\" | nc -l -p 80; done");

@Test
public void testIpDetection() throws Exception {
String line = getReaderForContainerPort80(container).readLine();
assertEquals("The container is accessible", "hello", line);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ public void extraHostTest() throws IOException {
assertTrue("The hosts file of container contains extra host", matcher.find());
}

private BufferedReader getReaderForContainerPort80(GenericContainer container) {
public static BufferedReader getReaderForContainerPort80(GenericContainer container) {

return Unreliables.retryUntilSuccess(10, TimeUnit.SECONDS, () -> {
Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
Expand Down
Loading

0 comments on commit 398a616

Please sign in to comment.