diff --git a/CHANGELOG.md b/CHANGELOG.md index 006659a92d9..830bd5c4713 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/core/pom.xml b/core/pom.xml index e7e8935720e..cbf235a1960 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -64,6 +64,11 @@ slf4j-ext 1.7.25 + + org.rnorth.visible-assertions + visible-assertions + 1.0.5 + diff --git a/core/src/main/java/org/testcontainers/DockerClientFactory.java b/core/src/main/java/org/testcontainers/DockerClientFactory.java index 693571802ad..816c058dce6 100644 --- a/core/src/main/java/org/testcontainers/DockerClientFactory.java +++ b/core/src/main/java/org/testcontainers/DockerClientFactory.java @@ -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; @@ -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) { @@ -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() { + @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) { @@ -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 runInsideDocker(Consumer createContainerCmdConsumer, BiFunction block) { if (strategy == null) { client(); @@ -176,9 +224,8 @@ private T runInsideDocker(DockerClient client, Consumer createContainerCmdConsumer.accept(createContainerCmd); String id = createContainerCmd.exec().getId(); - client.startContainerCmd(id).exec(); - try { + client.startContainerCmd(id).exec(); return block.apply(client, id); } finally { try { @@ -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("%", ""))); diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index 49a020b0889..859def3d3ba 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -147,7 +147,7 @@ public class GenericContainer> public GenericContainer() { - this("alpine:3.2"); + this(TestcontainersConfiguration.getInstance().getTinyImage()); } public GenericContainer(@NonNull final String dockerImageName) { diff --git a/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java b/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java index c969b08d4f6..1c90a1fabae 100644 --- a/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java +++ b/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java @@ -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(); @@ -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); diff --git a/pom.xml b/pom.xml index 8a3719927b4..8a395548335 100644 --- a/pom.xml +++ b/pom.xml @@ -95,12 +95,6 @@ 1.2.3 test - - org.rnorth.visible-assertions - visible-assertions - 1.0.5 - test -