All Downloads are FREE. Search and download functionalities are using the official Maven repository.

me.bazhenov.docker.Docker Maven / Gradle / Ivy

There is a newer version: 1.6.2
Show newest version
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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy