Skip to content

JENKINS-60473 Containerize workspace paths when running Linux container on Windows host #197

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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 @@ -53,6 +53,7 @@
import org.jenkinsci.plugins.docker.commons.tools.DockerTool;
import org.jenkinsci.plugins.docker.workflow.client.DockerClient;
import org.jenkinsci.plugins.docker.workflow.client.WindowsDockerClient;
import org.jenkinsci.plugins.docker.workflow.client.WindowsLinuxDockerClient;
import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl;
import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl;
import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl;
Expand Down Expand Up @@ -141,9 +142,18 @@ public Execution() {
workspace.mkdirs(); // otherwise it may be owned by root when created for -v
String ws = getPath(workspace);
toolName = step.toolName;
DockerClient dockerClient = launcher.isUnix()
? new DockerClient(launcher, node, toolName)
: new WindowsDockerClient(launcher, node, toolName);
DockerClient dockerClient;

if (launcher.isUnix()) {
dockerClient = new DockerClient(launcher, node, toolName);
}
else {
dockerClient = new WindowsDockerClient(launcher, node, toolName);
String os = dockerClient.inspect(new EnvVars(), step.image, ".Os");
if (os != null && os.equals("linux")) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be actually inverted? There is a lot of Unix operating systems being containerized, but Windows Client is for Windows only?

dockerClient = new WindowsLinuxDockerClient(launcher, node, toolName);
}
}

VersionNumber dockerVersion = dockerClient.version();
if (dockerVersion != null) {
Expand Down Expand Up @@ -195,7 +205,7 @@ public Execution() {
volumes.put(tmp, tmp);
}

String command = launcher.isUnix() ? "cat" : "cmd.exe";
String command = dockerClient.runCommand();
container = dockerClient.run(env, step.image, step.args, ws, volumes, volumesFromContainers, envReduced, dockerClient.whoAmI(), /* expected to hang until killed */ command);
final List<String> ps = dockerClient.listProcess(env, container);
if (!ps.contains(command)) {
Expand All @@ -209,7 +219,7 @@ public Execution() {
DockerFingerprints.addRunFacet(dockerClient.getContainerRecord(env, container), run);
ImageAction.add(step.image, run);
getContext().newBodyInvoker().
withContext(BodyInvoker.mergeLauncherDecorators(getContext().get(LauncherDecorator.class), new Decorator(container, envHost, ws, toolName, dockerVersion))).
withContext(BodyInvoker.mergeLauncherDecorators(getContext().get(LauncherDecorator.class), new Decorator(container, envHost, ws, toolName, dockerVersion, dockerClient.needToContainerizePath()))).
withCallback(new Callback(container, toolName)).
start();
return false;
Expand Down Expand Up @@ -247,14 +257,16 @@ private static class Decorator extends LauncherDecorator implements Serializable
private final @CheckForNull String toolName;
private final boolean hasEnv;
private final boolean hasWorkdir;
private final boolean needToContainerizePath;

Decorator(String container, EnvVars envHost, String ws, String toolName, VersionNumber dockerVersion) {
Decorator(String container, EnvVars envHost, String ws, String toolName, VersionNumber dockerVersion, boolean needToContainerizePath) {
this.container = container;
this.envHost = Util.mapToEnv(envHost);
this.ws = ws;
this.toolName = toolName;
this.hasEnv = dockerVersion != null && dockerVersion.compareTo(new VersionNumber("1.13.0")) >= 0;
this.hasWorkdir = dockerVersion != null && dockerVersion.compareTo(new VersionNumber("17.12")) >= 0;
this.needToContainerizePath = needToContainerizePath;
}

@Override public Launcher decorate(final Launcher launcher, final Node node) {
Expand Down Expand Up @@ -333,6 +345,14 @@ private static class Decorator extends LauncherDecorator implements Serializable
System.arraycopy(originalMasks, 0, masks, prefix.size(), originalMasks.length);
starter.masks(masks);

if (needToContainerizePath && ws != null) {
String wsTrimmed = ws.replaceAll("[/\\\\]+$", "");
List<String> cmds = starter.cmds();
for (int i=0; i<cmds.size(); i++) {
cmds.set(i, DockerClient.containerizePath(cmds.get(i), wsTrimmed));
}
}

return super.launch(starter);
}
@Override public void kill(Map<String,String> modelEnvVars) throws IOException, InterruptedException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,4 +378,113 @@ public List<String> getVolumes(@Nonnull EnvVars launchEnv, String containerID) t
}
return Arrays.asList(volumes.replace("\\", "/").split("\\n"));
}

public String runCommand() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add Javadoc for new API below?

return "cat";
}

public boolean needToContainerizePath() {
return false;
}

public String containerizePathIfNeeded(String path, String prefix) {
if (needToContainerizePath())
return DockerClient.containerizePath(path, prefix);

return path;
}

public static String containerizePath(String path, String prefix)
{
StringBuffer result = new StringBuffer();
char[] pathChars = path.toCharArray();
char[] prefixChars = (prefix == null) ? null : prefix.toCharArray();

for (int i = 0; i < pathChars.length; i++)
{
char currentChar = pathChars[i];
if (currentChar == ':' && i > 0 && i < pathChars.length - 1)
{
char previousChar = pathChars[i - 1];
if ((previousChar >= 'a' && previousChar <= 'z') || (previousChar >= 'A' && previousChar <= 'Z'))
{
char nextChar = pathChars[i + 1];
if (nextChar == '/' || nextChar == '\\')
{
char nextNextChar = (i < pathChars.length - 2) ? pathChars[i + 2] : ' ';
if (nextNextChar != '/')
{
if (prefix == null || checkPrefix(pathChars, i - 1, prefixChars))
{
result.setCharAt(i - 1, '/');
result.append(Character.toLowerCase(previousChar));
result.append('/');
i++;
i++;

boolean done = false;
for ( ; i < pathChars.length; i++)
{
currentChar = pathChars[i];
switch (currentChar)
{
case '\\':
result.append('/');
break;

case '?':
case '<':
case '>':
case ':':
case '*':
case '|':
case '"':
case '\'':
result.append(currentChar);
done = true;
break;

default:
result.append(currentChar);
break;
}

if (done)
break;
}

continue;
}
}
}
}
}

result.append(currentChar);
}

return result.toString();
}

private static boolean checkPrefix(char[] pathChars, int index, char[] prefixChars)
{
if (index + prefixChars.length > pathChars.length)
return false;

for (int i = 0; i < prefixChars.length; i++)
{
char pathChar = pathChars[index + i];
if (pathChar == '\\')
pathChar = '/';

char prefixChar = prefixChars[i];
if (prefixChar == '\\')
prefixChar = '/';

if (pathChar != prefixChar)
return false;
}

return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ public String run(@Nonnull EnvVars launchEnv, @Nonnull String image, @CheckForNu
}

if (workdir != null) {
argb.add("-w", workdir);
argb.add("-w", containerizePathIfNeeded(workdir, null));
}
for (Map.Entry<String, String> volume : volumes.entrySet()) {
argb.add("-v", volume.getKey() + ":" + volume.getValue());
argb.add("-v", volume.getKey() + ":" + containerizePathIfNeeded(volume.getValue(), null));
}
for (String containerId : volumesFromContainers) {
argb.add("--volumes-from", containerId);
Expand Down Expand Up @@ -80,7 +80,7 @@ public List<String> listProcess(@Nonnull EnvVars launchEnv, @Nonnull String cont
}
return processes;
}

@Override
public Optional<String> getContainerIdIfContainerized() throws IOException, InterruptedException {
if (node == null ||
Expand Down Expand Up @@ -111,10 +111,10 @@ public String whoAmI() throws IOException, InterruptedException {
}
}

private LaunchResult launch(EnvVars env, boolean quiet, FilePath workDir, String... args) throws IOException, InterruptedException {
protected LaunchResult launch(EnvVars env, boolean quiet, FilePath workDir, String... args) throws IOException, InterruptedException {
return launch(env, quiet, workDir, new ArgumentListBuilder(args));
}
private LaunchResult launch(EnvVars env, boolean quiet, FilePath workDir, ArgumentListBuilder argb) throws IOException, InterruptedException {
protected LaunchResult launch(EnvVars env, boolean quiet, FilePath workDir, ArgumentListBuilder argb) throws IOException, InterruptedException {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Executing command \"{0}\"", argb);
}
Expand All @@ -134,4 +134,10 @@ private LaunchResult launch(EnvVars env, boolean quiet, FilePath workDir, Argume

return result;
}

@Override
public String runCommand()
{
return "cmd.exe";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.jenkinsci.plugins.docker.workflow.client;

import com.google.common.base.Optional;
import hudson.EnvVars;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.Node;
import hudson.util.ArgumentListBuilder;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import java.io.*;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

public class WindowsLinuxDockerClient extends WindowsDockerClient {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking the code, it would be rather a Unix/POSIX container launcher. Not that important tho

private static final Logger LOGGER = Logger.getLogger(WindowsLinuxDockerClient.class.getName());

private final Launcher launcher;
private final Node node;

public WindowsLinuxDockerClient(@Nonnull Launcher launcher, @CheckForNull Node node, @CheckForNull String toolName) {
super(launcher, node, toolName);
this.launcher = launcher;
this.node = node;
}

@Override
public List<String> listProcess(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws IOException, InterruptedException {
LaunchResult result = launch(launchEnv, false, null, "docker", "top", containerId);
if (result.getStatus() != 0) {
throw new IOException(String.format("Failed to run top '%s'. Error: %s", containerId, result.getErr()));
}
List<String> processes = new ArrayList<>();
try (Reader r = new StringReader(result.getOut());
BufferedReader in = new BufferedReader(r)) {
String line;
in.readLine(); // ps header
while ((line = in.readLine()) != null) {
final StringTokenizer stringTokenizer = new StringTokenizer(line, " ");
if (stringTokenizer.countTokens() < 4) {
throw new IOException("Unexpected `docker top` output : "+line);
}

stringTokenizer.nextToken(); // PID
stringTokenizer.nextToken(); // USER
stringTokenizer.nextToken(); // TIME
processes.add(stringTokenizer.nextToken()); // COMMAND
}
}
return processes;
}

@Override
public String runCommand()
{
return "cat";
}

@Override
public boolean needToContainerizePath() {
return true;
}
}