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 ebbfd68a3..f306b0903 100644 --- a/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java @@ -25,40 +25,17 @@ import com.google.common.base.Optional; import com.google.inject.Inject; -import hudson.AbortException; -import hudson.EnvVars; -import hudson.Extension; -import hudson.FilePath; -import hudson.Launcher; -import hudson.LauncherDecorator; -import hudson.Proc; -import hudson.Util; +import hudson.*; import hudson.model.Computer; import hudson.model.Node; import hudson.model.Run; import hudson.model.TaskListener; import hudson.slaves.WorkspaceList; import hudson.util.VersionNumber; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; 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.workflow.steps.AbstractStepDescriptorImpl; -import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl; -import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; -import org.jenkinsci.plugins.workflow.steps.BodyExecutionCallback; -import org.jenkinsci.plugins.workflow.steps.BodyInvoker; -import org.jenkinsci.plugins.workflow.steps.StepContext; -import org.jenkinsci.plugins.workflow.steps.StepContextParameter; +import org.jenkinsci.plugins.workflow.steps.*; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; @@ -68,22 +45,26 @@ import java.io.IOException; import java.io.Serializable; import java.nio.charset.Charset; +import java.util.*; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; public class WithContainerStep extends AbstractStepImpl { - + private static final Logger LOGGER = Logger.getLogger(WithContainerStep.class.getName()); - private final @Nonnull String image; + private final @Nonnull + String image; private String args; private String toolName; + private static boolean needToContainerizePath = false; - @DataBoundConstructor public WithContainerStep(@Nonnull String image) { + @DataBoundConstructor + public WithContainerStep(@Nonnull String image) { this.image = image; } - + public String getImage() { return image; } @@ -101,7 +82,8 @@ public String getToolName() { return toolName; } - @DataBoundSetter public void setToolName(String toolName) { + @DataBoundSetter + public void setToolName(String toolName) { this.toolName = Util.fixEmpty(toolName); } @@ -112,22 +94,31 @@ private static void destroy(String container, Launcher launcher, Node node, EnvV // TODO switch to GeneralNonBlockingStepExecution public static class Execution extends AbstractStepExecutionImpl { private static final long serialVersionUID = 1; - @Inject(optional=true) private transient WithContainerStep step; - @StepContextParameter private transient Launcher launcher; - @StepContextParameter private transient TaskListener listener; - @StepContextParameter private transient FilePath workspace; - @StepContextParameter private transient EnvVars env; - @StepContextParameter private transient Computer computer; - @StepContextParameter private transient Node node; + @Inject(optional = true) + private transient WithContainerStep step; + @StepContextParameter + private transient Launcher launcher; + @StepContextParameter + private transient TaskListener listener; + @StepContextParameter + private transient FilePath workspace; + @StepContextParameter + private transient EnvVars env; + @StepContextParameter + private transient Computer computer; + @StepContextParameter + private transient Node node; @SuppressWarnings("rawtypes") // TODO not compiling on cloudbees.ci - @StepContextParameter private transient Run run; + @StepContextParameter + private transient Run run; private String container; private String toolName; public Execution() { } - @Override public boolean start() throws Exception { + @Override + public boolean start() throws Exception { EnvVars envReduced = new EnvVars(env); EnvVars envHost = computer.getEnvironment(); envReduced.entrySet().removeAll(envHost.entrySet()); @@ -140,10 +131,19 @@ 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); + String containerOsType = dockerClient.inspect(new EnvVars(), step.image, ".Os"); + + if (!launcher.isUnix() && containerOsType != null && containerOsType.equalsIgnoreCase("linux")) { + needToContainerizePath = true; + dockerClient.setNeedToContainerizePath(true); + dockerClient.setContainerUnix(true); + } + VersionNumber dockerVersion = dockerClient.version(); if (dockerVersion != null) { if (dockerVersion.isOlderThan(new VersionNumber("1.7"))) { @@ -194,7 +194,8 @@ 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)) { @@ -207,9 +208,9 @@ public Execution() { ImageAction.add(step.image, run); getContext().newBodyInvoker(). - withContext(BodyInvoker.mergeLauncherDecorators(getContext().get(LauncherDecorator.class), new Decorator(container, envHost, ws, toolName, dockerVersion))). - withCallback(new Callback(container, toolName)). - start(); + withContext(BodyInvoker.mergeLauncherDecorators(getContext().get(LauncherDecorator.class), new Decorator(container, envHost, ws, toolName, dockerVersion))). + withCallback(new Callback(container, toolName)). + start(); return false; } @@ -227,7 +228,8 @@ private static FilePath tempDir(FilePath ws) { return ws.sibling(ws.getName() + System.getProperty(WorkspaceList.class.getName(), "@") + "tmp"); } - @Override public void stop(@Nonnull Throwable cause) throws Exception { + @Override + public void stop(@Nonnull Throwable cause) throws Exception { if (container != null) { LOGGER.log(Level.FINE, "stopping container " + container, cause); destroy(container, launcher, getContext().get(Node.class), env, toolName); @@ -242,7 +244,8 @@ private static class Decorator extends LauncherDecorator implements Serializable private final String container; private final String[] envHost; private final String ws; - private final @CheckForNull String toolName; + private final @CheckForNull + String toolName; private final boolean hasEnv; private final boolean hasWorkdir; @@ -255,9 +258,11 @@ private static class Decorator extends LauncherDecorator implements Serializable this.hasWorkdir = dockerVersion != null && dockerVersion.compareTo(new VersionNumber("17.12")) >= 0; } - @Override public Launcher decorate(final Launcher launcher, final Node node) { + @Override + public Launcher decorate(final Launcher launcher, final Node node) { return new Launcher.DecoratedLauncher(launcher) { - @Override public Proc launch(Launcher.ProcStarter starter) throws IOException { + @Override + public Proc launch(Launcher.ProcStarter starter) throws IOException { String executable; try { executable = getExecutable(); @@ -311,8 +316,8 @@ private static class Decorator extends LauncherDecorator implements Serializable masksPrefixList.add(false); prefix.addAll(envReduced); masksPrefixList.addAll(envReduced.stream() - .map(v -> true) - .collect(Collectors.toList())); + .map(v -> true) + .collect(Collectors.toList())); } boolean[] originalMasks = starter.masks(); @@ -332,17 +337,28 @@ 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 < cmds.size(); i++) { + cmds.set(i, WindowsDockerClient.containerizePath(cmds.get(i), wsTrimmed)); + } + } + return super.launch(starter); } - @Override public void kill(Map modelEnvVars) throws IOException, InterruptedException { + + @Override + public void kill(Map modelEnvVars) throws IOException, InterruptedException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); String executable = getExecutable(); if (getInner().launch().cmds(executable, "exec", container, "ps", "-A", "-o", "pid,command", "e").stdout(baos).quiet(true).start().joinWithTimeout(DockerClient.CLIENT_TIMEOUT, TimeUnit.SECONDS, listener) != 0) { throw new IOException("failed to run ps"); } List pids = new ArrayList(); - LINE: for (String line : baos.toString(Charset.defaultCharset().name()).split("\n")) { - for (Map.Entry entry : modelEnvVars.entrySet()) { + LINE: + for (String line : baos.toString(Charset.defaultCharset().name()).split("\n")) { + for (Map.Entry entry : modelEnvVars.entrySet()) { // TODO this is imprecise: false positive when argv happens to match KEY=value even if environment does not. Cf. trick in BourneShellScript. if (!line.contains(entry.getKey() + "=" + entry.getValue())) { continue LINE; @@ -364,6 +380,7 @@ private static class Decorator extends LauncherDecorator implements Serializable } } } + private String getExecutable() throws IOException, InterruptedException { EnvVars env = new EnvVars(); for (String pair : envHost) { @@ -387,31 +404,37 @@ private static class Callback extends BodyExecutionCallback.TailCall { this.toolName = toolName; } - @Override protected void finished(StepContext context) throws Exception { + @Override + protected void finished(StepContext context) throws Exception { destroy(container, context.get(Launcher.class), context.get(Node.class), context.get(EnvVars.class), toolName); } } - @Extension public static class DescriptorImpl extends AbstractStepDescriptorImpl { + @Extension + public static class DescriptorImpl extends AbstractStepDescriptorImpl { public DescriptorImpl() { super(Execution.class); } - @Override public String getFunctionName() { + @Override + public String getFunctionName() { return "withDockerContainer"; } - @Override public String getDisplayName() { + @Override + public String getDisplayName() { return "Run build steps inside a Docker container"; } - @Override public boolean takesImplicitBlockArgument() { + @Override + public boolean takesImplicitBlockArgument() { return true; } - @Override public boolean isAdvanced() { + @Override + public boolean isAdvanced() { return true; } 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 336efa0bb..8cf4dce04 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 @@ -31,38 +31,28 @@ import hudson.model.Node; import hudson.util.ArgumentListBuilder; import hudson.util.VersionNumber; -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.Reader; -import java.io.StringReader; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.plugins.docker.commons.fingerprint.ContainerRecord; +import org.jenkinsci.plugins.docker.commons.tools.DockerTool; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.io.*; import java.nio.charset.Charset; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.StringTokenizer; +import java.util.*; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; -import org.apache.commons.lang.StringUtils; -import org.jenkinsci.plugins.docker.commons.fingerprint.ContainerRecord; -import org.jenkinsci.plugins.docker.commons.tools.DockerTool; -import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.NoExternalUse; /** * Simple docker client for Pipeline. - * + * * @author tom.fennelly@gmail.com */ public class DockerClient { @@ -72,24 +62,38 @@ public class DockerClient { /** * Maximum amount of time (in seconds) to wait for {@code docker} client operations which are supposed to be more or less instantaneous. */ - @SuppressFBWarnings(value="MS_SHOULD_BE_FINAL", justification="mutable for scripts") + @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "mutable for scripts") @Restricted(NoExternalUse.class) public static int CLIENT_TIMEOUT = Integer.getInteger(DockerClient.class.getName() + ".CLIENT_TIMEOUT", 180); // TODO 2.4+ SystemProperties /** * Skip removal of container after a container has been stopped. */ - @SuppressFBWarnings(value="MS_SHOULD_BE_FINAL", justification="mutable for scripts") + @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "mutable for scripts") @Restricted(NoExternalUse.class) public static boolean SKIP_RM_ON_STOP = Boolean.getBoolean(DockerClient.class.getName() + ".SKIP_RM_ON_STOP"); - // e.g. 2015-04-09T13:40:21.981801679Z + /** + * The constant DOCKER_DATE_TIME_FORMAT. + */ +// e.g. 2015-04-09T13:40:21.981801679Z public static final String DOCKER_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"; - + private final Launcher launcher; - private final @CheckForNull Node node; - private final @CheckForNull String toolName; + private final @CheckForNull + Node node; + private final @CheckForNull + String toolName; + private boolean needToContainerizePath = false; + private boolean isContainerUnix = true; + /** + * Instantiates a new Docker client. + * + * @param launcher the launcher + * @param node the node + * @param toolName the tool name + */ public DockerClient(@Nonnull Launcher launcher, @CheckForNull Node node, @CheckForNull String toolName) { this.launcher = launcher; this.node = node; @@ -99,16 +103,18 @@ public DockerClient(@Nonnull Launcher launcher, @CheckForNull Node node, @CheckF /** * Run a docker image. * - * @param launchEnv Docker client launch environment. - * @param image The image name. - * @param args Any additional arguments for the {@code docker run} command. - * @param workdir The working directory in the container, or {@code null} for default. - * @param volumes Volumes to be bound. Supply an empty list if no volumes are to be bound. + * @param launchEnv Docker client launch environment. + * @param image The image name. + * @param args Any additional arguments for the {@code docker run} command. + * @param workdir The working directory in the container, or {@code null} for default. + * @param volumes Volumes to be bound. Supply an empty list if no volumes are to be bound. * @param volumesFromContainers Mounts all volumes from the given containers. - * @param containerEnv Environment variables to set in container. - * @param user The uid:gid to execute the container command as. Use {@link #whoAmI()}. - * @param command The command to execute in the image container being run. + * @param containerEnv Environment variables to set in container. + * @param user The uid:gid to execute the container command as. Use {@link #whoAmI()}. + * @param command The command to execute in the image container being run. * @return The container ID. + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception */ public String run(@Nonnull EnvVars launchEnv, @Nonnull String image, @CheckForNull String args, @CheckForNull String workdir, @Nonnull Map volumes, @Nonnull Collection volumesFromContainers, @Nonnull EnvVars containerEnv, @Nonnull String user, @Nonnull String... command) throws IOException, InterruptedException { ArgumentListBuilder argb = new ArgumentListBuilder(); @@ -122,7 +128,7 @@ public String run(@Nonnull EnvVars launchEnv, @Nonnull String image, @CheckForNu if (args != null) { argb.addTokenized(args); } - + if (workdir != null) { argb.add("-w", workdir); } @@ -134,7 +140,7 @@ public String run(@Nonnull EnvVars launchEnv, @Nonnull String image, @CheckForNu } for (Map.Entry variable : containerEnv.entrySet()) { argb.add("-e"); - argb.addMasked(variable.getKey()+"="+variable.getValue()); + argb.addMasked(variable.getKey() + "=" + variable.getValue()); } argb.add(image).add(command); @@ -146,6 +152,15 @@ public String run(@Nonnull EnvVars launchEnv, @Nonnull String image, @CheckForNu } } + /** + * List process list. + * + * @param launchEnv the launch env + * @param containerId the container id + * @return list list + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception + */ public List listProcess(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws IOException, InterruptedException { LaunchResult result = launch(launchEnv, false, "top", containerId, "-eo", "pid,comm"); if (result.getStatus() != 0) { @@ -159,7 +174,7 @@ public List listProcess(@Nonnull EnvVars launchEnv, @Nonnull String cont while ((line = in.readLine()) != null) { final StringTokenizer stringTokenizer = new StringTokenizer(line, " "); if (stringTokenizer.countTokens() < 2) { - throw new IOException("Unexpected `docker top` output : "+line); + throw new IOException("Unexpected `docker top` output : " + line); } stringTokenizer.nextToken(); // PID processes.add(stringTokenizer.nextToken()); // COMMAND @@ -170,13 +185,15 @@ public List listProcess(@Nonnull EnvVars launchEnv, @Nonnull String cont /** * Stop a container. - * - *

+ * + *

* Also removes ({@link #rm(EnvVars, String)}) the container if property * SKIP_RM_ON_STOP is unset or equals false. - * - * @param launchEnv Docker client launch environment. + * + * @param launchEnv Docker client launch environment. * @param containerId The container ID. + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception */ public void stop(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws IOException, InterruptedException { LaunchResult result = launch(launchEnv, false, "stop", "--time=1", containerId); @@ -190,9 +207,11 @@ public void stop(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws /** * Remove a container. - * - * @param launchEnv Docker client launch environment. + * + * @param launchEnv Docker client launch environment. * @param containerId The container ID. + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception */ public void rm(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws IOException, InterruptedException { LaunchResult result; @@ -204,12 +223,16 @@ public void rm(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws I /** * Inspect a docker image/container. + * * @param launchEnv Docker client launch environment. - * @param objectId The image/container ID. + * @param objectId The image/container ID. * @param fieldPath The data path of the data required e.g. {@code .NetworkSettings.IPAddress}. * @return The inspected field value. Null if the command failed + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception */ - public @CheckForNull String inspect(@Nonnull EnvVars launchEnv, @Nonnull String objectId, @Nonnull String fieldPath) throws IOException, InterruptedException { + public @CheckForNull + String inspect(@Nonnull EnvVars launchEnv, @Nonnull String objectId, @Nonnull String fieldPath) throws IOException, InterruptedException { LaunchResult result = launch(launchEnv, true, "inspect", "-f", String.format("{{%s}}", fieldPath), objectId); if (result.getStatus() == 0) { return result.getOut(); @@ -217,27 +240,30 @@ public void rm(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws I return null; } } - + /** * Inspect a docker image/container. + * * @param launchEnv Docker client launch environment. - * @param objectId The image/container ID. + * @param objectId The image/container ID. * @param fieldPath The data path of the data required e.g. {@code .NetworkSettings.IPAddress}. * @return The inspected field value. May be an empty string - * @throws IOException Execution error. Also fails if cannot retrieve the requested field from the request + * @throws IOException Execution error. Also fails if cannot retrieve the requested field from the request * @throws InterruptedException Interrupted * @since 1.1 */ - public @Nonnull String inspectRequiredField(@Nonnull EnvVars launchEnv, @Nonnull String objectId, - @Nonnull String fieldPath) throws IOException, InterruptedException { + public @Nonnull + String inspectRequiredField(@Nonnull EnvVars launchEnv, @Nonnull String objectId, + @Nonnull String fieldPath) throws IOException, InterruptedException { final String fieldValue = inspect(launchEnv, objectId, fieldPath); if (fieldValue == null) { throw new IOException("Cannot retrieve " + fieldPath + " from 'docker inspect " + objectId + "'"); } return fieldValue; } - - private @CheckForNull Date getCreatedDate(@Nonnull EnvVars launchEnv, @Nonnull String objectId) throws IOException, InterruptedException { + + private @CheckForNull + Date getCreatedDate(@Nonnull EnvVars launchEnv, @Nonnull String objectId) throws IOException, InterruptedException { String createdString = inspect(launchEnv, objectId, "json .Created"); if (createdString == null) { return null; @@ -254,10 +280,12 @@ public void rm(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws I /** * Get the docker version. * - * @return The {@link VersionNumber} instance if the version string matches the expected format, - * otherwise {@code null}. + * @return The {@link VersionNumber} instance if the version string matches the expected format, otherwise {@code null}. + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception */ - public @CheckForNull VersionNumber version() throws IOException, InterruptedException { + public @CheckForNull + VersionNumber version() throws IOException, InterruptedException { LaunchResult result = launch(new EnvVars(), true, "-v"); if (result.getStatus() == 0) { return parseVersionNumber(result.getOut()); @@ -265,13 +293,14 @@ public void rm(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws I return null; } } - + private static final Pattern pattern = Pattern.compile("^(\\D+)(\\d+)\\.(\\d+)\\.(\\d+)(.*)"); + /** * Parse a Docker version string (e.g. "Docker version 1.5.0, build a8a31ef"). + * * @param versionString The version string to parse. - * @return The {@link VersionNumber} instance if the version string matched the - * expected format, otherwise {@code null}. + * @return The {@link VersionNumber} instance if the version string matched the expected format, otherwise {@code null}. */ protected static VersionNumber parseVersionNumber(@Nonnull String versionString) { Matcher matcher = pattern.matcher(versionString.trim()); @@ -282,15 +311,43 @@ protected static VersionNumber parseVersionNumber(@Nonnull String versionString) return new VersionNumber(String.format("%s.%s.%s", major, minor, maint)); } else { return null; - } + } } + /** + * @param launchEnv + * @param quiet + * @param args + * @return + * @throws IOException + * @throws InterruptedException + */ private LaunchResult launch(@Nonnull EnvVars launchEnv, boolean quiet, @Nonnull String... args) throws IOException, InterruptedException { return launch(launchEnv, quiet, null, args); } + + /** + * @param launchEnv + * @param quiet + * @param pwd + * @param args + * @return + * @throws IOException + * @throws InterruptedException + */ private LaunchResult launch(@Nonnull EnvVars launchEnv, boolean quiet, FilePath pwd, @Nonnull String... args) throws IOException, InterruptedException { return launch(launchEnv, quiet, pwd, new ArgumentListBuilder(args)); } + + /** + * @param launchEnv + * @param quiet + * @param pwd + * @param args + * @return + * @throws IOException + * @throws InterruptedException + */ private LaunchResult launch(@Nonnull EnvVars launchEnv, boolean quiet, FilePath pwd, @Nonnull ArgumentListBuilder args) throws IOException, InterruptedException { // Prepend the docker command args.prepend(DockerTool.getExecutable(toolName, node, launcher.getListener(), launchEnv)); @@ -319,6 +376,8 @@ private LaunchResult launch(@Nonnull EnvVars launchEnv, boolean quiet, FilePath * Who is executing this {@link DockerClient} instance. * * @return a {@link String} containing the uid:gid. + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception */ public String whoAmI() throws IOException, InterruptedException { if (!launcher.isUnix()) { @@ -340,8 +399,9 @@ public String whoAmI() throws IOException, InterruptedException { * Checks if this {@link DockerClient} instance is running inside a container and returns the id of the container * if so. * - * @return an optional string containing the container id, or absent if - * it isn't containerized. + * @return an optional string containing the container id, or absent if it isn't containerized. + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception * @see Discussion */ public Optional getContainerIdIfContainerized() throws IOException, InterruptedException { @@ -355,6 +415,15 @@ public Optional getContainerIdIfContainerized() throws IOException, Inte return ControlGroup.getContainerId(cgroupFile); } + /** + * Gets container record. + * + * @param launchEnv the launch env + * @param containerId the container id + * @return the container record + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception + */ public ContainerRecord getContainerRecord(@Nonnull EnvVars launchEnv, String containerId) throws IOException, InterruptedException { String host = inspectRequiredField(launchEnv, containerId, ".Config.Hostname"); String containerName = inspectRequiredField(launchEnv, containerId, ".Name"); @@ -363,17 +432,18 @@ public ContainerRecord getContainerRecord(@Nonnull EnvVars launchEnv, String con // TODO get tags and add for ContainerRecord return new ContainerRecord(host, containerId, image, containerName, - (created != null ? created.getTime() : 0L), - Collections.emptyMap()); + (created != null ? created.getTime() : 0L), + Collections.emptyMap()); } /** * Inspect the mounts of a container. * These might have been declared {@code VOLUME}s, or mounts defined via {@code --volume}. - * @param launchEnv Docker client launch environment. + * + * @param launchEnv Docker client launch environment. * @param containerID The container ID. * @return a list of filesystem paths inside the container - * @throws IOException Execution error. Also fails if cannot retrieve the requested field from the request + * @throws IOException Execution error. Also fails if cannot retrieve the requested field from the request * @throws InterruptedException Interrupted */ public List getVolumes(@Nonnull EnvVars launchEnv, String containerID) throws IOException, InterruptedException { @@ -388,4 +458,50 @@ public List getVolumes(@Nonnull EnvVars launchEnv, String containerID) t } return Arrays.asList(volumes.replace("\\", "/").split("\\n")); } + + /** + * Run command string. + * + * @return the string + */ + public String runCommand() { + return "cat"; + } + + /** + * Is need to containerize path boolean. + * + * @return the boolean + */ + public boolean isNeedToContainerizePath() { + return needToContainerizePath; + } + + /** + * Sets need to containerize path. + * + * @param needToContainerizePath the need to containerize path + */ + public void setNeedToContainerizePath(boolean needToContainerizePath) { + this.needToContainerizePath = needToContainerizePath; + } + + /** + * Is container unix boolean. + * + * @return the boolean + */ + public boolean isContainerUnix() { + return isContainerUnix; + } + + /** + * Sets container unix. + * + * @param containerUnix the container unix + */ + public void setContainerUnix(boolean containerUnix) { + isContainerUnix = containerUnix; + } + } 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 2de11c25d..3b4052961 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 @@ -17,18 +17,44 @@ import java.util.logging.Level; import java.util.logging.Logger; +/** + * The type Windows docker client. + */ public class WindowsDockerClient extends DockerClient { private static final Logger LOGGER = Logger.getLogger(WindowsDockerClient.class.getName()); private final Launcher launcher; private final Node node; + private boolean needToContainerizePath = false; + private boolean isContainerUnix = false; + /** + * Instantiates a new Windows docker client. + * + * @param launcher the launcher + * @param node the node + * @param toolName the tool name + */ public WindowsDockerClient(@Nonnull Launcher launcher, @CheckForNull Node node, @CheckForNull String toolName) { super(launcher, node, toolName); this.launcher = launcher; this.node = node; } + /** + * @param launchEnv Docker client launch environment. + * @param image The image name. + * @param args Any additional arguments for the {@code docker run} command. + * @param workdir The working directory in the container, or {@code null} for default. + * @param volumes Volumes to be bound. Supply an empty list if no volumes are to be bound. + * @param volumesFromContainers Mounts all volumes from the given containers. + * @param containerEnv Environment variables to set in container. + * @param user The uid:gid to execute the container command as. Use {@link #whoAmI()}. + * @param command The command to execute in the image container being run. + * @return The container ID. + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception + */ @Override public String run(@Nonnull EnvVars launchEnv, @Nonnull String image, @CheckForNull String args, @CheckForNull String workdir, @Nonnull Map volumes, @Nonnull Collection volumesFromContainers, @Nonnull EnvVars containerEnv, @Nonnull String user, @Nonnull String... command) throws IOException, InterruptedException { ArgumentListBuilder argb = new ArgumentListBuilder("docker", "run", "-d", "-t"); @@ -37,10 +63,10 @@ public String run(@Nonnull EnvVars launchEnv, @Nonnull String image, @CheckForNu } if (workdir != null) { - argb.add("-w", workdir); + argb.add("-w", containerizePathIfNeeded(workdir)); } for (Map.Entry volume : volumes.entrySet()) { - argb.add("-v", volume.getKey() + ":" + volume.getValue()); + argb.add("-v", volume.getKey() + ":" + containerizePathIfNeeded(volume.getValue())); } for (String containerId : volumesFromContainers) { argb.add("--volumes-from", containerId); @@ -59,8 +85,18 @@ public String run(@Nonnull EnvVars launchEnv, @Nonnull String image, @CheckForNu } } + /** + * @param launchEnv the launch env + * @param containerId the container id + * @return returns Process + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception + */ @Override public List listProcess(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws IOException, InterruptedException { + if (isContainerUnix) { + return listProcessUnixContainer(launchEnv, containerId); + } 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())); @@ -73,7 +109,7 @@ public List listProcess(@Nonnull EnvVars launchEnv, @Nonnull String cont while ((line = in.readLine()) != null) { final StringTokenizer stringTokenizer = new StringTokenizer(line, " "); if (stringTokenizer.countTokens() < 1) { - throw new IOException("Unexpected `docker top` output : "+line); + throw new IOException("Unexpected `docker top` output : " + line); } processes.add(stringTokenizer.nextToken()); // COMMAND @@ -82,6 +118,55 @@ public List listProcess(@Nonnull EnvVars launchEnv, @Nonnull String cont return processes; } + /** + * List process unix container list. + * + * @param launchEnv the launch env + * @param containerId the container id + * @return the list + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception + */ + public List listProcessUnixContainer(@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; + } + + /** + * @return command to run as entry-point + */ + @Override + public String runCommand() { + if (isContainerUnix) { + return "cat"; + } + return "cmd"; + } + + /** + * @return Container ID + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception + */ @Override public Optional getContainerIdIfContainerized() throws IOException, InterruptedException { if (node == null || @@ -90,13 +175,13 @@ public Optional getContainerIdIfContainerized() throws IOException, Inte } LaunchResult getComputerName = launch(new EnvVars(), true, null, "hostname"); - if(getComputerName.getStatus() != 0) { + if (getComputerName.getStatus() != 0) { throw new IOException("Failed to get hostname."); } String shortID = getComputerName.getOut().toLowerCase(); LaunchResult getLongIdResult = launch(new EnvVars(), true, null, "docker", "inspect", shortID, "--format={{.Id}}"); - if(getLongIdResult.getStatus() != 0) { + if (getLongIdResult.getStatus() != 0) { LOGGER.log(Level.INFO, "Running inside of a container but cannot determine container ID from current environment."); return Optional.absent(); } @@ -104,6 +189,11 @@ public Optional getContainerIdIfContainerized() throws IOException, Inte return Optional.of(getLongIdResult.getOut()); } + /** + * @return Current User + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception + */ @Override public String whoAmI() throws IOException, InterruptedException { try (ByteArrayOutputStream userId = new ByteArrayOutputStream()) { @@ -112,9 +202,28 @@ public String whoAmI() throws IOException, InterruptedException { } } + /** + * @param env + * @param quiet + * @param workDir + * @param args + * @return result of command executed + * @throws IOException the io exception + * @throws InterruptedException + */ private LaunchResult launch(EnvVars env, boolean quiet, FilePath workDir, String... args) throws IOException, InterruptedException { return launch(env, quiet, workDir, new ArgumentListBuilder(args)); } + + /** + * @param env + * @param quiet + * @param workDir + * @param argb + * @return + * @throws IOException + * @throws InterruptedException + */ private 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); @@ -135,4 +244,153 @@ private LaunchResult launch(EnvVars env, boolean quiet, FilePath workDir, Argume return result; } + + /** + * @return boolean path need to be containerize + */ + public boolean isNeedToContainerizePath() { + return needToContainerizePath; + } + + /** + * @param needToContainerizePath the need to containerize path + */ + public void setNeedToContainerizePath(boolean needToContainerizePath) { + this.needToContainerizePath = needToContainerizePath; + } + + + /** + * @return boolean container type (unix or windows) + */ + public boolean isContainerUnix() { + return isContainerUnix; + } + + /** + * @param containerUnix the container unix + */ + public void setContainerUnix(boolean containerUnix) { + isContainerUnix = containerUnix; + } + + /** + * Containerize path if needed string. + * + * @param path the path + * @return the string + */ + public String containerizePathIfNeeded(String path) { + return containerizePathIfNeeded(path, null); + } + + /** + * Containerize path if needed string. + * + * @param path the path + * @param prefix the prefix + * @return the string + */ + public String containerizePathIfNeeded(String path, String prefix) { + if (this.needToContainerizePath) + return WindowsDockerClient.containerizePath(path, prefix); + return path; + } + + /** + * Containerize path string. + * + * @param path the path + * @param prefix the prefix + * @return the string + */ + 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(); + + } + + /** + * @param pathChars + * @param index + * @param prefixChars + * @return + */ + 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; + } }