1 /*
   2  * Copyright (c) 2017, 2018, 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.IOException;
  28 import java.nio.file.Files;
  29 import java.nio.file.FileVisitResult;
  30 import java.nio.file.Path;
  31 import java.nio.file.Paths;
  32 import java.nio.file.SimpleFileVisitor;
  33 import java.nio.file.StandardCopyOption;
  34 import java.nio.file.attribute.BasicFileAttributes;
  35 import java.util.ArrayList;
  36 import java.util.Collections;
  37 import java.util.List;
  38 import jdk.test.lib.Platform;
  39 import jdk.test.lib.Utils;
  40 import jdk.test.lib.process.OutputAnalyzer;
  41 import jdk.test.lib.process.ProcessTools;
  42 
  43 
  44 public class DockerTestUtils {
  45     private static final String FS = File.separator;
  46     private static boolean isDockerEngineAvailable = false;
  47     private static boolean wasDockerEngineChecked = false;
  48 
  49     // Diagnostics: set to true to enable more diagnostic info
  50     private static final boolean DEBUG = false;
  51 
  52     /**
  53      * Optimized check of whether the docker engine is available in a given
  54      * environment. Checks only once, then remembers the result in a singleton.
  55      *
  56      * @return true if docker engine is available
  57      * @throws Exception
  58      */
  59     public static boolean isDockerEngineAvailable() throws Exception {
  60         if (wasDockerEngineChecked)
  61             return isDockerEngineAvailable;
  62 
  63         isDockerEngineAvailable = isDockerEngineAvailableCheck();
  64         wasDockerEngineChecked = true;
  65         return isDockerEngineAvailable;
  66     }
  67 
  68 
  69     /**
  70      * Convenience method, will check if docker engine is available and usable;
  71      * will print the appropriate message when not available.
  72      *
  73      * @return true if docker engine is available
  74      * @throws Exception
  75      */
  76     public static boolean canTestDocker() throws Exception {
  77         if (isDockerEngineAvailable()) {
  78             return true;
  79         } else {
  80             System.out.println("Docker engine is not available on this system");
  81             System.out.println("This test is SKIPPED");
  82             return false;
  83         }
  84     }
  85 
  86 
  87     /**
  88      * Simple check - is docker engine available, accessible and usable.
  89      * Run basic docker command: 'docker ps' - list docker instances.
  90      * If docker engine is available and accesible then true is returned
  91      * and we can proceed with testing docker.
  92      *
  93      * @return true if docker engine is available and usable
  94      * @throws Exception
  95      */
  96     private static boolean isDockerEngineAvailableCheck() throws Exception {
  97         try {
  98             execute("docker", "ps")
  99                 .shouldHaveExitValue(0)
 100                 .shouldContain("CONTAINER")
 101                 .shouldContain("IMAGE");
 102         } catch (Exception e) {
 103             return false;
 104         }
 105         return true;
 106     }
 107 
 108 
 109     /**
 110      * Build a docker image that contains JDK under test.
 111      * The jdk will be placed under the "/jdk/" folder inside the docker file system.
 112      *
 113      * @param imageName     name of the image to be created, including version tag
 114      * @param dockerfile    name of the dockerfile residing in the test source;
 115      *                      we check for a platform specific dockerfile as well
 116      *                      and use this one in case it exists
 117      * @param buildDirName  name of the docker build/staging directory, which will
 118      *                      be created in the jtreg's scratch folder
 119      * @throws Exception
 120      */
 121     public static void
 122         buildJdkDockerImage(String imageName, String dockerfile, String buildDirName)
 123             throws Exception {
 124 
 125         Path buildDir = Paths.get(".", buildDirName);
 126         if (Files.exists(buildDir)) {
 127             throw new RuntimeException("The docker build directory already exists: " + buildDir);
 128         }
 129         // check for the existance of a platform specific docker file as well
 130         String platformSpecificDockerfile = dockerfile + "-" + Platform.getOsArch();
 131         if (Files.exists(Paths.get(Utils.TEST_SRC, platformSpecificDockerfile))) {
 132           dockerfile = platformSpecificDockerfile;
 133         }
 134 
 135         Path jdkSrcDir = Paths.get(Utils.TEST_JDK);
 136         Path jdkDstDir = buildDir.resolve("jdk");
 137 
 138         Files.createDirectories(jdkDstDir);
 139 
 140         // Copy JDK-under-test tree to the docker build directory.
 141         // This step is required for building a docker image.
 142         Files.walkFileTree(jdkSrcDir, new CopyFileVisitor(jdkSrcDir, jdkDstDir));
 143         buildDockerImage(imageName, Paths.get(Utils.TEST_SRC, dockerfile), buildDir);
 144     }
 145 
 146 
 147     /**
 148      * Build a docker image based on given docker file and docker build directory.
 149      *
 150      * @param imageName  name of the image to be created, including version tag
 151      * @param dockerfile  path to the Dockerfile to be used for building the docker
 152      *        image. The specified dockerfile will be copied to the docker build
 153      *        directory as 'Dockerfile'
 154      * @param buildDir  build directory; it should already contain all the content
 155      *        needed to build the docker image.
 156      * @throws Exception
 157      */
 158     public static void
 159         buildDockerImage(String imageName, Path dockerfile, Path buildDir) throws Exception {
 160 
 161         // Copy docker file to the build dir
 162         Files.copy(dockerfile, buildDir.resolve("Dockerfile"));
 163 
 164         // Build the docker
 165         execute("docker", "build", "--no-cache", "--tag", imageName, buildDir.toString())
 166             .shouldHaveExitValue(0)
 167             .shouldContain("Successfully built");
 168     }
 169 
 170 
 171     /**
 172      * Run Java inside the docker image with specified parameters and options.
 173      *
 174      * @param DockerRunOptions optins for running docker
 175      *
 176      * @return output of the run command
 177      * @throws Exception
 178      */
 179     public static OutputAnalyzer dockerRunJava(DockerRunOptions opts) throws Exception {
 180         ArrayList<String> cmd = new ArrayList<>();
 181 
 182         cmd.add("docker");
 183         cmd.add("run");
 184         if (opts.tty)
 185             cmd.add("--tty=true");
 186         if (opts.removeContainerAfterUse)
 187             cmd.add("--rm");
 188 
 189         cmd.addAll(opts.dockerOpts);
 190         cmd.add(opts.imageNameAndTag);
 191         cmd.add(opts.command);
 192 
 193         cmd.addAll(opts.javaOpts);
 194         if (opts.appendTestJavaOptions) {
 195             Collections.addAll(cmd, Utils.getTestJavaOpts());
 196         }
 197 
 198         cmd.add(opts.classToRun);
 199         cmd.addAll(opts.classParams);
 200 
 201         return execute(cmd);
 202     }
 203 
 204 
 205      /**
 206      * Remove docker image
 207      *
 208      * @param DockerRunOptions optins for running docker
 209      * @return output of the command
 210      * @throws Exception
 211      */
 212     public static OutputAnalyzer removeDockerImage(String imageNameAndTag) throws Exception {
 213         return execute("docker", "rmi", "--force", imageNameAndTag);
 214     }
 215 
 216 
 217 
 218     /**
 219      * Convenience method - express command as sequence of strings
 220      *
 221      * @param command to execute
 222      * @return The output from the process
 223      * @throws Exception
 224      */
 225     public static OutputAnalyzer execute(List<String> command) throws Exception {
 226         return execute(command.toArray(new String[command.size()]));
 227     }
 228 
 229 
 230     /**
 231      * Execute a specified command in a process, report diagnostic info.
 232      *
 233      * @param command to be executed
 234      * @return The output from the process
 235      * @throws Exception
 236      */
 237     public static OutputAnalyzer execute(String... command) throws Exception {
 238 
 239         ProcessBuilder pb = new ProcessBuilder(command);
 240         System.out.println("[COMMAND]\n" + Utils.getCommandLine(pb));
 241 
 242         long started = System.currentTimeMillis();
 243         OutputAnalyzer output = new OutputAnalyzer(pb.start());
 244 
 245         System.out.println("[ELAPSED: " + (System.currentTimeMillis() - started) + " ms]");
 246         System.out.println("[STDERR]\n" + output.getStderr());
 247         System.out.println("[STDOUT]\n" + output.getStdout());
 248 
 249         return output;
 250     }
 251 
 252 
 253     private static class CopyFileVisitor extends SimpleFileVisitor<Path> {
 254         private final Path src;
 255         private final Path dst;
 256 
 257         public CopyFileVisitor(Path src, Path dst) {
 258             this.src = src;
 259             this.dst = dst;
 260         }
 261 
 262 
 263         @Override
 264         public FileVisitResult preVisitDirectory(Path file,
 265                 BasicFileAttributes attrs) throws IOException {
 266             Path dstDir = dst.resolve(src.relativize(file));
 267             if (!dstDir.toFile().exists()) {
 268                 Files.createDirectories(dstDir);
 269             }
 270             return FileVisitResult.CONTINUE;
 271         }
 272 
 273 
 274         @Override
 275         public FileVisitResult visitFile(Path file,
 276                 BasicFileAttributes attrs) throws IOException {
 277             if (!file.toFile().isFile()) {
 278                 return FileVisitResult.CONTINUE;
 279             }
 280             Path dstFile = dst.resolve(src.relativize(file));
 281             Files.copy(file, dstFile, StandardCopyOption.COPY_ATTRIBUTES);
 282             return FileVisitResult.CONTINUE;
 283         }
 284     }
 285 }