diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java index 8c6ccfb1a..aaaeb65ff 100644 --- a/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java @@ -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; @@ -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")) { + dockerClient = new WindowsLinuxDockerClient(launcher, node, toolName); + } + } VersionNumber dockerVersion = dockerClient.version(); if (dockerVersion != null) { @@ -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 ps = dockerClient.listProcess(env, container); if (!ps.contains(command)) { @@ -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; @@ -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) { @@ -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 cmds = starter.cmds(); + for (int i=0; i modelEnvVars) throws IOException, InterruptedException { diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java index 6562606fb..16e7c9be6 100644 --- a/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java @@ -378,4 +378,113 @@ public List getVolumes(@Nonnull EnvVars launchEnv, String containerID) t } return Arrays.asList(volumes.replace("\\", "/").split("\\n")); } + + public String runCommand() { + 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; + } } diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/client/WindowsDockerClient.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/WindowsDockerClient.java index 77b117e9d..32153fe08 100644 --- a/src/main/java/org/jenkinsci/plugins/docker/workflow/client/WindowsDockerClient.java +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/WindowsDockerClient.java @@ -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 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); @@ -80,7 +80,7 @@ public List listProcess(@Nonnull EnvVars launchEnv, @Nonnull String cont } return processes; } - + @Override public Optional getContainerIdIfContainerized() throws IOException, InterruptedException { if (node == null || @@ -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); } @@ -134,4 +134,10 @@ private LaunchResult launch(EnvVars env, boolean quiet, FilePath workDir, Argume return result; } + + @Override + public String runCommand() + { + return "cmd.exe"; + } } diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/client/WindowsLinuxDockerClient.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/WindowsLinuxDockerClient.java new file mode 100644 index 000000000..005585c5e --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/WindowsLinuxDockerClient.java @@ -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 { + 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 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 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; + } +}