package com.rationaleemotions.server; import com.google.common.base.Preconditions; import com.rationaleemotions.config.ConfigReader; import com.rationaleemotions.config.MappingInfo; import com.rationaleemotions.server.docker.DeviceInfo; import com.spotify.docker.client.DefaultDockerClient; import com.spotify.docker.client.DockerClient; import com.spotify.docker.client.LoggingBuildHandler; import com.spotify.docker.client.ProgressHandler; import com.spotify.docker.client.exceptions.DockerException; import com.spotify.docker.client.messages.*; import java.util.stream.Collectors; import org.openqa.selenium.net.PortProber; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import static com.rationaleemotions.config.ConfigReader.getInstance; /** * A Helper class that facilitates interaction with a Docker Daemon. */ class DockerHelper { private interface Marker { } private static final Logger LOG = Logger.getLogger(Marker.class.getEnclosingClass().getName()); public static final String UNIX_SCHEME = "unix"; private DockerHelper() { DockerClient client = getClient(); Runtime.getRuntime().addShutdownHook(new Thread(new DockerCleanup(client))); } /** * @param id - The ID of the container that is to be cleaned up. * @throws DockerException - In case of any issues. * @throws InterruptedException - In case of any issues. */ static void killAndRemoveContainer(String id) throws DockerException, InterruptedException { if (LOG.isLoggable(Level.FINE)) { LOG.fine("Killing the container : [" + id + "]."); } getClient().killContainer(id); getClient().removeContainer(id); } /** * @param image - The name of the image for which a docker container is to be spun off. For e.g., you could * specify the image name as <code>selenium/standalone-chrome:3.0.1</code> to download the * <code>standalone-chrome</code> image with its tag as <code>3.0.1</code> * @param isPrivileged - <code>true</code> if the container is to be run in privileged mode. * @param devices - A List of {@link DeviceInfo} objects * @return - A {@link ContainerInfo} object that represents the newly spun off container. * @throws DockerException - In case of any issues. * @throws InterruptedException - In case of any issues. */ static ContainerInfo startContainerFor(String image, boolean isPrivileged, List<DeviceInfo> devices) throws DockerException, InterruptedException { if (LOG.isLoggable(Level.FINE)) { LOG.fine("Commencing starting of container for the image [" + image + "]."); } Preconditions.checkState("ok".equalsIgnoreCase(getClient().ping()), "Ensuring that the Docker Daemon is up and running."); DockerHelper.predownloadImagesIfRequired(); final Map<String, List<PortBinding>> portBindings = new HashMap<>(); List<PortBinding> randomPort = new ArrayList<>(); int port = PortProber.findFreePort(); String localHost = ConfigReader.getInstance().getLocalhost(); PortBinding binding = PortBinding.create(localHost, Integer.toString(port)); randomPort.add(binding); portBindings.put(ConfigReader.getInstance().getDockerImagePort(), randomPort); List<Device> deviceList = new LinkedList<>(); for (DeviceInfo each : devices) { deviceList.add(new Device() { @Override public String pathOnHost() { return each.getPathOnHost(); } @Override public String pathInContainer() { return each.getPathOnContainer(); } @Override public String cgroupPermissions() { return each.getGroupPermissions(); } }); } HostConfig.Builder hostConfigBuilder = HostConfig.builder() .portBindings(portBindings) .privileged(isPrivileged); String volume = ConfigReader.getInstance().getVolume(); if (volume != null && !volume.isEmpty()) { hostConfigBuilder = hostConfigBuilder.binds(ConfigReader.getInstance().getVolume()); } if (!deviceList.isEmpty()) { hostConfigBuilder = hostConfigBuilder.devices(deviceList); } final HostConfig hostConfig = hostConfigBuilder.build(); // add environmental variables List<String> envVariables = ConfigReader.getInstance().getEnvironment() .entrySet().stream() .map(entry -> entry.getKey() + "=" + entry.getValue()) .collect(Collectors.toList()); final ContainerConfig containerConfig = ContainerConfig.builder() .hostConfig(hostConfig) .image(image).exposedPorts(ConfigReader.getInstance().getDockerImagePort()).env(envVariables) .build(); final ContainerCreation creation = getClient().createContainer(containerConfig); final String id = creation.id(); // Inspect container final com.spotify.docker.client.messages.ContainerInfo containerInfo = getClient().inspectContainer(id); if (LOG.isLoggable(Level.FINE)) { LOG.fine(String.format("Container Information %s", containerInfo)); String msg = "Checking to see if the container with id [" + id + "] and name [" + containerInfo.name() + "]..."; LOG.fine(msg); } if (!containerInfo.state().running()) { // Start container getClient().startContainer(id); if (LOG.isLoggable(Level.FINE)) { LOG.info(containerInfo.name() + " is now running."); } } else { if (LOG.isLoggable(Level.FINE)) { LOG.info(containerInfo.name() + " was already running."); } } ContainerInfo info = new ContainerInfo(id, port); if (LOG.isLoggable(Level.FINE)) { LOG.fine("******" + info + "******"); } return info; } private static void predownloadImagesIfRequired() throws DockerException, InterruptedException { DockerClient client = getClient(); LOG.warning("Commencing download of images."); Collection<MappingInfo> images = getInstance().getMapping().values(); ProgressHandler handler = new LoggingBuildHandler(); for (MappingInfo image : images) { List<Image> foundImages = client.listImages(DockerClient.ListImagesParam.byName(image.getTarget())); if (! foundImages.isEmpty()) { LOG.warning(String.format("Skipping download for Image [%s] because it's already available.", image.getTarget())); continue; } client.pull(image.getTarget(), handler); } } private static DockerClient getClient() { return DefaultDockerClient.builder().uri(getInstance().getDockerRestApiUri()).build(); } /** * A Simple POJO that represents the newly spun off container, encapsulating the container Id and the port on which * the container is running. */ public static class ContainerInfo { private int port; private String containerId; ContainerInfo(String containerId, int port) { this.port = port; this.containerId = containerId; } public int getPort() { return port; } public String getContainerId() { return containerId; } @Override public String toString() { return String.format("%s running on %d", containerId, port); } } private static class DockerCleanup implements Runnable { private DockerClient client; DockerCleanup(DockerClient client) { this.client = client; } @Override public void run() { if (client != null) { client.close(); } } } }