1 /*
   2  * Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 package jdk.test.lib.containers.docker;
  25 
  26 import java.io.File;
  27 import java.io.FileWriter;
  28 import java.io.IOException;
  29 import java.nio.file.Files;
  30 import java.nio.file.FileVisitResult;
  31 import java.nio.file.Path;
  32 import java.nio.file.Paths;
  33 import java.nio.file.SimpleFileVisitor;
  34 import java.nio.file.StandardCopyOption;
  35 import java.nio.file.attribute.BasicFileAttributes;
  36 import java.util.Arrays;
  37 import java.util.ArrayList;
  38 import java.util.Collections;
  39 import java.util.List;
  40 import jdk.test.lib.Utils;
  41 import jdk.test.lib.process.OutputAnalyzer;
  42 import jdk.test.lib.process.ProcessTools;
  43 import jtreg.SkippedException;
  44 
  45 
  46 public class DockerTestUtils {
  47     private static final String FS = File.separator;
  48     private static boolean isDockerEngineAvailable = false;
  49     private static boolean wasDockerEngineChecked = false;
  50 
  51     // Specifies how many lines to copy from child STDOUT to main test output.
  52     // Having too many lines in the main test output will result
  53     // in JT harness trimming the output, and can lead to loss of useful
  54     // diagnostic information.
  55     private static final int MAX_LINES_TO_COPY_FOR_CHILD_STDOUT = 100;
  56 
  57     // Use this property to specify docker location on your system.
  58     // E.g.: "/usr/local/bin/docker".
  59     private static final String DOCKER_COMMAND =
  60         System.getProperty("jdk.test.docker.command", "docker");
  61 
  62     // Set this property to true to retain image after test. By default
  63     // images are removed after test execution completes.
  64     // Retaining the image can be useful for diagnostics and image inspection.
  65     // E.g.: start image interactively: docker run -it <IMAGE_NAME>.
  66     public static final boolean RETAIN_IMAGE_AFTER_TEST =
  67         Boolean.getBoolean("jdk.test.docker.retain.image");
  68 
  69     // Path to a JDK under test.
  70     // This may be useful when developing tests on non-Linux platforms.
  71     public static final String JDK_UNDER_TEST =
  72         System.getProperty("jdk.test.docker.jdk", Utils.TEST_JDK);
  73 
  74 
  75     /**
  76      * Optimized check of whether the docker engine is available in a given
  77      * environment. Checks only once, then remembers the result in a singleton.
  78      *
  79      * @return true if docker engine is available
  80      * @throws Exception
  81      */
  82     public static boolean isDockerEngineAvailable() throws Exception {
  83         if (wasDockerEngineChecked)
  84             return isDockerEngineAvailable;
  85 
  86         isDockerEngineAvailable = isDockerEngineAvailableCheck();
  87         wasDockerEngineChecked = true;
  88         return isDockerEngineAvailable;
  89     }
  90 
  91 
  92     /**
  93      * Convenience method, will check if docker engine is available and usable;
  94      * will print the appropriate message when not available.
  95      *
  96      * @return true if docker engine is available
  97      * @throws Exception
  98      */
  99     public static boolean canTestDocker() throws Exception {
 100         if (isDockerEngineAvailable()) {
 101             return true;
 102         } else {
 103             throw new SkippedException("Docker engine is not available on this system");
 104         }
 105     }
 106 
 107 
 108     /**
 109      * Simple check - is docker engine available, accessible and usable.
 110      * Run basic docker command: 'docker ps' - list docker instances.
 111      * If docker engine is available and accesible then true is returned
 112      * and we can proceed with testing docker.
 113      *
 114      * @return true if docker engine is available and usable
 115      * @throws Exception
 116      */
 117     private static boolean isDockerEngineAvailableCheck() throws Exception {
 118         try {
 119             execute(DOCKER_COMMAND, "ps")
 120                 .shouldHaveExitValue(0)
 121                 .shouldContain("CONTAINER")
 122                 .shouldContain("IMAGE");
 123         } catch (Exception e) {
 124             return false;
 125         }
 126         return true;
 127     }
 128 
 129 
 130     /**
 131      * Build a docker image that contains JDK under test.
 132      * The jdk will be placed under the "/jdk/" folder inside the docker file system.
 133      *
 134      * @param imageName     name of the image to be created, including version tag
 135      * @param dockerfile    name of the dockerfile residing in the test source;
 136      *                      we check for a platform specific dockerfile as well
 137      *                      and use this one in case it exists
 138      * @param buildDirName  name of the docker build/staging directory, which will
 139      *                      be created in the jtreg's scratch folder
 140      * @throws Exception
 141      */
 142     public static void
 143         buildJdkDockerImage(String imageName, String dockerfile, String buildDirName)
 144             throws Exception {
 145 
 146         Path buildDir = Paths.get(".", buildDirName);
 147         if (Files.exists(buildDir)) {
 148             throw new RuntimeException("The docker build directory already exists: " + buildDir);
 149         }
 150 
 151         Path jdkSrcDir = Paths.get(JDK_UNDER_TEST);
 152         Path jdkDstDir = buildDir.resolve("jdk");
 153 
 154         Files.createDirectories(jdkDstDir);
 155 
 156         // Copy JDK-under-test tree to the docker build directory.
 157         // This step is required for building a docker image.
 158         Files.walkFileTree(jdkSrcDir, new CopyFileVisitor(jdkSrcDir, jdkDstDir));
 159         buildDockerImage(imageName, Paths.get(Utils.TEST_SRC, dockerfile), buildDir);
 160     }
 161 
 162 
 163     /**
 164      * Build a docker image based on given docker file and docker build directory.
 165      *
 166      * @param imageName  name of the image to be created, including version tag
 167      * @param dockerfile  path to the Dockerfile to be used for building the docker
 168      *        image. The specified dockerfile will be copied to the docker build
 169      *        directory as 'Dockerfile'
 170      * @param buildDir  build directory; it should already contain all the content
 171      *        needed to build the docker image.
 172      * @throws Exception
 173      */
 174     public static void
 175         buildDockerImage(String imageName, Path dockerfile, Path buildDir) throws Exception {
 176 
 177         generateDockerFile(buildDir.resolve("Dockerfile"),
 178                            DockerfileConfig.getBaseImageName(),
 179                            DockerfileConfig.getBaseImageVersion());
 180         try {
 181             // Build the docker
 182             execute(DOCKER_COMMAND, "build", "--no-cache", "--tag", imageName, buildDir.toString())
 183                 .shouldHaveExitValue(0)
 184                 .shouldContain("Successfully built");
 185         } catch (Exception e) {
 186             // If docker image building fails there is a good chance it happens due to environment and/or
 187             // configuration other than product failure. Throw jtreg skipped exception in such case
 188             // instead of failing the test.
 189             throw new SkippedException("Building docker image failed. Details: \n" + e.getMessage());
 190         }
 191     }
 192 
 193 
 194     /**
 195      * Build the docker command to run java inside a container
 196      *
 197      * @param DockerRunOptions optins for running docker
 198      *
 199      * @return command
 200      * @throws Exception
 201      */
 202     public static List<String> buildJavaCommand(DockerRunOptions opts) throws Exception {
 203         List<String> cmd = new ArrayList<>();
 204 
 205         cmd.add(DOCKER_COMMAND);
 206         cmd.add("run");
 207         if (opts.tty)
 208             cmd.add("--tty=true");
 209         if (opts.removeContainerAfterUse)
 210             cmd.add("--rm");
 211 
 212         cmd.addAll(opts.dockerOpts);
 213         cmd.add(opts.imageNameAndTag);
 214         cmd.add(opts.command);
 215 
 216         cmd.addAll(opts.javaOpts);
 217         if (opts.appendTestJavaOptions) {
 218             Collections.addAll(cmd, Utils.getTestJavaOpts());
 219         }
 220 
 221         cmd.add(opts.classToRun);
 222         cmd.addAll(opts.classParams);
 223 
 224         return cmd;
 225     }
 226 
 227     /**
 228      * Run Java inside the docker image with specified parameters and options.
 229      *
 230      * @param DockerRunOptions optins for running docker
 231      *
 232      * @return output of the run command
 233      * @throws Exception
 234      */
 235     public static OutputAnalyzer dockerRunJava(DockerRunOptions opts) throws Exception {
 236         return execute(buildJavaCommand(opts));
 237     }
 238 
 239 
 240      /**
 241      * Remove docker image
 242      *
 243      * @param DockerRunOptions optins for running docker
 244      * @throws Exception
 245      */
 246     public static void removeDockerImage(String imageNameAndTag) throws Exception {
 247             execute(DOCKER_COMMAND, "rmi", "--force", imageNameAndTag);
 248     }
 249 
 250 
 251 
 252     /**
 253      * Convenience method - express command as sequence of strings
 254      *
 255      * @param command to execute
 256      * @return The output from the process
 257      * @throws Exception
 258      */
 259     public static OutputAnalyzer execute(List<String> command) throws Exception {
 260         return execute(command.toArray(new String[command.size()]));
 261     }
 262 
 263 
 264     /**
 265      * Execute a specified command in a process, report diagnostic info.
 266      *
 267      * @param command to be executed
 268      * @return The output from the process
 269      * @throws Exception
 270      */
 271     public static OutputAnalyzer execute(String... command) throws Exception {
 272 
 273         ProcessBuilder pb = new ProcessBuilder(command);
 274         System.out.println("[COMMAND]\n" + Utils.getCommandLine(pb));
 275 
 276         long started = System.currentTimeMillis();
 277         Process p = pb.start();
 278         long pid = p.pid();
 279         OutputAnalyzer output = new OutputAnalyzer(p);
 280 
 281         String stdoutLogFile = String.format("docker-stdout-%d.log", pid);
 282         System.out.println("[ELAPSED: " + (System.currentTimeMillis() - started) + " ms]");
 283         System.out.println("[STDERR]\n" + output.getStderr());
 284         System.out.println("[STDOUT]\n" +
 285                            trimLines(output.getStdout(),MAX_LINES_TO_COPY_FOR_CHILD_STDOUT));
 286         System.out.printf("Child process STDOUT is trimmed to %d lines \n",
 287                            MAX_LINES_TO_COPY_FOR_CHILD_STDOUT);
 288         writeOutputToFile(output.getStdout(), stdoutLogFile);
 289         System.out.println("Full child process STDOUT was saved to " + stdoutLogFile);
 290 
 291         return output;
 292     }
 293 
 294 
 295     private static void writeOutputToFile(String output, String fileName) throws Exception {
 296         try (FileWriter fw = new FileWriter(fileName)) {
 297             fw.write(output, 0, output.length());
 298         }
 299     }
 300 
 301 
 302     private static String trimLines(String buffer, int nrOfLines) {
 303         List<String> l = Arrays.asList(buffer.split("\\R"));
 304         if (l.size() < nrOfLines) {
 305             return buffer;
 306         }
 307 
 308         return String.join("\n", l.subList(0, nrOfLines));
 309     }
 310 
 311 
 312     private static void generateDockerFile(Path dockerfile, String baseImage,
 313                                            String baseImageVersion) throws Exception {
 314         String template =
 315             "FROM %s:%s\n" +
 316             "COPY /jdk /jdk\n" +
 317             "ENV JAVA_HOME=/jdk\n" +
 318             "CMD [\"/bin/bash\"]\n";
 319         String dockerFileStr = String.format(template, baseImage, baseImageVersion);
 320         Files.writeString(dockerfile, dockerFileStr);
 321     }
 322 
 323 
 324     private static class CopyFileVisitor extends SimpleFileVisitor<Path> {
 325         private final Path src;
 326         private final Path dst;
 327 
 328         public CopyFileVisitor(Path src, Path dst) {
 329             this.src = src;
 330             this.dst = dst;
 331         }
 332 
 333 
 334         @Override
 335         public FileVisitResult preVisitDirectory(Path file,
 336                 BasicFileAttributes attrs) throws IOException {
 337             Path dstDir = dst.resolve(src.relativize(file));
 338             if (!dstDir.toFile().exists()) {
 339                 Files.createDirectories(dstDir);
 340             }
 341             return FileVisitResult.CONTINUE;
 342         }
 343 
 344 
 345         @Override
 346         public FileVisitResult visitFile(Path file,
 347                 BasicFileAttributes attrs) throws IOException {
 348             if (!file.toFile().isFile()) {
 349                 return FileVisitResult.CONTINUE;
 350             }
 351             Path dstFile = dst.resolve(src.relativize(file));
 352             Files.copy(file, dstFile, StandardCopyOption.COPY_ATTRIBUTES);
 353             return FileVisitResult.CONTINUE;
 354         }
 355     }
 356 }