Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
package org.jenkinsci.plugins.docker.workflow;

import com.google.common.base.Optional;
import org.jenkinsci.plugins.docker.workflow.client.DockerClient;
import com.google.inject.Inject;
import hudson.AbortException;
import hudson.EnvVars;
Expand All @@ -39,30 +38,21 @@
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.slaves.WorkspaceList;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.Charset;
import hudson.util.VersionNumber;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
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 java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nonnull;

import hudson.util.VersionNumber;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.annotation.CheckForNull;
import org.jenkinsci.plugins.docker.commons.fingerprint.DockerFingerprints;
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;
Expand All @@ -73,6 +63,17 @@
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.Charset;
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());
Expand Down Expand Up @@ -111,7 +112,6 @@ 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;
Expand All @@ -125,6 +125,9 @@ public static class Execution extends AbstractStepExecutionImpl {
private String container;
private String toolName;

public Execution() {
}

@Override public boolean start() throws Exception {
EnvVars envReduced = new EnvVars(env);
EnvVars envHost = computer.getEnvironment();
Expand All @@ -136,24 +139,29 @@ public static class Execution extends AbstractStepExecutionImpl {

LOGGER.log(Level.FINE, "reduced environment: {0}", envReduced);
workspace.mkdirs(); // otherwise it may be owned by root when created for -v
String ws = workspace.getRemote();
String ws = getPath(workspace);
toolName = step.toolName;
DockerClient dockerClient = new DockerClient(launcher, node, toolName);
DockerClient dockerClient = launcher.isUnix()
? new DockerClient(launcher, node, toolName)
: new WindowsDockerClient(launcher, node, toolName);

VersionNumber dockerVersion = dockerClient.version();
if (dockerVersion != null) {
if (dockerVersion.isOlderThan(new VersionNumber("1.7"))) {
throw new AbortException("The docker version is less than v1.7. Pipeline functions requiring 'docker exec' (e.g. 'docker.inside') or SELinux labeling will not work.");
} else if (dockerVersion.isOlderThan(new VersionNumber("1.8"))) {
listener.error("The docker version is less than v1.8. Running a 'docker.inside' from inside a container will not work.");
} else if (dockerVersion.isOlderThan(new VersionNumber("1.13"))) {
if (!launcher.isUnix())
throw new AbortException("The docker version is less than v1.13. Running a 'docker.inside' from inside a Windows container will not work.");
}
} else {
listener.error("Failed to parse docker version. Please note there is a minimum docker version requirement of v1.7.");
}

FilePath tempDir = tempDir(workspace);
tempDir.mkdirs();
String tmp = tempDir.getRemote();
String tmp = getPath(tempDir);

Map<String, String> volumes = new LinkedHashMap<String, String>();
Collection<String> volumesFromContainers = new LinkedHashSet<String>();
Expand All @@ -166,7 +174,11 @@ public static class Execution extends AbstractStepExecutionImpl {
// check if there is any volume which contains the directory
boolean found = false;
for (String vol : mountedVolumes) {
if (dir.startsWith(vol)) {
boolean dirStartsWithVol = launcher.isUnix()
? dir.startsWith(vol) // Linux
: dir.toLowerCase().startsWith(vol.toLowerCase()); // Windows

if (dirStartsWithVol) {
volumesFromContainers.add(containerId.get());
found = true;
break;
Expand All @@ -183,9 +195,10 @@ public static class Execution extends AbstractStepExecutionImpl {
volumes.put(tmp, tmp);
}

container = dockerClient.run(env, step.image, step.args, ws, volumes, volumesFromContainers, envReduced, dockerClient.whoAmI(), /* expected to hang until killed */ "cat");
String command = launcher.isUnix() ? "cat" : "cmd.exe";
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("cat")) {
if (!ps.contains(command)) {
listener.error(
"The container started but didn't run the expected command. " +
"Please double check your ENTRYPOINT does execute the command passed as docker run argument, " +
Expand All @@ -202,6 +215,15 @@ public static class Execution extends AbstractStepExecutionImpl {
return false;
}

private String getPath(FilePath filePath)
throws IOException, InterruptedException {
if (launcher.isUnix()) {
return filePath.getRemote();
} else {
return filePath.toURI().getPath().substring(1).replace("\\", "/");
}
}

// TODO use 1.652 use WorkspaceList.tempDir
private static FilePath tempDir(FilePath ws) {
return ws.sibling(ws.getName() + System.getProperty(WorkspaceList.class.getName(), "@") + "tmp");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,6 @@
import hudson.model.Node;
import hudson.util.ArgumentListBuilder;
import hudson.util.VersionNumber;
import org.jenkinsci.plugins.docker.commons.fingerprint.ContainerRecord;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
Expand All @@ -44,18 +40,22 @@
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.Map;
import java.util.List;
import java.util.Arrays;
import java.util.Map;
import java.util.StringTokenizer;
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;
Expand Down Expand Up @@ -106,7 +106,12 @@ public DockerClient(@Nonnull Launcher launcher, @CheckForNull Node node, @CheckF
public String run(@Nonnull EnvVars launchEnv, @Nonnull String image, @CheckForNull String args, @CheckForNull String workdir, @Nonnull Map<String, String> volumes, @Nonnull Collection<String> volumesFromContainers, @Nonnull EnvVars containerEnv, @Nonnull String user, @Nonnull String... command) throws IOException, InterruptedException {
ArgumentListBuilder argb = new ArgumentListBuilder();

argb.add("run", "-t", "-d", "-u", user);
argb.add("run", "-t", "-d");

// Username might be empty because we are running on Windows
if (StringUtils.isNotEmpty(user)) {
argb.add("-u", user);
}
if (args != null) {
argb.addTokenized(args);
}
Expand Down Expand Up @@ -306,6 +311,10 @@ private LaunchResult launch(@CheckForNull @Nonnull EnvVars launchEnv, boolean qu
* @return a {@link String} containing the <strong>uid:gid</strong>.
*/
public String whoAmI() throws IOException, InterruptedException {
if (!launcher.isUnix()) {
// Windows does not support username
return "";
}
ByteArrayOutputStream userId = new ByteArrayOutputStream();
launcher.launch().cmds("id", "-u").quiet(true).stdout(userId).start().joinWithTimeout(CLIENT_TIMEOUT, TimeUnit.SECONDS, launcher.getListener());

Expand Down Expand Up @@ -367,6 +376,6 @@ public List<String> getVolumes(@Nonnull EnvVars launchEnv, String containerID) t
if (volumes.isEmpty()) {
return Collections.emptyList();
}
return Arrays.asList(volumes.split("\\n"));
return Arrays.asList(volumes.replace("\\", "/").split("\\n"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
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 WindowsDockerClient extends DockerClient {
private static final Logger LOGGER = Logger.getLogger(WindowsDockerClient.class.getName());

private final Launcher launcher;
private final Node node;

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

@Override
public String run(@Nonnull EnvVars launchEnv, @Nonnull String image, @CheckForNull String args, @CheckForNull String workdir, @Nonnull Map<String, String> volumes, @Nonnull Collection<String> volumesFromContainers, @Nonnull EnvVars containerEnv, @Nonnull String user, @Nonnull String... command) throws IOException, InterruptedException {
ArgumentListBuilder argb = new ArgumentListBuilder("docker", "run", "-d", "-t");
if (args != null) {
argb.addTokenized(args);
}

if (workdir != null) {
argb.add("-w", workdir);
}
for (Map.Entry<String, String> volume : volumes.entrySet()) {
argb.add("-v", volume.getKey() + ":" + volume.getValue());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should we use --mount instead?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

As an aside, I notice the regular docker client uses volumes instead of mounts as well. Should probably keep them consistent (which I suspect means switching DockerClient.java to mounts too mind you - and a separate PR)

}
for (String containerId : volumesFromContainers) {
argb.add("--volumes-from", containerId);
}
for (Map.Entry<String, String> variable : containerEnv.entrySet()) {
argb.add("-e");
argb.addMasked(variable.getKey()+"="+variable.getValue());
}
argb.add(image).add(command);

LaunchResult result = launch(launchEnv, false, null, argb);
if (result.getStatus() == 0) {
return result.getOut();
} else {
throw new IOException(String.format("Failed to run image '%s'. Error: %s", image, result.getErr()));
}
}

@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() < 1) {
throw new IOException("Unexpected `docker top` output : "+line);
}

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

@Override
public Optional<String> getContainerIdIfContainerized() throws IOException, InterruptedException {
if (node == null ||
launch(new EnvVars(), true, null, "sc.exe", "query", "cexecsvc").getStatus() != 0) {
return Optional.absent();
}

LaunchResult getComputerName = launch(new EnvVars(), true, null, "hostname");
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) {
LOGGER.log(Level.INFO, "Running inside of a container but cannot determine container ID from current environment.");
return Optional.absent();
}

return Optional.of(getLongIdResult.getOut());
}

@Override
public String whoAmI() throws IOException, InterruptedException {
try (ByteArrayOutputStream userId = new ByteArrayOutputStream()) {
launcher.launch().cmds("whoami").quiet(true).stdout(userId).start().joinWithTimeout(CLIENT_TIMEOUT, TimeUnit.SECONDS, launcher.getListener());
return userId.toString(Charset.defaultCharset().name()).trim();
}
}

private 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 {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Executing command \"{0}\"", argb);
}

Launcher.ProcStarter procStarter = launcher.launch();
if (workDir != null) {
procStarter.pwd(workDir);
}

LaunchResult result = new LaunchResult();
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayOutputStream err = new ByteArrayOutputStream();
result.setStatus(procStarter.quiet(quiet).cmds(argb).envs(env).stdout(out).stderr(err).start().joinWithTimeout(CLIENT_TIMEOUT, TimeUnit.SECONDS, launcher.getListener()));
final String charsetName = Charset.defaultCharset().name();
result.setOut(out.toString(charsetName));
result.setErr(err.toString(charsetName));

return result;
}
}
Loading