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