package org.testcontainers;
import com.github.dockerjava.api.DockerClient;
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.Image;
import com.github.dockerjava.api.model.Info;
import com.github.dockerjava.api.model.Version;
import com.github.dockerjava.core.command.PullImageResultCallback;
import lombok.Synchronized;
import lombok.extern.slf4j.Slf4j;
import org.testcontainers.dockerclient.*;
import org.testcontainers.utility.TestcontainersConfiguration;
import java.util.List;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import static java.util.Arrays.asList;
/**
* 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 = TestcontainersConfiguration.getInstance().getTinyImage();
private static DockerClientFactory instance;
// Cached client configuration
private DockerClientProviderStrategy strategy;
private boolean preconditionsChecked = false;
private static final List<DockerClientProviderStrategy> CONFIGURATION_STRATEGIES =
asList(new EnvironmentAndSystemPropertyClientProviderStrategy(),
new UnixSocketClientProviderStrategy(),
new ProxiedUnixSocketClientProviderStrategy(),
new DockerMachineClientProviderStrategy(),
new WindowsClientProviderStrategy());
private String activeApiVersion;
private String activeExecutionDriver;
static {
System.setProperty("org.testcontainers.shaded.io.netty.packagePrefix", "org.testcontainers.shaded.");
}
/**
* Private constructor
*/
private DockerClientFactory() {
}
/**
* Obtain an instance of the DockerClientFactory.
*
* @return the singleton instance of DockerClientFactory
*/
public synchronized static DockerClientFactory instance() {
if (instance == null) {
instance = new DockerClientFactory();
}
return instance;
}
/**
*
* @return a new initialized Docker client
*/
@Synchronized
public DockerClient client() {
if (strategy != null) {
return strategy.getClient();
}
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();
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());
checkDiskSpaceAndHandleExceptions(client);
preconditionsChecked = true;
}
return client;
}
/**
* Check whether the image is available locally and pull it otherwise
*/
private void checkAndPullImage(DockerClient client, String image) {
List<Image> images = client.listImagesCmd().withImageNameFilter(image).exec();
if (images.isEmpty()) {
client.pullImageCmd(image).exec(new PullImageResultCallback()).awaitSuccess();
}
}
/**
* @return the IP address of the host running Docker
*/
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();
}
// We can't use client() here because it might create an infinite loop
return runInsideDocker(strategy.getClient(), createContainerCmdConsumer, block);
}
private <T> T runInsideDocker(DockerClient client, Consumer<CreateContainerCmd> createContainerCmdConsumer, BiFunction<DockerClient, String, T> block) {
checkAndPullImage(client, TINY_IMAGE);
CreateContainerCmd createContainerCmd = client.createContainerCmd(TINY_IMAGE);
createContainerCmdConsumer.accept(createContainerCmd);
String id = createContainerCmd.exec().getId();
client.startContainerCmd(id).exec();
try {
return block.apply(client, id);
} finally {
try {
client.removeContainerCmd(id).withRemoveVolumes(true).withForce(true).exec();
} catch (NotFoundException | InternalServerErrorException ignored) {
log.debug("", ignored);
}
}
}
private static class DiskSpaceUsage {
Optional<Integer> availableMB = Optional.empty();
Optional<Integer> usedPercent = Optional.empty();
}
private DiskSpaceUsage parseAvailableDiskSpace(String dfOutput) {
DiskSpaceUsage df = new DiskSpaceUsage();
String[] lines = dfOutput.split("\n");
for (String line : lines) {
String[] fields = line.split("\\s+");
if (fields[5].equals("/")) {
int availableKB = Integer.valueOf(fields[3]);
df.availableMB = Optional.of(availableKB / 1024);
df.usedPercent = Optional.of(Integer.valueOf(fields[4].replace("%", "")));
}
}
return df;
}
/**
* @return the docker API version of the daemon that we have connected to
*/
public String getActiveApiVersion() {
if (!preconditionsChecked) {
client();
}
return activeApiVersion;
}
/**
* @return the docker execution driver of the daemon that we have connected to
*/
public String getActiveExecutionDriver() {
if (!preconditionsChecked) {
client();
}
return activeExecutionDriver;
}
/**
* @param providerStrategyClass a class that extends {@link DockerMachineClientProviderStrategy}
* @return whether or not the currently active strategy is of the provided type
*/
public boolean isUsing(Class<? extends DockerClientProviderStrategy> providerStrategyClass) {
return providerStrategyClass.isAssignableFrom(this.strategy.getClass());
}
private static class NotEnoughDiskSpaceException extends RuntimeException {
NotEnoughDiskSpaceException(String message) {
super(message);
}
}
}