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