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