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

Implement pre-flight checks #363

Merged
merged 7 commits into from
Jun 18, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.
## UNRELEASED
### Changed
- Added `TC_DAEMON` JDBC URL flag to prevent `ContainerDatabaseDriver` from shutting down containers at the time all connections are closed. (#359, #360)
- Added pre-flight checks (can be disabled with `checks.disable` configuration property) (#363)
- Removed unused Jersey dependencies (#361)

## [1.3.0] - 2017-06-05
Expand Down
5 changes: 5 additions & 0 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@
<artifactId>slf4j-ext</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.rnorth.visible-assertions</groupId>
<artifactId>visible-assertions</artifactId>
<version>1.0.5</version>
</dependency>

<!-- Test dependencies -->
<dependency>
Expand Down
151 changes: 99 additions & 52 deletions core/src/main/java/org/testcontainers/DockerClientFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,28 @@

import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.CreateContainerCmd;
import com.github.dockerjava.api.command.InspectContainerResponse;
import com.github.dockerjava.api.exception.InternalServerErrorException;
import com.github.dockerjava.api.exception.NotFoundException;
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.api.model.*;
import com.github.dockerjava.core.command.ExecStartResultCallback;
import com.github.dockerjava.core.command.PullImageResultCallback;

import lombok.Synchronized;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.rnorth.visibleassertions.VisibleAssertions;
import org.testcontainers.dockerclient.*;
import org.testcontainers.utility.ComparableVersion;
import org.testcontainers.utility.MountableFile;
import org.testcontainers.utility.TestcontainersConfiguration;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Optional;
import java.util.function.BiFunction;
Expand Down Expand Up @@ -82,7 +92,8 @@ public DockerClient client() {

strategy = DockerClientProviderStrategy.getFirstValidStrategy(CONFIGURATION_STRATEGIES);

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

if (!preconditionsChecked) {
Expand All @@ -96,15 +107,93 @@ public DockerClient client() {
" Operating System: " + dockerInfo.getOperatingSystem() + "\n" +
" Total Memory: " + dockerInfo.getMemTotal() / (1024 * 1024) + " MB");

checkVersion(version.getVersion());
checkDiskSpaceAndHandleExceptions(client);
if (!TestcontainersConfiguration.getInstance().isDisableChecks()) {
VisibleAssertions.info("Checking the system...");

checkDockerVersion(version.getVersion());

MountableFile mountableFile = MountableFile.forClasspathResource(this.getClass().getName().replace(".", "/") + ".class");

runInsideDocker(
client,
cmd -> cmd
.withCmd("/bin/sh", "-c", "while true; do printf 'hello' | nc -l -p 80; done")
.withBinds(new Bind(mountableFile.getResolvedPath(), new Volume("/dummy"), AccessMode.ro))
.withExposedPorts(new ExposedPort(80))
.withPublishAllPorts(true),
(dockerClient, id) -> {

checkDiskSpace(dockerClient, id);
checkMountableFile(dockerClient, id);
checkExposedPort(hostIpAddress, dockerClient, id);

return null;
});
}
preconditionsChecked = true;
}

return client;
}

/**
private void checkDockerVersion(String dockerVersion) {
VisibleAssertions.assertThat("Docker version", dockerVersion, new BaseMatcher<String>() {
@Override
public boolean matches(Object o) {
return new ComparableVersion(o.toString()).compareTo(new ComparableVersion("1.6.0")) >= 0;
}

@Override
public void describeTo(Description description) {
description.appendText("is newer than 1.6.0");
}
});
}

private void checkDiskSpace(DockerClient dockerClient, String id) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

try {
dockerClient
.execStartCmd(dockerClient.execCreateCmd(id).withAttachStdout(true).withCmd("df", "-P").exec().getId())
.exec(new ExecStartResultCallback(outputStream, null))
.awaitCompletion();
} catch (Exception e) {
log.debug("Can't exec disk checking command", e);
}

DiskSpaceUsage df = parseAvailableDiskSpace(outputStream.toString());

VisibleAssertions.assertTrue(
"Docker environment has more than 2GB free",
df.availableMB.map(it -> it >= 2048).orElse(true)
);
}

private void checkMountableFile(DockerClient dockerClient, String id) {
try (InputStream stream = dockerClient.copyArchiveFromContainerCmd(id, "/dummy").exec()) {
stream.read();
VisibleAssertions.pass("File should be mountable");
} catch (Exception e) {
VisibleAssertions.fail("File should be mountable but fails with " + e.getMessage());
}
}

private void checkExposedPort(String hostIpAddress, DockerClient dockerClient, String id) {
InspectContainerResponse inspectedContainer = dockerClient.inspectContainerCmd(id).exec();

String portSpec = inspectedContainer.getNetworkSettings().getPorts().getBindings().values().iterator().next()[0].getHostPortSpec();

String response;
try (Socket socket = new Socket(hostIpAddress, Integer.parseInt(portSpec))) {
response = IOUtils.toString(socket.getInputStream(), Charset.defaultCharset());
} catch (IOException e) {
response = e.getMessage();
}
VisibleAssertions.assertEquals("Exposed port is accessible", "hello", response);
}

/**
* Check whether the image is available locally and pull it otherwise
*/
private void checkAndPullImage(DockerClient client, String image) {
Expand All @@ -121,47 +210,6 @@ public String dockerHostIpAddress() {
return strategy.getDockerHostIpAddress();
}

private void checkVersion(String version) {
String[] splitVersion = version.split("\\.");
if (Integer.valueOf(splitVersion[0]) <= 1 && Integer.valueOf(splitVersion[1]) < 6) {
throw new IllegalStateException("Docker version 1.6.0+ is required, but version " + version + " was found");
}
}

private void checkDiskSpaceAndHandleExceptions(DockerClient client) {
try {
checkDiskSpace(client);
} catch (NotEnoughDiskSpaceException e) {
throw e;
} catch (Exception e) {
log.warn("Encountered and ignored error while checking disk space", e);
}
}

/**
* Check whether this docker installation is likely to have disk space problems
* @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.map(it -> it < 2048).orElse(false)) {
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");
}
}

public <T> T runInsideDocker(Consumer<CreateContainerCmd> createContainerCmdConsumer, BiFunction<DockerClient, String, T> block) {
if (strategy == null) {
client();
Expand All @@ -176,9 +224,8 @@ private <T> T runInsideDocker(DockerClient client, Consumer<CreateContainerCmd>
createContainerCmdConsumer.accept(createContainerCmd);
String id = createContainerCmd.exec().getId();

client.startContainerCmd(id).exec();

try {
client.startContainerCmd(id).exec();
return block.apply(client, id);
} finally {
try {
Expand All @@ -199,7 +246,7 @@ private DiskSpaceUsage parseAvailableDiskSpace(String dfOutput) {
String[] lines = dfOutput.split("\n");
for (String line : lines) {
String[] fields = line.split("\\s+");
if (fields[5].equals("/")) {
if (fields.length > 5 && fields[5].equals("/")) {
int availableKB = Integer.valueOf(fields[3]);
df.availableMB = Optional.of(availableKB / 1024);
df.usedPercent = Optional.of(Integer.valueOf(fields[4].replace("%", "")));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public class GenericContainer<SELF extends GenericContainer<SELF>>


public GenericContainer() {
this("alpine:3.2");
this(TestcontainersConfiguration.getInstance().getTinyImage());
}

public GenericContainer(@NonNull final String dockerImageName) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ public class TestcontainersConfiguration {

private String ambassadorContainerImage = "richnorth/ambassador:latest";
private String vncRecordedContainerImage = "richnorth/vnc-recorder:latest";
private String tinyImage = "alpine:3.2";
private String tinyImage = "alpine:3.5";
private boolean disableChecks = false;

private static TestcontainersConfiguration loadConfiguration() {
final TestcontainersConfiguration config = new TestcontainersConfiguration();
Expand All @@ -44,6 +45,7 @@ private static TestcontainersConfiguration loadConfiguration() {
config.ambassadorContainerImage = properties.getProperty("ambassador.container.image", config.ambassadorContainerImage);
config.vncRecordedContainerImage = properties.getProperty("vncrecorder.container.image", config.vncRecordedContainerImage);
config.tinyImage = properties.getProperty("tinyimage.container.image", config.tinyImage);
config.disableChecks = Boolean.parseBoolean(properties.getProperty("checks.disable", config.disableChecks + ""));

log.debug("Testcontainers configuration overrides loaded from {}: {}", configOverrides, config);

Expand Down
6 changes: 0 additions & 6 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,6 @@
<version>1.2.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.rnorth.visible-assertions</groupId>
<artifactId>visible-assertions</artifactId>
<version>1.0.5</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down