Skip to content

Commit 12f27b0

Browse files
rbutcherjetersen
authored andcommitted
Added support to run Docker stages on Windows slaves
1 parent 38c4a56 commit 12f27b0

File tree

5 files changed

+222
-26
lines changed

5 files changed

+222
-26
lines changed

src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
package org.jenkinsci.plugins.docker.workflow;
2525

2626
import com.google.common.base.Optional;
27+
import hudson.slaves.NodeProperty;
28+
import hudson.slaves.NodePropertyDescriptor;
29+
import hudson.util.DescribableList;
2730
import org.jenkinsci.plugins.docker.workflow.client.DockerClient;
2831
import com.google.inject.Inject;
2932
import hudson.AbortException;
@@ -63,6 +66,7 @@
6366
import javax.annotation.CheckForNull;
6467
import org.jenkinsci.plugins.docker.commons.fingerprint.DockerFingerprints;
6568
import org.jenkinsci.plugins.docker.commons.tools.DockerTool;
69+
import org.jenkinsci.plugins.docker.workflow.client.WindowsDockerClient;
6670
import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl;
6771
import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl;
6872
import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl;
@@ -111,7 +115,6 @@ private static void destroy(String container, Launcher launcher, Node node, EnvV
111115

112116
// TODO switch to GeneralNonBlockingStepExecution
113117
public static class Execution extends AbstractStepExecutionImpl {
114-
115118
private static final long serialVersionUID = 1;
116119
@Inject(optional=true) private transient WithContainerStep step;
117120
@StepContextParameter private transient Launcher launcher;
@@ -138,7 +141,9 @@ public static class Execution extends AbstractStepExecutionImpl {
138141
workspace.mkdirs(); // otherwise it may be owned by root when created for -v
139142
String ws = workspace.getRemote();
140143
toolName = step.toolName;
141-
DockerClient dockerClient = new DockerClient(launcher, node, toolName);
144+
DockerClient dockerClient = launcher.isUnix()
145+
? new DockerClient(launcher, node, toolName)
146+
: new WindowsDockerClient(launcher, node, toolName);
142147

143148
VersionNumber dockerVersion = dockerClient.version();
144149
if (dockerVersion != null) {
@@ -166,7 +171,11 @@ public static class Execution extends AbstractStepExecutionImpl {
166171
// check if there is any volume which contains the directory
167172
boolean found = false;
168173
for (String vol : mountedVolumes) {
169-
if (dir.startsWith(vol)) {
174+
boolean dirStartsWithVol = launcher.isUnix()
175+
? dir.startsWith(vol) // Linux
176+
: dir.toLowerCase().startsWith(vol.toLowerCase()); // Windows
177+
178+
if (dirStartsWithVol) {
170179
volumesFromContainers.add(containerId.get());
171180
found = true;
172181
break;
@@ -183,9 +192,10 @@ public static class Execution extends AbstractStepExecutionImpl {
183192
volumes.put(tmp, tmp);
184193
}
185194

186-
container = dockerClient.run(env, step.image, step.args, ws, volumes, volumesFromContainers, envReduced, dockerClient.whoAmI(), /* expected to hang until killed */ "cat");
195+
String command = launcher.isUnix() ? "cat" : "cmd.exe";
196+
container = dockerClient.run(env, step.image, step.args, ws, volumes, volumesFromContainers, envReduced, dockerClient.whoAmI(), /* expected to hang until killed */ command);
187197
final List<String> ps = dockerClient.listProcess(env, container);
188-
if (!ps.contains("cat")) {
198+
if (!ps.contains(command)) {
189199
listener.error(
190200
"The container started but didn't run the expected command. " +
191201
"Please double check your ENTRYPOINT does execute the command passed as docker run argument, " +
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package org.jenkinsci.plugins.docker.workflow.client;
2+
3+
import com.google.common.base.Optional;
4+
import hudson.EnvVars;
5+
import hudson.FilePath;
6+
import hudson.Launcher;
7+
import hudson.model.Node;
8+
import hudson.util.ArgumentListBuilder;
9+
10+
import javax.annotation.CheckForNull;
11+
import javax.annotation.Nonnull;
12+
import java.io.*;
13+
import java.nio.charset.Charset;
14+
import java.util.*;
15+
import java.util.concurrent.TimeUnit;
16+
import java.util.logging.Level;
17+
import java.util.logging.Logger;
18+
19+
public class WindowsDockerClient extends DockerClient {
20+
private static final Logger LOGGER = Logger.getLogger(WindowsDockerClient.class.getName());
21+
22+
private final Launcher launcher;
23+
private final Node node;
24+
25+
public WindowsDockerClient(@Nonnull Launcher launcher, @CheckForNull Node node, @CheckForNull String toolName) {
26+
super(launcher, node, toolName);
27+
this.launcher = launcher;
28+
this.node = node;
29+
}
30+
31+
@Override
32+
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 {
33+
ArgumentListBuilder argb = new ArgumentListBuilder("docker", "run", "-d", "-t");
34+
if (args != null) {
35+
argb.addTokenized(args);
36+
}
37+
38+
if (workdir != null) {
39+
argb.add("-w", workdir);
40+
}
41+
for (Map.Entry<String, String> volume : volumes.entrySet()) {
42+
argb.add("-v", volume.getKey() + ":" + volume.getValue());
43+
}
44+
for (String containerId : volumesFromContainers) {
45+
argb.add("--volumes-from", containerId);
46+
}
47+
for (Map.Entry<String, String> variable : containerEnv.entrySet()) {
48+
argb.add("-e");
49+
argb.addMasked(variable.getKey()+"="+variable.getValue());
50+
}
51+
argb.add(image).add(command);
52+
53+
LaunchResult result = launch(launchEnv, false, null, argb);
54+
if (result.getStatus() == 0) {
55+
return result.getOut();
56+
} else {
57+
throw new IOException(String.format("Failed to run image '%s'. Error: %s", image, result.getErr()));
58+
}
59+
}
60+
61+
@Override
62+
public List<String> listProcess(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws IOException, InterruptedException {
63+
LaunchResult result = launch(launchEnv, false, null, "docker", "top", containerId);
64+
if (result.getStatus() != 0) {
65+
throw new IOException(String.format("Failed to run top '%s'. Error: %s", containerId, result.getErr()));
66+
}
67+
List<String> processes = new ArrayList<>();
68+
try (Reader r = new StringReader(result.getOut());
69+
BufferedReader in = new BufferedReader(r)) {
70+
String line;
71+
in.readLine(); // ps header
72+
while ((line = in.readLine()) != null) {
73+
final StringTokenizer stringTokenizer = new StringTokenizer(line, " ");
74+
if (stringTokenizer.countTokens() < 1) {
75+
throw new IOException("Unexpected `docker top` output : "+line);
76+
}
77+
78+
processes.add(stringTokenizer.nextToken()); // COMMAND
79+
}
80+
}
81+
return processes;
82+
}
83+
84+
@Override
85+
public Optional<String> getContainerIdIfContainerized() throws IOException, InterruptedException {
86+
if (node == null ||
87+
launch(new EnvVars(), true, null, "sc.exe", "query", "cexecsvc").getStatus() != 0) {
88+
return Optional.absent();
89+
}
90+
91+
LaunchResult getComputerName = launch(new EnvVars(), true, null, "hostname");
92+
if(getComputerName.getStatus() != 0) {
93+
throw new IOException("Failed to get hostname.");
94+
}
95+
96+
String shortID = getComputerName.getOut().toLowerCase();
97+
LaunchResult getLongIdResult = launch(new EnvVars(), true, null, "docker", "inspect", shortID, "--format={{.Id}}");
98+
if(getLongIdResult.getStatus() != 0) {
99+
LOGGER.log(Level.INFO, "Running inside of a container but cannot determine container ID from current environment.");
100+
return Optional.absent();
101+
}
102+
103+
return Optional.of(getLongIdResult.getOut());
104+
}
105+
106+
private LaunchResult launch(@Nonnull EnvVars env, boolean quiet, FilePath workDir, String... args) throws IOException, InterruptedException {
107+
return launch(env, quiet, workDir, new ArgumentListBuilder(args));
108+
}
109+
private LaunchResult launch(@CheckForNull @Nonnull EnvVars env, boolean quiet, FilePath workDir, ArgumentListBuilder argb) throws IOException, InterruptedException {
110+
if (LOGGER.isLoggable(Level.FINE)) {
111+
LOGGER.log(Level.FINE, "Executing command \"{0}\"", argb);
112+
}
113+
114+
Launcher.ProcStarter procStarter = launcher.launch();
115+
if (workDir != null) {
116+
procStarter.pwd(workDir);
117+
}
118+
119+
LaunchResult result = new LaunchResult();
120+
ByteArrayOutputStream out = new ByteArrayOutputStream();
121+
ByteArrayOutputStream err = new ByteArrayOutputStream();
122+
result.setStatus(procStarter.quiet(quiet).cmds(argb).envs(env).stdout(out).stderr(err).start().joinWithTimeout(CLIENT_TIMEOUT, TimeUnit.SECONDS, launcher.getListener()));
123+
final String charsetName = Charset.defaultCharset().name();
124+
result.setOut(out.toString(charsetName));
125+
result.setErr(err.toString(charsetName));
126+
127+
return result;
128+
}
129+
}

src/test/java/org/jenkinsci/plugins/docker/workflow/DockerTestUtil.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
*/
2424
package org.jenkinsci.plugins.docker.workflow;
2525

26+
import hudson.EnvVars;
2627
import org.jenkinsci.plugins.docker.workflow.client.DockerClient;
2728
import hudson.Launcher;
2829
import hudson.util.StreamTaskListener;
@@ -64,6 +65,25 @@ public static void assumeNotWindows() throws Exception {
6465
Assume.assumeFalse(System.getProperty("os.name").toLowerCase().contains("windows"));
6566
}
6667

68+
public static EnvVars newDockerLaunchEnv() {
69+
// Create the KeyMaterial for connecting to the docker host/server.
70+
// E.g. currently need to add something like the following to your env
71+
// -DDOCKER_HOST_FOR_TEST="tcp://192.168.x.y:2376"
72+
// -DDOCKER_HOST_KEY_DIR_FOR_TEST="/Users/tfennelly/.boot2docker/certs/boot2docker-vm"
73+
final String docker_host_for_test = System.getProperty("DOCKER_HOST_FOR_TEST");
74+
final String docker_host_key_dir_for_test = System.getProperty("DOCKER_HOST_KEY_DIR_FOR_TEST");
75+
76+
EnvVars env = new EnvVars();
77+
if (docker_host_for_test != null) {
78+
env.put("DOCKER_HOST", docker_host_for_test);
79+
}
80+
if (docker_host_key_dir_for_test != null) {
81+
env.put("DOCKER_TLS_VERIFY", "1");
82+
env.put("DOCKER_CERT_PATH", docker_host_key_dir_for_test);
83+
}
84+
return env;
85+
}
86+
6787
private DockerTestUtil() {}
6888

6989
}

src/test/java/org/jenkinsci/plugins/docker/workflow/client/DockerClientTest.java

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public void setup() throws Exception {
5757

5858
@Test
5959
public void test_run() throws IOException, InterruptedException {
60-
EnvVars launchEnv = newLaunchEnv();
60+
EnvVars launchEnv = DockerTestUtil.newDockerLaunchEnv();
6161
String containerId =
6262
dockerClient.run(launchEnv, "learn/tutorial", null, null, Collections.<String, String>emptyMap(), Collections.<String>emptyList(), new EnvVars(),
6363
dockerClient.whoAmI(), "cat");
@@ -87,24 +87,4 @@ public void test_valid_version() {
8787
public void test_invalid_version() {
8888
Assert.assertNull(DockerClient.parseVersionNumber("xxx"));
8989
}
90-
91-
92-
private EnvVars newLaunchEnv() {
93-
// Create the KeyMaterial for connecting to the docker host/server.
94-
// E.g. currently need to add something like the following to your env
95-
// -DDOCKER_HOST_FOR_TEST="tcp://192.168.x.y:2376"
96-
// -DDOCKER_HOST_KEY_DIR_FOR_TEST="/Users/tfennelly/.boot2docker/certs/boot2docker-vm"
97-
final String docker_host_for_test = System.getProperty("DOCKER_HOST_FOR_TEST");
98-
final String docker_host_key_dir_for_test = System.getProperty("DOCKER_HOST_KEY_DIR_FOR_TEST");
99-
100-
EnvVars env = new EnvVars();
101-
if (docker_host_for_test != null) {
102-
env.put("DOCKER_HOST", docker_host_for_test);
103-
}
104-
if (docker_host_key_dir_for_test != null) {
105-
env.put("DOCKER_TLS_VERIFY", "1");
106-
env.put("DOCKER_CERT_PATH", docker_host_key_dir_for_test);
107-
}
108-
return env;
109-
}
11090
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package org.jenkinsci.plugins.docker.workflow.client;
2+
3+
import hudson.EnvVars;
4+
import hudson.Launcher;
5+
import hudson.model.TaskListener;
6+
import hudson.util.StreamTaskListener;
7+
import org.jenkinsci.plugins.docker.commons.fingerprint.ContainerRecord;
8+
import org.jenkinsci.plugins.docker.workflow.DockerTestUtil;
9+
import org.junit.Assert;
10+
import org.junit.Before;
11+
import org.junit.Test;
12+
13+
import java.io.IOException;
14+
import java.util.Collections;
15+
16+
public class WindowsDockerClientTest {
17+
18+
private DockerClient dockerClient;
19+
20+
@Before
21+
public void setup() throws Exception {
22+
DockerTestUtil.assumeDocker();
23+
24+
TaskListener taskListener = StreamTaskListener.fromStderr();
25+
Launcher.LocalLauncher launcher = new Launcher.LocalLauncher(taskListener);
26+
27+
dockerClient = new WindowsDockerClient(launcher, null, null);
28+
}
29+
30+
@Test
31+
public void test_run() throws IOException, InterruptedException {
32+
EnvVars launchEnv = DockerTestUtil.newDockerLaunchEnv();
33+
String containerId = dockerClient.run(
34+
launchEnv,
35+
"microsoft/nanoserver",
36+
null,
37+
null,
38+
Collections.emptyMap(),
39+
Collections.emptyList(),
40+
new EnvVars(),
41+
dockerClient.whoAmI(),
42+
"cmd");
43+
44+
Assert.assertEquals(64, containerId.length());
45+
ContainerRecord containerRecord = dockerClient.getContainerRecord(launchEnv, containerId);
46+
Assert.assertEquals(dockerClient.inspect(launchEnv, "microsoft/nanoserver", ".Id"), containerRecord.getImageId());
47+
Assert.assertTrue(containerRecord.getContainerName().length() > 0);
48+
Assert.assertTrue(containerRecord.getHost().length() > 0);
49+
Assert.assertTrue(containerRecord.getCreated() > 1000000000000L);
50+
Assert.assertEquals(Collections.<String>emptyList(), dockerClient.getVolumes(launchEnv, containerId));
51+
52+
// Also test that the stop works and cleans up after itself
53+
Assert.assertNotNull(dockerClient.inspect(launchEnv, containerId, ".Name"));
54+
dockerClient.stop(launchEnv, containerId);
55+
Assert.assertNull(dockerClient.inspect(launchEnv, containerId, ".Name"));
56+
}
57+
}

0 commit comments

Comments
 (0)