me.bazhenov.docker.Docker Maven / Gradle / Ivy
package me.bazhenov.docker;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.slf4j.Logger;
import java.io.*;
import java.util.*;
import static java.io.File.createTempFile;
import static java.lang.Integer.parseInt;
import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.lang.Thread.sleep;
import static java.nio.file.Files.readAllLines;
import static java.util.Arrays.asList;
import static java.util.Collections.singleton;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.stream.Collectors.joining;
import static org.slf4j.LoggerFactory.getLogger;
/**
* This class provides Docker container facility.
*
* Two main methods are:
*
* - {@link #executeAndReturnOutput(ContainerDefinition)};
* - {@link #start(ContainerDefinition)}.
*
*/
@SuppressWarnings("WeakerAccess")
public final class Docker implements Closeable {
private static final Logger log = getLogger(Docker.class);
private static final ObjectMapper jsonReader = new ObjectMapper();
private final String pathToDocker;
private final Set containersToRemove = new HashSet<>();
public Docker(String pathToDocker) {
this.pathToDocker = requireNonNull(pathToDocker);
}
public Docker() {
this("docker");
}
/**
* Runs given container wait for it to finish and then return its stdout.
*
* Be careful to use this method when large output is generated by a container. This method is fully buffered, so
* OOM can possibly be generated.
*
* @param definition definition of a container
* @return stdout of a container
* @throws IOException in case when container finished with non-zero exit code or any other problem when
* starting container
* @throws InterruptedException when thread was interrupted
*/
public String executeAndReturnOutput(ContainerDefinition definition) throws IOException, InterruptedException {
return readFully(execute(definition).getInputStream());
}
public Process execute(ContainerDefinition definition) throws IOException, InterruptedException {
List cmd = prepareDockerCommand(definition);
return doExecute(cmd);
}
/**
* Starts a container in background-mode
*
* @param definition container definition
* @return container id
* @throws IOException if there is error while starting container
* @throws InterruptedException when thread was interrupted
*/
public String start(ContainerDefinition definition) throws IOException, InterruptedException {
ensureImageAvailable(definition.getImage());
File cidFile = createTempFile("docker", "cid");
cidFile.deleteOnExit();
// Docker requires cid-file to be not present at the moment of starting a container
if (!cidFile.delete()) {
throw new IllegalStateException("Docker requires cid-file to be not present at the moment of starting a container");
}
List cmd = prepareDockerCommand(definition, "--cidfile", cidFile.getAbsolutePath());
Process process = runProcess(cmd);
try {
String cid = waitForCid(process, cidFile);
if (definition.isRemoveAfterCompletion())
containersToRemove.add(cid);
checkContainerState(cid, "running");
if (shouldWaitForOpenPorts(definition))
waitForPorts(cid, definition.getPublishedPorts().keySet());
return cid;
} catch (IOException e) {
throw new UncheckedIOException("Unable to start container\n" +
"Container stderr: " + readFully(process.getErrorStream()), e);
}
}
private static boolean shouldWaitForOpenPorts(ContainerDefinition definition) {
return !definition.getPublishedPorts().isEmpty() && definition.isWaitForAllExposedPortsToBeOpen();
}
private void ensureImageAvailable(String image) throws IOException, InterruptedException {
Process process = doExecute(asList(pathToDocker, "image", "inspect", image), new HashSet<>(asList(0, 1)));
if (process.exitValue() == 1) {
log.warn("Image {} is not found locally. It will take some time to download it.", image);
}
}
private String waitForCid(Process process, File cidFile) throws InterruptedException, IOException {
do {
if (cidFile.isFile() && cidFile.length() > 0) {
return readAllLines(cidFile.toPath()).get(0);
} else if (!process.isAlive() && process.exitValue() != 0) {
throw new IllegalStateException("Unable to start Docker container.\n" +
"Exit code: " + process.exitValue() + "\n" +
"Stderr: " + readFully(process.getErrorStream()));
}
sleep(100);
} while (true);
}
private static String doExecuteAndGetFullOutput(List cmd) throws IOException, InterruptedException {
return readFully(doExecute(cmd).getInputStream());
}
private static Process doExecute(List cmd) throws IOException, InterruptedException {
return doExecute(cmd, singleton(0));
}
private static Process doExecute(List cmd, Set expectedExitCodes)
throws IOException, InterruptedException {
Process process = runProcess(cmd);
int exitCode = process.waitFor();
if (!expectedExitCodes.contains(exitCode)) {
throw new IOException("Unable to execute: " + String.join(" ", cmd) + "\n" +
"Exit code: " + exitCode + "\n" +
"Stderr: " + readFully(process.getErrorStream()) + "\n" +
"Stdout: " + readFully(process.getInputStream()));
}
return process;
}
private static Process runProcess(List cmd) throws IOException {
if (log.isDebugEnabled()) {
log.debug("Executing: {}", prettyFormatCommand(cmd));
}
ProcessBuilder builder = new ProcessBuilder(cmd);
return builder.start();
}
private List prepareDockerCommand(ContainerDefinition def, String... additionalOpts) {
List cmd = new ArrayList<>();
cmd.add(pathToDocker);
cmd.add("run");
cmd.add("-l");
cmd.add("docker");
if (additionalOpts.length > 0) {
cmd.addAll(asList(additionalOpts));
}
def.getPublishedPorts().forEach((key, value) -> {
cmd.add("-p");
cmd.add(value > 0 ? value + ":" + key : String.valueOf(key));
});
// Mounting volumes
for (Map.Entry volume : def.getVolumes().entrySet()) {
if (volume.getValue() == null || volume.getValue().isEmpty()) {
cmd.add("-v");
cmd.add(volume.getKey());
} else {
cmd.add("-v");
cmd.add(volume.getValue() + ":" + volume.getKey());
}
}
for (Map.Entry i : def.getEnvironment().entrySet()) {
cmd.add("-e");
cmd.add(i.getKey() + "=" + i.getValue());
}
if (def.isRemoveAfterCompletion())
cmd.add("--rm");
if (def.getWorkingDirectory() != null) {
cmd.add("-w");
cmd.add(def.getWorkingDirectory());
}
cmd.addAll(def.getCustomOptions());
cmd.add(def.getImage());
cmd.addAll(def.getCommand());
return cmd;
}
private static String prettyFormatCommand(List cmd) {
return cmd.stream()
.map(c -> c.contains(" ") ? "'" + c + "'" : c)
.collect(joining(" "));
}
static String readFully(InputStream stream) {
Scanner scanner = new Scanner(stream).useDelimiter("\\A");
return scanner.hasNext()
? scanner.next()
: "";
}
/**
* Waits for given ports to be open in a container.
*
* Only TCP ports are monitored at the moment using /proc/self/net/tcp
*
* @param cid container to monitor
* @param ports ports to wait for
*/
private void waitForPorts(String cid, Set ports) throws IOException, InterruptedException {
Thread self = currentThread();
long start = currentTimeMillis();
boolean reported = false;
while (!self.isInterrupted()) {
Set openPorts = new HashSet<>();
openPorts.addAll(readListenPorts(docker("exec", cid, "cat", "/proc/self/net/tcp")));
openPorts.addAll(readListenPorts(docker("exec", cid, "cat", "/proc/self/net/tcp6")));
if (openPorts.containsAll(ports))
return;
checkContainerState(cid, "running");
if (!reported && currentTimeMillis() - start > 5000) {
reported = true;
log.warn("Waiting for ports {} to open in container {}", ports, cid);
}
MILLISECONDS.sleep(200);
}
}
private String docker(String command, String... args) throws IOException, InterruptedException {
List cmd = new ArrayList<>(args.length + 2);
cmd.add(pathToDocker);
cmd.add(command);
cmd.addAll(asList(args));
return doExecuteAndGetFullOutput(cmd);
}
private void checkContainerState(String id, String expectedState) throws IOException, InterruptedException {
String json = docker("inspect", id);
JsonNode root = jsonReader.readTree(json);
String state = root.at("/0/State/Status").asText();
if (!expectedState.equalsIgnoreCase(state)) {
throw new IllegalStateException("Container " + id + " failed to start. Current state: " + state);
}
}
/**
* @param containerName container name or id
* @return Map where keys are container ports and values are host ports
* @throws IOException if there is error while docker inspecting
* @throws InterruptedException when thread was interrupted
*/
public Map getPublishedTcpPorts(String containerName) throws IOException, InterruptedException {
String json = docker("inspect", containerName);
JsonNode root = jsonReader.readTree(json);
return doGetPublishedPorts(root);
}
@Override
public void close() throws IOException {
if (!containersToRemove.isEmpty()) {
try {
List cmd = new ArrayList<>(asList(pathToDocker, "rm", "-f", "-v"));
cmd.addAll(containersToRemove);
doExecute(cmd);
containersToRemove.clear();
} catch (IOException e) {
throw new UncheckedIOException(e);
} catch (InterruptedException e) {
Thread.interrupted();
}
}
}
static Map doGetPublishedPorts(JsonNode root) {
JsonNode candidate = root.at("/0/NetworkSettings/Ports");
if (candidate.isMissingNode() || candidate.isNull())
return Collections.emptyMap();
ObjectNode ports = (ObjectNode) candidate;
Iterator names = ports.fieldNames();
Map pts = new HashMap<>();
while (names.hasNext()) {
String field = names.next();
if (field.matches("\\d+/tcp")) {
String[] parts = field.split("/", 2);
int containerPort = parseInt(parts[0]);
int localPort = ports.at("/" + field.replace("/", "~1") + "/0/HostPort").asInt();
pts.put(containerPort, localPort);
}
}
return pts;
}
public static Set readListenPorts(String output) {
Scanner scanner = new Scanner(output);
scanner.useRadix(16).useDelimiter("[\\s:]+");
Set result = new HashSet<>();
if (scanner.hasNextLine())
scanner.nextLine();
while (scanner.hasNextLine()) {
scanner.nextInt();
scanner.next();
int localPort = scanner.nextInt();
result.add(localPort);
scanner.nextLine();
}
return result;
}
/**
* Used for testing purposes only
*
* @return the number of volumes registered in docker
*/
int getVolumesCount() throws IOException, InterruptedException {
List cmd = new ArrayList<>();
cmd.add(pathToDocker);
cmd.add("volume");
cmd.add("ls");
cmd.add("-q");
String out = doExecuteAndGetFullOutput(cmd);
String[] parts = out.trim().split("\n");
return parts.length;
}
}