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 }