1 /*
   2  * Copyright (c) 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 com.oracle.java.testlibrary;
  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 
  39 import com.oracle.java.testlibrary.Utils;
  40 import com.oracle.java.testlibrary.Platform;
  41 import com.oracle.java.testlibrary.OutputAnalyzer;
  42 import com.oracle.java.testlibrary.ProcessTools;
  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         Path buildDir = Paths.get(".", buildDirName);
 125         if (Files.exists(buildDir)) {
 126             throw new RuntimeException("The docker build directory already exists: " + buildDir);
 127         }
 128         // check for the existance of a platform specific docker file as well
 129         String platformSpecificDockerfile = dockerfile + "-" + Platform.getOsArch();
 130         if (Files.exists(Paths.get(Utils.TEST_SRC, platformSpecificDockerfile))) {
 131           dockerfile = platformSpecificDockerfile;
 132         }
 133 
 134         Path jdkSrcDir = Paths.get(Utils.TEST_JDK);
 135         Path jdkDstDir = buildDir.resolve("jdk");
 136 
 137         Files.createDirectories(jdkDstDir);
 138 
 139         // Copy JDK-under-test tree to the docker build directory.
 140         // This step is required for building a docker image.
 141         Files.walkFileTree(jdkSrcDir, new CopyFileVisitor(jdkSrcDir, jdkDstDir));
 142         buildDockerImage(imageName, Paths.get(Utils.TEST_SRC, dockerfile), buildDir);
 143     }
 144 
 145 
 146     /**
 147      * Build a docker image based on given docker file and docker build directory.
 148      *
 149      * @param imageName  name of the image to be created, including version tag
 150      * @param dockerfile  path to the Dockerfile to be used for building the docker
 151      *        image. The specified dockerfile will be copied to the docker build
 152      *        directory as 'Dockerfile'
 153      * @param buildDir  build directory; it should already contain all the content
 154      *        needed to build the docker image.
 155      * @throws Exception
 156      */
 157     public static void
 158         buildDockerImage(String imageName, Path dockerfile, Path buildDir) throws Exception {
 159         // Copy docker file to the build dir
 160         Files.copy(dockerfile, buildDir.resolve("Dockerfile"));
 161 
 162         // Build the docker
 163         execute("docker", "build", "--no-cache", "--tag", imageName, buildDir.toString())
 164             .shouldHaveExitValue(0)
 165             .shouldContain("Successfully built");
 166     }
 167 
 168 
 169     /**
 170      * Build the docker command to run java inside a container
 171      *
 172      * @param DockerRunOptions optins for running docker
 173      *
 174      * @return command
 175      * @throws Exception
 176      */
 177     public static List<String> buildJavaCommand(DockerRunOptions opts) throws Exception {
 178         List<String> cmd = new ArrayList<>();
 179 
 180         cmd.add("docker");
 181         cmd.add("run");
 182         if (opts.tty)
 183             cmd.add("--tty=true");
 184         if (opts.removeContainerAfterUse)
 185             cmd.add("--rm");
 186 
 187         cmd.addAll(opts.dockerOpts);
 188         cmd.add(opts.imageNameAndTag);
 189         cmd.add(opts.command);
 190 
 191         cmd.addAll(opts.javaOpts);
 192         if (opts.appendTestJavaOptions) {
 193             Collections.addAll(cmd, Utils.getTestJavaOpts());
 194         }
 195 
 196         cmd.add(opts.classToRun);
 197         cmd.addAll(opts.classParams);
 198         return cmd;
 199     }
 200 
 201     /**
 202      * Run Java inside the docker image with specified parameters and options.
 203      *
 204      * @param DockerRunOptions optins for running docker
 205      *
 206      * @return output of the run command
 207      * @throws Exception
 208      */
 209     public static OutputAnalyzer dockerRunJava(DockerRunOptions opts) throws Exception {
 210         return execute(buildJavaCommand(opts));
 211     }
 212 
 213      /**
 214      * Remove docker image
 215      *
 216      * @param DockerRunOptions optins for running docker
 217      * @return output of the command
 218      * @throws Exception
 219      */
 220     public static OutputAnalyzer removeDockerImage(String imageNameAndTag) throws Exception {
 221         return execute("docker", "rmi", "--force", imageNameAndTag);
 222     }
 223 
 224 
 225 
 226     /**
 227      * Convenience method - express command as sequence of strings
 228      *
 229      * @param command to execute
 230      * @return The output from the process
 231      * @throws Exception
 232      */
 233     public static OutputAnalyzer execute(List<String> command) throws Exception {
 234         return execute(command.toArray(new String[command.size()]));
 235     }
 236 
 237 
 238     /**
 239      * Execute a specified command in a process, report diagnostic info.
 240      *
 241      * @param command to be executed
 242      * @return The output from the process
 243      * @throws Exception
 244      */
 245     public static OutputAnalyzer execute(String... command) throws Exception {
 246 
 247         ProcessBuilder pb = new ProcessBuilder(command);
 248         System.out.println("[COMMAND]\n" + Utils.getCommandLine(pb));
 249 
 250         long started = System.currentTimeMillis();
 251         OutputAnalyzer output = new OutputAnalyzer(pb.start());
 252 
 253         System.out.println("[ELAPSED: " + (System.currentTimeMillis() - started) + " ms]");
 254         System.out.println("[STDERR]\n" + output.getStderr());
 255         System.out.println("[STDOUT]\n" + output.getStdout());
 256 
 257         return output;
 258     }
 259 
 260 
 261     private static class CopyFileVisitor extends SimpleFileVisitor<Path> {
 262         private final Path src;
 263         private final Path dst;
 264 
 265         public CopyFileVisitor(Path src, Path dst) {
 266             this.src = src;
 267             this.dst = dst;
 268         }
 269 
 270 
 271         @Override
 272         public FileVisitResult preVisitDirectory(Path file,
 273                 BasicFileAttributes attrs) throws IOException {
 274             Path dstDir = dst.resolve(src.relativize(file));
 275             if (!dstDir.toFile().exists()) {
 276                 Files.createDirectories(dstDir);
 277             }
 278             return FileVisitResult.CONTINUE;
 279         }
 280 
 281 
 282         @Override
 283         public FileVisitResult visitFile(Path file,
 284                 BasicFileAttributes attrs) throws IOException {
 285             if (!file.toFile().isFile()) {
 286                 return FileVisitResult.CONTINUE;
 287             }
 288             Path dstFile = dst.resolve(src.relativize(file));
 289             Files.copy(file, dstFile, StandardCopyOption.COPY_ATTRIBUTES);
 290             return FileVisitResult.CONTINUE;
 291         }
 292     }
 293 }