Skip to content

Commit 1001d94

Browse files
authored
[JENKINS-75102] Fix Windows Docker running Windows container with spaces in workspace path (#326)
1 parent 261627b commit 1001d94

File tree

3 files changed

+130
-3
lines changed

3 files changed

+130
-3
lines changed

src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java

+19-3
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,11 @@ private static class Decorator extends LauncherDecorator implements Serializable
277277
if (hasWorkdir) {
278278
prefix.add("--workdir");
279279
masksPrefixList.add(false);
280-
prefix.add(path);
280+
if (super.isUnix()) {
281+
prefix.add(path);
282+
} else {
283+
prefix.add(WindowsUtil.quoteArgument(path));
284+
}
281285
masksPrefixList.add(false);
282286
} else {
283287
String safePath = path.replace("'", "'\"'\"'");
@@ -333,8 +337,20 @@ private static class Decorator extends LauncherDecorator implements Serializable
333337
originalMasks = new boolean[starter.cmds().size()];
334338
}
335339

336-
// Adapted from decorateByPrefix:
337-
starter.cmds().addAll(0, prefix);
340+
List<String> cmds = new ArrayList<>();
341+
cmds.addAll(prefix);
342+
343+
if (!super.isUnix() && starter.cmds().size() >= 3 && "cmd".equals(starter.cmds().get(0)) && "/c".equalsIgnoreCase(starter.cmds().get(1))) {
344+
// JENKINS-75102 Docker exec on Windows processes character escaping differently.
345+
// Modify launch to work with special characters in a way that docker exec can handle.
346+
cmds.addAll(starter.cmds().subList(0, 2));
347+
cmds.add("call");
348+
cmds.addAll(starter.cmds().subList(2, starter.cmds().size()).stream()
349+
.map(cmd -> cmd.replaceAll("\"\"(.*)\"\"", "\"$1\"")).collect(Collectors.toList()));
350+
} else {
351+
cmds.addAll(starter.cmds());
352+
}
353+
starter.cmds(cmds);
338354

339355
boolean[] masks = new boolean[originalMasks.length + prefix.size()];
340356
boolean[] masksPrefix = new boolean[masksPrefixList.size()];

src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java

+86
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,16 @@
3030
import hudson.util.VersionNumber;
3131
import org.junit.Assume;
3232

33+
import java.io.ByteArrayOutputStream;
3334
import java.io.IOException;
35+
import java.util.Arrays;
36+
import java.util.List;
37+
import java.util.Optional;
3438
import java.util.concurrent.TimeUnit;
39+
import java.util.regex.Matcher;
40+
import java.util.regex.Pattern;
3541

42+
import org.hamcrest.Matchers;
3643
import org.jenkinsci.plugins.docker.commons.tools.DockerTool;
3744

3845
/**
@@ -41,6 +48,17 @@
4148
public class DockerTestUtil {
4249
public static String DEFAULT_MINIMUM_VERSION = "1.3";
4350

51+
// Major Windows kernel versions. See https://hub.docker.com/r/microsoft/windows-nanoserver
52+
private static List<String> MAJOR_WINDOWS_KERNEL_VERSIONS = Arrays.asList(
53+
"10.0.17763.6659", // 1809
54+
"10.0.18363.1556", // 1909
55+
"10.0.19041.1415", // 2004
56+
"10.0.19042.1889", // 20H2
57+
"10.0.20348.2966", // 2022
58+
"10.0.26100.2605" // 2025
59+
);
60+
61+
4462
public static void assumeDocker() throws Exception {
4563
assumeDocker(new VersionNumber(DEFAULT_MINIMUM_VERSION));
4664
}
@@ -61,10 +79,78 @@ public static void assumeDocker(VersionNumber minimumVersion) throws Exception {
6179
Assume.assumeFalse("Docker version not < " + minimumVersion.toString(), dockerClient.version().isOlderThan(minimumVersion));
6280
}
6381

82+
/**
83+
* Used to assume docker Windows is running in a particular os mode
84+
* @param os The os [windows, linux]
85+
* @throws Exception
86+
*/
87+
public static void assumeDockerServerOSMode(String os) throws Exception {
88+
Launcher.LocalLauncher localLauncher = new Launcher.LocalLauncher(StreamTaskListener.NULL);
89+
try {
90+
ByteArrayOutputStream out = new ByteArrayOutputStream();
91+
int status = localLauncher
92+
.launch()
93+
.cmds(DockerTool.getExecutable(null, null, null, null), "version", "-f", "{{.Server.Os}}")
94+
.stdout(out)
95+
.start()
96+
.joinWithTimeout(DockerClient.CLIENT_TIMEOUT, TimeUnit.SECONDS, localLauncher.getListener());
97+
Assume.assumeTrue("Docker working", status == 0);
98+
Assume.assumeThat("Docker running in " + os + " mode", out.toString().trim(), Matchers.equalToIgnoringCase(os));
99+
} catch (IOException x) {
100+
Assume.assumeNoException("Docker retrieve OS", x);
101+
}
102+
}
103+
104+
public static void assumeWindows() throws Exception {
105+
Assume.assumeTrue(System.getProperty("os.name").toLowerCase().contains("windows"));
106+
}
107+
64108
public static void assumeNotWindows() throws Exception {
65109
Assume.assumeFalse(System.getProperty("os.name").toLowerCase().contains("windows"));
66110
}
67111

112+
public static String getWindowsKernelVersion() throws Exception {
113+
Launcher.LocalLauncher localLauncher = new Launcher.LocalLauncher(StreamTaskListener.NULL);
114+
ByteArrayOutputStream out = new ByteArrayOutputStream();
115+
ByteArrayOutputStream err = new ByteArrayOutputStream();
116+
117+
int status = localLauncher
118+
.launch()
119+
.cmds("cmd", "/c", "ver")
120+
.stdout(out)
121+
.stderr(err)
122+
.start()
123+
.joinWithTimeout(DockerClient.CLIENT_TIMEOUT, TimeUnit.SECONDS, localLauncher.getListener());
124+
125+
if (status != 0) {
126+
throw new RuntimeException(String.format("Failed to obtain Windows kernel version with exit code: %d stdout: %s stderr: %s", status, out, err));
127+
}
128+
129+
Matcher matcher = Pattern.compile("Microsoft Windows \\[Version ([^\\]]+)\\]").matcher(out.toString().trim());
130+
131+
if (matcher.matches()) {
132+
return matcher.group(1);
133+
} else {
134+
throw new RuntimeException("Unable to obtain Windows kernel version from output: " + out);
135+
}
136+
}
137+
138+
/**
139+
* @return The image tag of an image with a kernel version corresponding to the closest compatible Windows release
140+
* @throws Exception
141+
*/
142+
public static String getWindowsImageTag() throws Exception {
143+
// Kernel must match when running Windows containers on docker on Windows if < Windows 11 with Server 2022
144+
String kernelVersion = DockerTestUtil.getWindowsKernelVersion();
145+
146+
// Select the highest well known kernel version <= ours since sometimes an image may not exist for our version
147+
Optional<String> wellKnownKernelVersion = MAJOR_WINDOWS_KERNEL_VERSIONS.stream()
148+
.filter(k -> k.compareTo(kernelVersion) <= 0).max(java.util.Comparator.naturalOrder());
149+
150+
// Fall back to trying our kernel version
151+
return wellKnownKernelVersion.orElse(kernelVersion);
152+
}
153+
68154
public static EnvVars newDockerLaunchEnv() {
69155
// Create the KeyMaterial for connecting to the docker host/server.
70156
// E.g. currently need to add something like the following to your env

src/test/java/org/jenkinsci/plugins/docker/workflow/WithContainerStepTest.java

+25
Original file line numberDiff line numberDiff line change
@@ -491,4 +491,29 @@ private static final class Execution extends SynchronousNonBlockingStepExecution
491491
});
492492
}
493493

494+
@Issue("JENKINS-75102")
495+
@Test public void windowsRunningWindowsContainerSpaceInPath() {
496+
// Launching batch scripts through cmd /c in docker exec gets tricky with special characters
497+
// By default, the path of the temporary Jenkins install and workspace have a space in a folder name and a prj@tmp folder
498+
story.addStep(new Statement() {
499+
@Override public void evaluate() throws Throwable {
500+
DockerTestUtil.assumeWindows();
501+
DockerTestUtil.assumeDocker();
502+
DockerTestUtil.assumeDockerServerOSMode("windows");
503+
504+
// Kernel must match when running Windows containers on docker on Windows
505+
String releaseTag = DockerTestUtil.getWindowsImageTag();
506+
507+
WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "prj");
508+
p.setDefinition(new CpsFlowDefinition(
509+
"node {\n" +
510+
" withDockerContainer('mcr.microsoft.com/windows/nanoserver:" + releaseTag + "') { \n" +
511+
" bat 'echo ran OK' \n" +
512+
" }\n" +
513+
"}", true));
514+
WorkflowRun b = story.j.assertBuildStatusSuccess(p.scheduleBuild2(0));
515+
story.j.assertLogContains("ran OK", b);
516+
}
517+
});
518+
}
494519
}

0 commit comments

Comments
 (0)