Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-75102] Fix Windows Docker running Windows container with spaces in workspace path #326

Merged
merged 3 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,11 @@
if (hasWorkdir) {
prefix.add("--workdir");
masksPrefixList.add(false);
prefix.add(path);
if (super.isUnix()) {

Check warning on line 280 in src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 280 is only partially covered, one branch is missing
prefix.add(path);
} else {
prefix.add(WindowsUtil.quoteArgument(path));

Check warning on line 283 in src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 283 is not covered by tests
}
masksPrefixList.add(false);
} else {
String safePath = path.replace("'", "'\"'\"'");
Expand Down Expand Up @@ -333,8 +337,20 @@
originalMasks = new boolean[starter.cmds().size()];
}

// Adapted from decorateByPrefix:
starter.cmds().addAll(0, prefix);
List<String> cmds = new ArrayList<>();
cmds.addAll(prefix);

if (!super.isUnix() && starter.cmds().size() >= 3 && "cmd".equals(starter.cmds().get(0)) && "/c".equalsIgnoreCase(starter.cmds().get(1))) {

Check warning on line 343 in src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 343 is only partially covered, 7 branches are missing
// JENKINS-75102 Docker exec on Windows processes character escaping differently.
// Modify launch to work with special characters in a way that docker exec can handle.
cmds.addAll(starter.cmds().subList(0, 2));
cmds.add("call");
cmds.addAll(starter.cmds().subList(2, starter.cmds().size()).stream()
.map(cmd -> cmd.replaceAll("\"\"(.*)\"\"", "\"$1\"")).collect(Collectors.toList()));

Check warning on line 349 in src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 346-349 are not covered by tests
} else {
cmds.addAll(starter.cmds());
}
starter.cmds(cmds);

boolean[] masks = new boolean[originalMasks.length + prefix.size()];
boolean[] masksPrefix = new boolean[masksPrefixList.size()];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,16 @@
import hudson.util.VersionNumber;
import org.junit.Assume;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.hamcrest.Matchers;
import org.jenkinsci.plugins.docker.commons.tools.DockerTool;

/**
Expand All @@ -41,6 +48,17 @@
public class DockerTestUtil {
public static String DEFAULT_MINIMUM_VERSION = "1.3";

// Major Windows kernel versions. See https://hub.docker.com/r/microsoft/windows-nanoserver
private static List<String> MAJOR_WINDOWS_KERNEL_VERSIONS = Arrays.asList(
"10.0.17763.6659", // 1809
"10.0.18363.1556", // 1909
"10.0.19041.1415", // 2004
"10.0.19042.1889", // 20H2
"10.0.20348.2966", // 2022
"10.0.26100.2605" // 2025
);


public static void assumeDocker() throws Exception {
assumeDocker(new VersionNumber(DEFAULT_MINIMUM_VERSION));
}
Expand All @@ -61,10 +79,78 @@ public static void assumeDocker(VersionNumber minimumVersion) throws Exception {
Assume.assumeFalse("Docker version not < " + minimumVersion.toString(), dockerClient.version().isOlderThan(minimumVersion));
}

/**
* Used to assume docker Windows is running in a particular os mode
* @param os The os [windows, linux]
* @throws Exception
*/
public static void assumeDockerServerOSMode(String os) throws Exception {
Launcher.LocalLauncher localLauncher = new Launcher.LocalLauncher(StreamTaskListener.NULL);
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
int status = localLauncher
.launch()
.cmds(DockerTool.getExecutable(null, null, null, null), "version", "-f", "{{.Server.Os}}")
.stdout(out)
.start()
.joinWithTimeout(DockerClient.CLIENT_TIMEOUT, TimeUnit.SECONDS, localLauncher.getListener());
Assume.assumeTrue("Docker working", status == 0);
Assume.assumeThat("Docker running in " + os + " mode", out.toString().trim(), Matchers.equalToIgnoringCase(os));
} catch (IOException x) {
Assume.assumeNoException("Docker retrieve OS", x);
}
}

public static void assumeWindows() throws Exception {
Assume.assumeTrue(System.getProperty("os.name").toLowerCase().contains("windows"));
}

public static void assumeNotWindows() throws Exception {
Assume.assumeFalse(System.getProperty("os.name").toLowerCase().contains("windows"));
}

public static String getWindowsKernelVersion() throws Exception {
Launcher.LocalLauncher localLauncher = new Launcher.LocalLauncher(StreamTaskListener.NULL);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayOutputStream err = new ByteArrayOutputStream();

int status = localLauncher
.launch()
.cmds("cmd", "/c", "ver")
.stdout(out)
.stderr(err)
.start()
.joinWithTimeout(DockerClient.CLIENT_TIMEOUT, TimeUnit.SECONDS, localLauncher.getListener());

if (status != 0) {
throw new RuntimeException(String.format("Failed to obtain Windows kernel version with exit code: %d stdout: %s stderr: %s", status, out, err));
}

Matcher matcher = Pattern.compile("Microsoft Windows \\[Version ([^\\]]+)\\]").matcher(out.toString().trim());

if (matcher.matches()) {
return matcher.group(1);
} else {
throw new RuntimeException("Unable to obtain Windows kernel version from output: " + out);
}
}

/**
* @return The image tag of an image with a kernel version corresponding to the closest compatible Windows release
* @throws Exception
*/
public static String getWindowsImageTag() throws Exception {
// Kernel must match when running Windows containers on docker on Windows if < Windows 11 with Server 2022
String kernelVersion = DockerTestUtil.getWindowsKernelVersion();

// Select the highest well known kernel version <= ours since sometimes an image may not exist for our version
Optional<String> wellKnownKernelVersion = MAJOR_WINDOWS_KERNEL_VERSIONS.stream()
.filter(k -> k.compareTo(kernelVersion) <= 0).max(java.util.Comparator.naturalOrder());

// Fall back to trying our kernel version
return wellKnownKernelVersion.orElse(kernelVersion);
}

public static EnvVars newDockerLaunchEnv() {
// Create the KeyMaterial for connecting to the docker host/server.
// E.g. currently need to add something like the following to your env
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -491,4 +491,29 @@ private static final class Execution extends SynchronousNonBlockingStepExecution
});
}

@Issue("JENKINS-75102")
@Test public void windowsRunningWindowsContainerSpaceInPath() {
// Launching batch scripts through cmd /c in docker exec gets tricky with special characters
// By default, the path of the temporary Jenkins install and workspace have a space in a folder name and a prj@tmp folder
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
DockerTestUtil.assumeWindows();
DockerTestUtil.assumeDocker();
DockerTestUtil.assumeDockerServerOSMode("windows");

// Kernel must match when running Windows containers on docker on Windows
String releaseTag = DockerTestUtil.getWindowsImageTag();

WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "prj");
p.setDefinition(new CpsFlowDefinition(
"node {\n" +
" withDockerContainer('mcr.microsoft.com/windows/nanoserver:" + releaseTag + "') { \n" +
" bat 'echo ran OK' \n" +
" }\n" +
"}", true));
WorkflowRun b = story.j.assertBuildStatusSuccess(p.scheduleBuild2(0));
story.j.assertLogContains("ran OK", b);
}
});
}
}
Loading