Skip to content
Closed
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 @@ -24,54 +24,33 @@
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;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.LauncherDecorator;
import hudson.Proc;
import hudson.Util;
import hudson.*;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry for the aggressive import changes. Alt+Enter on intellij 😄
I can change this back if necessary.

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 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.*;
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.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.Set;
import java.util.TreeSet;
import java.util.*;
import java.util.concurrent.TimeUnit;
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.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.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;

public class WithContainerStep extends AbstractStepImpl {

Expand Down Expand Up @@ -111,7 +90,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 @@ -138,14 +116,19 @@ public static class Execution extends AbstractStepExecutionImpl {
workspace.mkdirs(); // otherwise it may be owned by root when created for -v
String ws = workspace.getRemote();
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.");
Expand All @@ -166,7 +149,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 +170,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 Down
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());
}
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ class Docker implements Serializable {
new Image(this, id)
}

String shell() {
node {
Copy link
Member

Choose a reason for hiding this comment

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

Can be deleted.

script.isUnix() ? "sh" : "bat"
}
}

public Image build(String image, String args = '.') {
node {
def parsedArgs = args.split(/ (?=([^"']*["'][^"']*["'])*[^"']*$)/)
Expand Down Expand Up @@ -121,11 +127,11 @@ class Docker implements Serializable {
public <V> V inside(String args = '', Closure<V> body) {
docker.node {
def toRun = imageName()
if (toRun != id && docker.script.sh(script: "docker inspect -f . ${id}", returnStatus: true) == 0) {
if (toRun != id && docker.script."${docker.shell()}"(script: "docker inspect -f . ${id}", returnStatus: true) == 0) {
// Can run it without registry prefix, because it was locally built.
toRun = id
} else {
if (docker.script.sh(script: "docker inspect -f . ${toRun}", returnStatus: true) != 0) {
if (docker.script."${docker.shell()}"(script: "docker inspect -f . ${toRun}", returnStatus: true) != 0) {
// Not yet present locally.
// withDockerContainer requires the image to be available locally, since its start phase is not a durable task.
pull()
Expand All @@ -139,13 +145,13 @@ class Docker implements Serializable {

public void pull() {
docker.node {
docker.script.sh "docker pull ${imageName()}"
docker.script."${docker.shell()}" "docker pull ${imageName()}"
}
}

public Container run(String args = '', String command = "") {
docker.node {
def container = docker.script.sh(script: "docker run -d${args != '' ? ' ' + args : ''} ${id}${command != '' ? ' ' + command : ''}", returnStdout: true).trim()
def container = docker.script."${docker.shell()}"(script: "docker run -d${args != '' ? ' ' + args : ''} ${id}${command != '' ? ' ' + command : ''}", returnStdout: true).trim()
docker.script.dockerFingerprintRun containerId: container, toolName: docker.script.env.DOCKER_TOOL_NAME
new Container(docker, container)
}
Expand All @@ -165,7 +171,7 @@ class Docker implements Serializable {
public void tag(String tagName = parsedId.tag, boolean force = true) {
docker.node {
def taggedImageName = toQualifiedImageName(parsedId.userAndRepo + ':' + tagName)
docker.script.sh "docker tag ${id} ${taggedImageName}"
docker.script."${docker.shell()}" "docker tag ${id} ${taggedImageName}"
return taggedImageName;
}
}
Expand All @@ -175,7 +181,7 @@ class Docker implements Serializable {
// The image may have already been tagged, so the tagging may be a no-op.
// That's ok since tagging is cheap.
def taggedImageName = tag(tagName, force)
docker.script.sh "docker push ${taggedImageName}"
docker.script."${docker.shell()}" "docker push ${taggedImageName}"
}
}

Expand All @@ -192,11 +198,11 @@ class Docker implements Serializable {
}

public void stop() {
docker.script.sh "docker stop ${id} && docker rm -f ${id}"
docker.script."${docker.shell()}" "docker stop ${id} && docker rm -f ${id}"
}

public String port(int port) {
docker.script.sh(script: "docker port ${id} ${port}", returnStdout: true).trim()
docker.script."${docker.shell()}"(script: "docker port ${id} ${port}", returnStdout: true).trim()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*/
package org.jenkinsci.plugins.docker.workflow;

import hudson.EnvVars;
import org.jenkinsci.plugins.docker.workflow.client.DockerClient;
import hudson.Launcher;
import hudson.util.StreamTaskListener;
Expand Down Expand Up @@ -60,6 +61,25 @@ public static void assumeNotWindows() throws Exception {
Assume.assumeFalse(System.getProperty("os.name").toLowerCase().contains("windows"));
}

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
// -DDOCKER_HOST_FOR_TEST="tcp://192.168.x.y:2376"
// -DDOCKER_HOST_KEY_DIR_FOR_TEST="/Users/tfennelly/.boot2docker/certs/boot2docker-vm"
final String docker_host_for_test = System.getProperty("DOCKER_HOST_FOR_TEST");
final String docker_host_key_dir_for_test = System.getProperty("DOCKER_HOST_KEY_DIR_FOR_TEST");

EnvVars env = new EnvVars();
if (docker_host_for_test != null) {
env.put("DOCKER_HOST", docker_host_for_test);
}
if (docker_host_key_dir_for_test != null) {
env.put("DOCKER_TLS_VERIFY", "1");
env.put("DOCKER_CERT_PATH", docker_host_key_dir_for_test);
}
return env;
}

private DockerTestUtil() {}

}
Loading