/* * (C) Copyright 2015 Kurento (http://kurento.org/) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.kurento.test.docker; import static org.kurento.commons.PropertiesManager.getProperty; import static org.kurento.test.config.TestConfiguration.TEST_SELENIUM_TRANSPORT; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.io.Writer; import java.net.InterfaceAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.PosixFilePermissions; import java.util.Arrays; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.Random; import java.util.concurrent.TimeUnit; import org.kurento.commons.PropertiesManager; import org.kurento.commons.exception.KurentoException; import org.kurento.test.base.KurentoTest; import org.kurento.test.browser.BrowserType; import org.kurento.test.config.TestConfiguration; import org.kurento.test.utils.Shell; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.command.ExecCreateCmdResponse; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.exception.DockerClientException; import com.github.dockerjava.api.exception.NotFoundException; import com.github.dockerjava.api.model.AccessMode; import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.Frame; import com.github.dockerjava.api.model.Statistics; import com.github.dockerjava.api.model.Volume; import com.github.dockerjava.api.model.VolumesFrom; import com.github.dockerjava.core.DockerClientBuilder; import com.github.dockerjava.core.command.ExecStartResultCallback; import com.github.dockerjava.core.command.LogContainerResultCallback; import com.github.dockerjava.core.command.PullImageResultCallback; /** * Docker client for tests. * * @author Boni Garcia (bgarcia@gsyc.es) * @since 6.1.1 */ public class Docker implements Closeable { private static final Logger log = LoggerFactory.getLogger(Docker.class); private static final String DOCKER_SERVER_URL_PROPERTY = "docker.server.url"; private static final String DOCKER_SERVER_URL_DEFAULT = "unix:///var/run/docker.sock"; public static final String DOCKER_CONTAINER_NAME_PROPERTY = "docker.container.name"; private static final int WAIT_CONTAINER_POLL_TIME = 200; // milliseconds private static final int WAIT_CONTAINER_POLL_TIMEOUT = 10; // seconds private static Docker singleton = null; private static Boolean isRunningInContainer; private static String hostIp; private DockerClient client; private String containerName; private String dockerServerUrl; public static Docker getSingleton(String dockerServerUrl) { if (singleton == null) { synchronized (Docker.class) { if (singleton == null) { singleton = new Docker(dockerServerUrl); } } } return singleton; } public static Docker getSingleton() { return getSingleton( PropertiesManager.getProperty(DOCKER_SERVER_URL_PROPERTY, getDefaultDockerServerUrl())); } private static String getDefaultDockerServerUrl() { return DOCKER_SERVER_URL_DEFAULT; } public Docker(String dockerServerUrl) { this.dockerServerUrl = dockerServerUrl; } public boolean isRunningInContainer() { return isRunningInContainerInternal(); } private static synchronized boolean isRunningInContainerInternal() { if (isRunningInContainer == null) { try (BufferedReader br = Files.newBufferedReader(Paths.get("/proc/1/cgroup"), StandardCharsets.UTF_8)) { String line = null; while ((line = br.readLine()) != null) { if (!line.endsWith("/docker")) { return false; } } isRunningInContainer = true; } catch (IOException e) { isRunningInContainer = false; } } return isRunningInContainer; } private static synchronized String getHostIp() { if (hostIp == null) { if (isRunningInContainerInternal()) { try { String ipRoute = Shell.runAndWait("sh", "-c", "/sbin/ip route"); String[] tokens = ipRoute.split("\\s"); hostIp = tokens[2]; } catch (Exception e) { throw new DockerClientException("Exception executing /sbin/ip route", e); } } else { hostIp = "127.0.0.1"; } } log.debug("Host IP is {}", hostIp); return hostIp; } public boolean isRunningContainer(String containerName) { boolean isRunning = false; if (existsContainer(containerName)) { isRunning = inspectContainer(containerName).getState().getRunning(); log.trace("Container {} is running: {}", containerName, isRunning); } return isRunning; } public boolean existsContainer(String containerName) { boolean exists = true; try { getClient().inspectContainerCmd(containerName).exec(); log.trace("Container {} already exist", containerName); } catch (NotFoundException e) { log.trace("Container {} does not exist", containerName); exists = false; } return exists; } public boolean existsImage(String imageName) { boolean exists = true; try { getClient().inspectImageCmd(imageName).exec(); log.trace("Image {} exists", imageName); } catch (NotFoundException e) { log.trace("Image {} does not exist", imageName); exists = false; } return exists; } public void createContainer(String imageId, String containerName, boolean mountFolders, String... env) { if (!existsContainer(containerName)) { pullImageIfNecessary(imageId, false); log.debug("Creating container {}", containerName); CreateContainerCmd createContainerCmd = getClient().createContainerCmd(imageId).withName(containerName).withEnv(env) .withVolumes(new Volume("/var/run/docker.sock")); if (mountFolders) { mountDefaultFolders(createContainerCmd); } createContainerCmd.exec(); log.debug("Container {} started...", containerName); } else { log.debug("Container {} already exists", containerName); } } public void mountDefaultFolders(CreateContainerCmd createContainerCmd) { mountDefaultFolders(createContainerCmd, null); } public void mountDefaultFolders(CreateContainerCmd createContainerCmd, String configFilePath) { if (isRunningInContainer()) { createContainerCmd.withVolumesFrom(new VolumesFrom(getContainerId())); if (configFilePath != null) { String workspace = PropertiesManager.getProperty(TestConfiguration.TEST_WORKSPACE_PROP, TestConfiguration.TEST_WORKSPACE_DEFAULT); String workspaceHost = PropertiesManager.getProperty(TestConfiguration.TEST_WORKSPACE_HOST_PROP, TestConfiguration.TEST_WORKSPACE_HOST_DEFAULT); String hostConfigFilePath = Paths.get(workspaceHost) .resolve(Paths.get(workspace).relativize(Paths.get(configFilePath))).toString(); log.debug("Config file volume {}", hostConfigFilePath); Volume configVol = new Volume("/opt/selenium/config.json"); createContainerCmd.withVolumes(configVol) .withBinds(new Bind(hostConfigFilePath, configVol)); } } else { String testFilesPath = KurentoTest.getTestFilesDiskPath(); Volume testFilesVolume = new Volume(testFilesPath); String workspacePath = Paths.get(KurentoTest.getTestDir()).toAbsolutePath().toString(); Volume workspaceVolume = new Volume(workspacePath); Volume configVol = new Volume("/opt/selenium/config.json"); Volume dockerSock = new Volume("/var/run/docker.sock"); if (configFilePath != null) { createContainerCmd.withVolumes(testFilesVolume, workspaceVolume, configVol, dockerSock) .withBinds( new Bind(testFilesPath, testFilesVolume, AccessMode.ro), new Bind(workspacePath, workspaceVolume, AccessMode.rw), new Bind(configFilePath, configVol)); } else { createContainerCmd.withVolumes(testFilesVolume, workspaceVolume, dockerSock).withBinds( new Bind(testFilesPath, testFilesVolume, AccessMode.ro), new Bind(workspacePath, workspaceVolume, AccessMode.rw)); } } } public void pullImageIfNecessary(String imageId, boolean force) { if (force || !existsImage(imageId)) { log.debug("Pulling Docker image {} ... please be patient until the process finishes", imageId); getClient().pullImageCmd(imageId).exec(new PullImageResultCallback()).awaitSuccess(); log.debug("Image {} downloaded", imageId); } else { log.debug("Image {} already exists", imageId); } } public InspectContainerResponse inspectContainer(String containerName) { return getClient().inspectContainerCmd(containerName).exec(); } public void startContainer(String containerName) { if (!isRunningContainer(containerName)) { log.debug("Starting container {}", containerName); getClient().startContainerCmd(containerName).exec(); log.debug("Started container {}", containerName); } else { log.debug("Container {} is already started", containerName); } } @Override public void close() { if (client != null) { try { getClient().close(); } catch (IOException e) { log.error("Exception closing Docker client", e); } } } public DockerClient getClient() { if (client == null) { synchronized (this) { if (client == null) { client = DockerClientBuilder.getInstance(dockerServerUrl).build(); } } } return client; } public void stopContainers(String... containerNames) { for (String containerName : containerNames) { stopContainer(containerName); } } public void stopContainer(String containerName) { if (isRunningContainer(containerName)) { log.debug("Stopping container {}", containerName); getClient().stopContainerCmd(containerName).exec(); } else { log.debug("Container {} is not running", containerName); } } public void removeContainers(String... containerNames) { for (String containerName : containerNames) { removeContainer(containerName); } } public void removeContainer(String containerName) { if (existsContainer(containerName)) { log.debug("Removing container {}", containerName); boolean removed = false; int count = 0; do { try { count++; getClient().removeContainerCmd(containerName).withRemoveVolumes(true).exec(); log.debug("*** Only for debuggin: After Docker.removeContainer({}). Times: {}", containerName, count); removed = true; } catch (Throwable e) { if (count == 10) { log.error("*** Only for debugging: Exception {} -> Docker.removeContainer({}).", containerName, e.getMessage()); } try { log.debug("Waiting for removing {}. Times: {}", containerName, count); Thread.sleep(WAIT_CONTAINER_POLL_TIMEOUT); } catch (InterruptedException e1) { // Nothing todo } } } while (!removed && count <= 10); } } public void stopAndRemoveContainer(String containerName) { stopContainer(containerName); removeContainer(containerName); } public void stopAndRemoveContainers(String... containerNames) { for (String containerName : containerNames) { stopAndRemoveContainer(containerName); } } public synchronized String startHub(String hubName, String imageId) { // Create hub if not exist createContainer(imageId, hubName, false, "GRID_TIMEOUT=3600000"); // Start hub if stopped startContainer(hubName); // Read IP address String hubIp = inspectContainer(hubName).getNetworkSettings().getIpAddress(); log.debug("Hub started on IP address: {}", hubIp); return hubIp; } public void startNode(String id, BrowserType browserType, String nodeName, String imageId, String hubIp) { // Create node if not exist if (!existsContainer(nodeName)) { pullImageIfNecessary(imageId, false); log.debug("Creating container {}", nodeName); CreateContainerCmd createContainerCmd = getClient().createContainerCmd(imageId).withName(nodeName); String configFile = generateConfigFile(id, browserType); mountDefaultFolders(createContainerCmd, configFile); createContainerCmd.withEnv(new String[] { "HUB_PORT_4444_TCP_ADDR=" + hubIp }); createContainerCmd.exec(); log.debug("Container {} started...", nodeName); } else { log.debug("Container {} already exists", nodeName); } // Start node if stopped startContainer(nodeName); } public void startNode(String id, BrowserType browserType, String nodeName, String imageId, String hubIp, String containerIp) { // Create node if not exist if (!existsContainer(nodeName)) { pullImageIfNecessary(imageId, false); log.debug("Creating container {}", nodeName); CreateContainerCmd createContainerCmd = getClient().createContainerCmd(imageId).withName(nodeName); String configFile = generateConfigFile(id, browserType); mountDefaultFolders(createContainerCmd, configFile); createContainerCmd.withNetworkMode("none"); Map<String, String> labels = new HashMap<>(); labels.put("KurentoDnat", "true"); labels.put("Transport", getProperty(TEST_SELENIUM_TRANSPORT)); labels.put("IpAddress", containerIp); createContainerCmd.withLabels(labels); createContainerCmd.withEnv(new String[] { "HUB_PORT_4444_TCP_ADDR=" + hubIp, "REMOTE_HOST=http://" + containerIp + ":5555" }); createContainerCmd.exec(); log.debug("Container {} started...", nodeName); } else { log.debug("Container {} already exists", nodeName); } // Start node if stopped startContainer(nodeName); } private String generateConfigFile(String id, BrowserType browserType) { try { String workspace = PropertiesManager.getProperty(TestConfiguration.TEST_WORKSPACE_PROP, TestConfiguration.TEST_WORKSPACE_DEFAULT); Path config = Files.createTempFile(Paths.get(workspace), "", "-config.json", PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-r--r--"))); String browserName1; String browserName2; if (browserType == BrowserType.CHROME) { browserName1 = "*googlechrome"; browserName2 = "chrome"; } else if (browserType == BrowserType.FIREFOX) { browserName1 = "*firefox"; browserName2 = "firefox"; } else { throw new KurentoException("Unsupported browser type: " + browserType); } try (Writer w = Files.newBufferedWriter(config, StandardCharsets.UTF_8)) { w.write("{\n" + " \"capabilities\": [\n" + " {\n" + " \"browserName\": \"" + browserName1 + "\",\n" + " \"maxInstances\": 1,\n" + " \"seleniumProtocol\": \"Selenium\",\n" + " \"applicationName\": \"" + id + "\"\n" + " },\n" + " {\n" + " \"browserName\": \"" + browserName2 + "\",\n" + " \"maxInstances\": 1,\n" + " \"seleniumProtocol\": \"WebDriver\",\n" + " \"applicationName\": \"" + id + "\"\n" + " }\n" + " ],\n" + " \"configuration\": {\n" + " \"proxy\": \"org.openqa.grid.selenium.proxy.DefaultRemoteProxy\",\n" + " \"maxSession\": 1,\n" + " \"port\": 5555,\n" + " \"register\": true,\n" + " \"registerCycle\": 5000\n" + " }\n" + "}"); } return config.toAbsolutePath().toString(); } catch (IOException e) { throw new KurentoException("Exception creating config file", e); } } public void startAndWaitNode(String id, BrowserType browserType, String nodeName, String imageId, String hubIp) { startNode(id, browserType, nodeName, imageId, hubIp); waitForContainer(nodeName); } public void startAndWaitNode(String id, BrowserType browserType, String nodeName, String imageId, String hubIp, String containerIp) { startNode(id, browserType, nodeName, imageId, hubIp, containerIp); waitForContainer(nodeName); } public String startAndWaitHub(String hubName, String imageId) { String hubIp = startHub(hubName, imageId); waitForContainer(hubName); return hubIp; } public void waitForContainer(String containerName) { boolean isRunning = false; long timeoutMs = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(WAIT_CONTAINER_POLL_TIMEOUT); do { isRunning = isRunningContainer(containerName); if (!isRunning) { // Check timeout if (System.currentTimeMillis() > timeoutMs) { throw new KurentoException("Timeout of " + WAIT_CONTAINER_POLL_TIMEOUT + " seconds waiting for container " + containerName); } try { // Wait WAIT_HUB_POLL_TIME ms log.debug("Container {} is not still running ... waiting {} ms", containerName, WAIT_CONTAINER_POLL_TIME); Thread.sleep(WAIT_CONTAINER_POLL_TIME); } catch (InterruptedException e) { log.error("Exception waiting for hub"); } } } while (!isRunning); } public String getContainerId() { try { BufferedReader br = Files.newBufferedReader(Paths.get("/proc/self/cgroup"), StandardCharsets.UTF_8); String line = null; while ((line = br.readLine()) != null) { log.debug(line); if (line.contains("docker")) { return line.substring(line.lastIndexOf('/') + 1, line.length()); } } throw new DockerClientException("Exception obtaining containerId. " + "The file /proc/self/cgroup doesn't contain a line with 'docker'"); } catch (IOException e) { throw new DockerClientException( "Exception obtaining containerId. " + "Exception reading file /proc/self/cgroup", e); } } public String getContainerName() { if (!isRunningInContainer()) { throw new DockerClientException("Can't obtain container name if not running in container"); } if (containerName == null) { containerName = System.getProperty(DOCKER_CONTAINER_NAME_PROPERTY); if (containerName == null) { String containerId = getContainerId(); containerName = inspectContainer(containerId).getName(); containerName = containerName.substring(1); } } return containerName; } public String getContainerIpAddress() { if (isRunningInContainer()) { String ipAddr = inspectContainer(getContainerName()).getNetworkSettings().getIpAddress(); log.debug("Docker container IP address {}", ipAddr); return ipAddr; } else { throw new DockerClientException( "Can't obtain container ip address if not running in container"); } } public String getHostIpForContainers() { try { Enumeration<NetworkInterface> b = NetworkInterface.getNetworkInterfaces(); while (b.hasMoreElements()) { NetworkInterface iface = b.nextElement(); if (iface.getName().contains("docker")) { for (InterfaceAddress f : iface.getInterfaceAddresses()) { if (f.getAddress().isSiteLocalAddress()) { String addr = f.getAddress().toString(); log.debug("Host IP for container is {}", addr); return addr; } } } } } catch (SocketException e) { // This shouldn't happen log.warn("Exception getting docker address", e); } return null; } /** * Return an ip address according with some parameters for testing Ice * * @param container * @param webRtcCandidate * @param isKmsDnat * @param isSeleniumDnat * @param isUpdTransport * @return */ public String generateIpAddressForContainer() { String baseIpAddress = "172.17"; String ipAddress = ""; Random random = new Random(); Integer x; Integer y; String output = ""; do { x = random.nextInt((240 - 1) + 1) + 1; y = random.nextInt((240 - 1) + 1) + 1; ipAddress = baseIpAddress + "." + x + "." + y; output = Shell.runAndWaitString("ping -c 1 " + ipAddress); } while (!output.contains("Destination Host Unreachable")); log.debug("Ip address generated: {}", ipAddress); return ipAddress; } public void downloadLog(String containerName, Path file) throws IOException { LogContainerRetrieverCallback loggingCallback = new LogContainerRetrieverCallback(file); getClient().logContainerCmd(containerName).withStdErr(true).withStdOut(true) .exec(loggingCallback); try { loggingCallback.awaitCompletion(); } catch (InterruptedException e) { log.warn("Interrupted while downloading logs for container {}", containerName); } } public static class LogContainerRetrieverCallback extends LogContainerResultCallback { private PrintWriter pw; public LogContainerRetrieverCallback(Path file) throws IOException { pw = new PrintWriter(Files.newBufferedWriter(file, StandardCharsets.UTF_8)); } @Override public void onNext(Frame frame) { pw.append(new String(frame.getPayload())); super.onNext(frame); } @Override public void onComplete() { pw.close(); super.onComplete(); } } public Statistics getStatistics(String containerId) { FirstObjectResultCallback<Statistics> resultCallback = new FirstObjectResultCallback<>(); try { return getClient().statsCmd(containerId).exec(resultCallback).waitForObject(); } catch (InterruptedException e) { throw new KurentoException("Interrupted while waiting for statistics"); } } public String execCommand(String containerId, String... command) { ExecCreateCmdResponse exec = client.execCreateCmd(containerId).withCmd(command).withTty(false) .withAttachStdin(true).withAttachStdout(true).withAttachStderr(true).exec(); OutputStream outputStream = new ByteArrayOutputStream(); String output = null; try { client.execStartCmd(exec.getId()).withDetach(false).withTty(true) .exec(new ExecStartResultCallback(outputStream, System.err)).awaitCompletion(); output = outputStream.toString();// IOUtils.toString(outputStream, Charset.defaultCharset()); } catch (InterruptedException e) { log.warn("Exception executing command {} on container {}", Arrays.toString(command), containerId, e); } return output; } }