-
Notifications
You must be signed in to change notification settings - Fork 5.2k
/
DockerTestUtils.java
362 lines (307 loc) · 12.8 KB
/
DockerTestUtils.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
/*
* Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.test.lib.containers.docker;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import jdk.test.lib.Container;
import jdk.test.lib.Utils;
import jdk.test.lib.process.OutputAnalyzer;
import jtreg.SkippedException;
public class DockerTestUtils {
private static boolean isDockerEngineAvailable = false;
private static boolean wasDockerEngineChecked = false;
// Specifies how many lines to copy from child STDOUT to main test output.
// Having too many lines in the main test output will result
// in JT harness trimming the output, and can lead to loss of useful
// diagnostic information.
private static final int MAX_LINES_TO_COPY_FOR_CHILD_STDOUT = 100;
// Set this property to true to retain image after test. By default
// images are removed after test execution completes.
// Retaining the image can be useful for diagnostics and image inspection.
// E.g.: start image interactively: docker run -it <IMAGE_NAME>.
public static final boolean RETAIN_IMAGE_AFTER_TEST =
Boolean.getBoolean("jdk.test.docker.retain.image");
// Path to a JDK under test.
// This may be useful when developing tests on non-Linux platforms.
public static final String JDK_UNDER_TEST =
System.getProperty("jdk.test.docker.jdk", Utils.TEST_JDK);
/**
* Optimized check of whether the docker engine is available in a given
* environment. Checks only once, then remembers the result in a singleton.
*
* @return true if docker engine is available
* @throws Exception
*/
public static boolean isDockerEngineAvailable() throws Exception {
if (wasDockerEngineChecked)
return isDockerEngineAvailable;
isDockerEngineAvailable = isDockerEngineAvailableCheck();
wasDockerEngineChecked = true;
return isDockerEngineAvailable;
}
/**
* Convenience method, will check if docker engine is available and usable;
* will print the appropriate message when not available.
*
* @return true if docker engine is available
* @throws Exception
*/
public static boolean canTestDocker() throws Exception {
if (isDockerEngineAvailable()) {
return true;
} else {
throw new SkippedException("Docker engine is not available on this system");
}
}
/**
* Simple check - is docker engine available, accessible and usable.
* Run basic docker command: 'docker ps' - list docker instances.
* If docker engine is available and accesible then true is returned
* and we can proceed with testing docker.
*
* @return true if docker engine is available and usable
* @throws Exception
*/
private static boolean isDockerEngineAvailableCheck() throws Exception {
try {
execute(Container.ENGINE_COMMAND, "ps")
.shouldHaveExitValue(0)
.shouldContain("CONTAINER")
.shouldContain("IMAGE");
} catch (Exception e) {
return false;
}
return true;
}
/**
* Build a container image that contains JDK under test.
* The jdk will be placed under the "/jdk/" folder inside the image/container file system.
*
* @param imageName name of the image to be created, including version tag
* @throws Exception
*/
public static void buildJdkContainerImage(String imageName) throws Exception {
buildJdkContainerImage(imageName, null);
}
/**
* Build a container image that contains JDK under test.
* The jdk will be placed under the "/jdk/" folder inside the image/container file system.
*
* @param imageName name of the image to be created, including version tag
* @param dockerfileContent content of the Dockerfile; use null to generate default content
* @throws Exception
*/
public static void buildJdkContainerImage(String imageName, String dockerfileContent) throws Exception {
// image name may contain tag, hence replace ':'
String imageDirName = imageName.replace(":", "-");
// Create an image build/staging directory
Path buildDir = Paths.get(imageDirName);
if (Files.exists(buildDir)) {
throw new RuntimeException("The docker build directory already exists: " + buildDir);
}
Files.createDirectories(buildDir);
// Generate Dockerfile
if (dockerfileContent != null) {
Files.writeString(buildDir.resolve("Dockerfile"), dockerfileContent);
} else {
generateDockerFile(buildDir.resolve("Dockerfile"),
DockerfileConfig.getBaseImageName(),
DockerfileConfig.getBaseImageVersion());
}
// Copy JDK-under-test tree to the docker build directory.
// This step is required for building a docker image.
Path jdkSrcDir = Paths.get(JDK_UNDER_TEST);
Path jdkDstDir = buildDir.resolve("jdk");
Files.createDirectories(jdkDstDir);
Files.walkFileTree(jdkSrcDir, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, new CopyFileVisitor(jdkSrcDir, jdkDstDir));
buildImage(imageName, buildDir);
}
/**
* Build a container image based on image build directory.
*
* @param imageName name of the image to be created, including version tag
* @param buildDir build directory; it should already contain all the content
* needed to build the image.
* @throws Exception
*/
private static void buildImage(String imageName, Path buildDir) throws Exception {
try {
execute(Container.ENGINE_COMMAND, "build", "--no-cache", "--tag", imageName, buildDir.toString())
.shouldHaveExitValue(0);
} catch (Exception e) {
// If docker image building fails there is a good chance it happens due to environment and/or
// configuration other than product failure. Throw jtreg skipped exception in such case
// instead of failing the test.
throw new SkippedException("Building docker image failed. Details: \n" + e.getMessage());
}
}
/**
* Build the docker command to run java inside a container
*
* @param DockerRunOptions options for running docker
*
* @return command
* @throws Exception
*/
public static List<String> buildJavaCommand(DockerRunOptions opts) throws Exception {
List<String> cmd = new ArrayList<>();
cmd.add(Container.ENGINE_COMMAND);
cmd.add("run");
if (opts.tty)
cmd.add("--tty=true");
if (opts.removeContainerAfterUse)
cmd.add("--rm");
cmd.addAll(opts.dockerOpts);
cmd.add(opts.imageNameAndTag);
cmd.add(opts.command);
cmd.addAll(opts.javaOpts);
if (opts.appendTestJavaOptions) {
Collections.addAll(cmd, Utils.getTestJavaOpts());
}
cmd.addAll(opts.javaOptsAppended);
cmd.add(opts.classToRun);
cmd.addAll(opts.classParams);
return cmd;
}
/**
* Run Java inside the docker image with specified parameters and options.
*
* @param DockerRunOptions options for running docker
*
* @return output of the run command
* @throws Exception
*/
public static OutputAnalyzer dockerRunJava(DockerRunOptions opts) throws Exception {
return execute(buildJavaCommand(opts));
}
/**
* Remove docker image
*
* @param DockerRunOptions options for running docker
* @throws Exception
*/
public static void removeDockerImage(String imageNameAndTag) throws Exception {
execute(Container.ENGINE_COMMAND, "rmi", "--force", imageNameAndTag);
}
/**
* Convenience method - express command as sequence of strings
*
* @param command to execute
* @return The output from the process
* @throws Exception
*/
public static OutputAnalyzer execute(List<String> command) throws Exception {
return execute(command.toArray(new String[command.size()]));
}
/**
* Execute a specified command in a process, report diagnostic info.
*
* @param command to be executed
* @return The output from the process
* @throws Exception
*/
public static OutputAnalyzer execute(String... command) throws Exception {
ProcessBuilder pb = new ProcessBuilder(command);
System.out.println("[COMMAND]\n" + Utils.getCommandLine(pb));
long started = System.currentTimeMillis();
Process p = pb.start();
long pid = p.pid();
OutputAnalyzer output = new OutputAnalyzer(p);
int max = MAX_LINES_TO_COPY_FOR_CHILD_STDOUT;
String stdout = output.getStdout();
String stdoutLimited = limitLines(stdout, max);
System.out.println("[ELAPSED: " + (System.currentTimeMillis() - started) + " ms]");
System.out.println("[STDERR]\n" + output.getStderr());
System.out.println("[STDOUT]\n" + stdoutLimited);
if (stdout != stdoutLimited) {
System.out.printf("Child process STDOUT is limited to %d lines\n",
max);
}
String stdoutLogFile = String.format("docker-stdout-%d.log", pid);
writeOutputToFile(stdout, stdoutLogFile);
System.out.println("Full child process STDOUT was saved to " + stdoutLogFile);
return output;
}
private static void writeOutputToFile(String output, String fileName) throws Exception {
try (FileWriter fw = new FileWriter(fileName)) {
fw.write(output, 0, output.length());
}
}
private static String limitLines(String buffer, int nrOfLines) {
List<String> l = Arrays.asList(buffer.split("\\R"));
if (l.size() < nrOfLines) {
return buffer;
}
return String.join("\n", l.subList(0, nrOfLines));
}
private static void generateDockerFile(Path dockerfile, String baseImage,
String baseImageVersion) throws Exception {
String template =
"FROM %s:%s\n" +
"COPY /jdk /jdk\n" +
"ENV JAVA_HOME=/jdk\n" +
"CMD [\"/bin/bash\"]\n";
String dockerFileStr = String.format(template, baseImage, baseImageVersion);
Files.writeString(dockerfile, dockerFileStr);
}
private static class CopyFileVisitor extends SimpleFileVisitor<Path> {
private final Path src;
private final Path dst;
public CopyFileVisitor(Path src, Path dst) {
this.src = src;
this.dst = dst;
}
@Override
public FileVisitResult preVisitDirectory(Path file,
BasicFileAttributes attrs) throws IOException {
Path dstDir = dst.resolve(src.relativize(file));
if (!dstDir.toFile().exists()) {
Files.createDirectories(dstDir);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file,
BasicFileAttributes attrs) throws IOException {
if (!file.toFile().isFile()) {
return FileVisitResult.CONTINUE;
}
Path dstFile = dst.resolve(src.relativize(file));
Files.copy(file, dstFile, StandardCopyOption.COPY_ATTRIBUTES);
return FileVisitResult.CONTINUE;
}
}
}