Skip to content

Commit 6d01b80

Browse files
Merge pull request #184 from 3shapeAS/features/windows_slaves
Features/windows agents with pathing issues fixed
2 parents 19ebb47 + 6982db7 commit 6d01b80

File tree

7 files changed

+291
-60
lines changed

7 files changed

+291
-60
lines changed

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

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

2626
import com.google.common.base.Optional;
27-
import org.jenkinsci.plugins.docker.workflow.client.DockerClient;
2827
import com.google.inject.Inject;
2928
import hudson.AbortException;
3029
import hudson.EnvVars;
@@ -39,30 +38,21 @@
3938
import hudson.model.Run;
4039
import hudson.model.TaskListener;
4140
import hudson.slaves.WorkspaceList;
42-
import java.io.ByteArrayOutputStream;
43-
import java.io.IOException;
44-
import java.io.Serializable;
45-
import java.nio.charset.Charset;
41+
import hudson.util.VersionNumber;
4642
import java.util.ArrayList;
4743
import java.util.Arrays;
4844
import java.util.Collection;
4945
import java.util.Iterator;
50-
import java.util.List;
51-
import java.util.Map;
5246
import java.util.LinkedHashMap;
5347
import java.util.LinkedHashSet;
48+
import java.util.List;
49+
import java.util.Map;
5450
import java.util.Set;
5551
import java.util.TreeSet;
56-
import java.util.logging.Level;
57-
import java.util.logging.Logger;
58-
import javax.annotation.Nonnull;
59-
60-
import hudson.util.VersionNumber;
61-
import java.util.concurrent.TimeUnit;
62-
import java.util.stream.Collectors;
63-
import javax.annotation.CheckForNull;
6452
import org.jenkinsci.plugins.docker.commons.fingerprint.DockerFingerprints;
6553
import org.jenkinsci.plugins.docker.commons.tools.DockerTool;
54+
import org.jenkinsci.plugins.docker.workflow.client.DockerClient;
55+
import org.jenkinsci.plugins.docker.workflow.client.WindowsDockerClient;
6656
import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl;
6757
import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl;
6858
import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl;
@@ -73,6 +63,17 @@
7363
import org.kohsuke.stapler.DataBoundConstructor;
7464
import org.kohsuke.stapler.DataBoundSetter;
7565

66+
import javax.annotation.CheckForNull;
67+
import javax.annotation.Nonnull;
68+
import java.io.ByteArrayOutputStream;
69+
import java.io.IOException;
70+
import java.io.Serializable;
71+
import java.nio.charset.Charset;
72+
import java.util.concurrent.TimeUnit;
73+
import java.util.logging.Level;
74+
import java.util.logging.Logger;
75+
import java.util.stream.Collectors;
76+
7677
public class WithContainerStep extends AbstractStepImpl {
7778

7879
private static final Logger LOGGER = Logger.getLogger(WithContainerStep.class.getName());
@@ -111,7 +112,6 @@ private static void destroy(String container, Launcher launcher, Node node, EnvV
111112

112113
// TODO switch to GeneralNonBlockingStepExecution
113114
public static class Execution extends AbstractStepExecutionImpl {
114-
115115
private static final long serialVersionUID = 1;
116116
@Inject(optional=true) private transient WithContainerStep step;
117117
@StepContextParameter private transient Launcher launcher;
@@ -125,6 +125,9 @@ public static class Execution extends AbstractStepExecutionImpl {
125125
private String container;
126126
private String toolName;
127127

128+
public Execution() {
129+
}
130+
128131
@Override public boolean start() throws Exception {
129132
EnvVars envReduced = new EnvVars(env);
130133
EnvVars envHost = computer.getEnvironment();
@@ -136,24 +139,29 @@ public static class Execution extends AbstractStepExecutionImpl {
136139

137140
LOGGER.log(Level.FINE, "reduced environment: {0}", envReduced);
138141
workspace.mkdirs(); // otherwise it may be owned by root when created for -v
139-
String ws = workspace.getRemote();
142+
String ws = getPath(workspace);
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) {
145150
if (dockerVersion.isOlderThan(new VersionNumber("1.7"))) {
146151
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.");
147152
} else if (dockerVersion.isOlderThan(new VersionNumber("1.8"))) {
148153
listener.error("The docker version is less than v1.8. Running a 'docker.inside' from inside a container will not work.");
154+
} else if (dockerVersion.isOlderThan(new VersionNumber("1.13"))) {
155+
if (!launcher.isUnix())
156+
throw new AbortException("The docker version is less than v1.13. Running a 'docker.inside' from inside a Windows container will not work.");
149157
}
150158
} else {
151159
listener.error("Failed to parse docker version. Please note there is a minimum docker version requirement of v1.7.");
152160
}
153161

154162
FilePath tempDir = tempDir(workspace);
155163
tempDir.mkdirs();
156-
String tmp = tempDir.getRemote();
164+
String tmp = getPath(tempDir);
157165

158166
Map<String, String> volumes = new LinkedHashMap<String, String>();
159167
Collection<String> volumesFromContainers = new LinkedHashSet<String>();
@@ -166,7 +174,11 @@ public static class Execution extends AbstractStepExecutionImpl {
166174
// check if there is any volume which contains the directory
167175
boolean found = false;
168176
for (String vol : mountedVolumes) {
169-
if (dir.startsWith(vol)) {
177+
boolean dirStartsWithVol = launcher.isUnix()
178+
? dir.startsWith(vol) // Linux
179+
: dir.toLowerCase().startsWith(vol.toLowerCase()); // Windows
180+
181+
if (dirStartsWithVol) {
170182
volumesFromContainers.add(containerId.get());
171183
found = true;
172184
break;
@@ -183,9 +195,10 @@ public static class Execution extends AbstractStepExecutionImpl {
183195
volumes.put(tmp, tmp);
184196
}
185197

186-
container = dockerClient.run(env, step.image, step.args, ws, volumes, volumesFromContainers, envReduced, dockerClient.whoAmI(), /* expected to hang until killed */ "cat");
198+
String command = launcher.isUnix() ? "cat" : "cmd.exe";
199+
container = dockerClient.run(env, step.image, step.args, ws, volumes, volumesFromContainers, envReduced, dockerClient.whoAmI(), /* expected to hang until killed */ command);
187200
final List<String> ps = dockerClient.listProcess(env, container);
188-
if (!ps.contains("cat")) {
201+
if (!ps.contains(command)) {
189202
listener.error(
190203
"The container started but didn't run the expected command. " +
191204
"Please double check your ENTRYPOINT does execute the command passed as docker run argument, " +
@@ -202,6 +215,15 @@ public static class Execution extends AbstractStepExecutionImpl {
202215
return false;
203216
}
204217

218+
private String getPath(FilePath filePath)
219+
throws IOException, InterruptedException {
220+
if (launcher.isUnix()) {
221+
return filePath.getRemote();
222+
} else {
223+
return filePath.toURI().getPath().substring(1).replace("\\", "/");
224+
}
225+
}
226+
205227
// TODO use 1.652 use WorkspaceList.tempDir
206228
private static FilePath tempDir(FilePath ws) {
207229
return ws.sibling(ws.getName() + System.getProperty(WorkspaceList.class.getName(), "@") + "tmp");

src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,6 @@
3131
import hudson.model.Node;
3232
import hudson.util.ArgumentListBuilder;
3333
import hudson.util.VersionNumber;
34-
import org.jenkinsci.plugins.docker.commons.fingerprint.ContainerRecord;
35-
36-
import javax.annotation.CheckForNull;
37-
import javax.annotation.Nonnull;
3834
import java.io.BufferedReader;
3935
import java.io.ByteArrayOutputStream;
4036
import java.io.IOException;
@@ -44,18 +40,22 @@
4440
import java.text.ParseException;
4541
import java.text.SimpleDateFormat;
4642
import java.util.ArrayList;
43+
import java.util.Arrays;
4744
import java.util.Collection;
4845
import java.util.Collections;
4946
import java.util.Date;
50-
import java.util.Map;
5147
import java.util.List;
52-
import java.util.Arrays;
48+
import java.util.Map;
5349
import java.util.StringTokenizer;
5450
import java.util.concurrent.TimeUnit;
5551
import java.util.logging.Level;
5652
import java.util.logging.Logger;
5753
import java.util.regex.Matcher;
5854
import java.util.regex.Pattern;
55+
import javax.annotation.CheckForNull;
56+
import javax.annotation.Nonnull;
57+
import org.apache.commons.lang.StringUtils;
58+
import org.jenkinsci.plugins.docker.commons.fingerprint.ContainerRecord;
5959
import org.jenkinsci.plugins.docker.commons.tools.DockerTool;
6060
import org.kohsuke.accmod.Restricted;
6161
import org.kohsuke.accmod.restrictions.NoExternalUse;
@@ -106,7 +106,12 @@ public DockerClient(@Nonnull Launcher launcher, @CheckForNull Node node, @CheckF
106106
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 {
107107
ArgumentListBuilder argb = new ArgumentListBuilder();
108108

109-
argb.add("run", "-t", "-d", "-u", user);
109+
argb.add("run", "-t", "-d");
110+
111+
// Username might be empty because we are running on Windows
112+
if (StringUtils.isNotEmpty(user)) {
113+
argb.add("-u", user);
114+
}
110115
if (args != null) {
111116
argb.addTokenized(args);
112117
}
@@ -306,6 +311,10 @@ private LaunchResult launch(@CheckForNull @Nonnull EnvVars launchEnv, boolean qu
306311
* @return a {@link String} containing the <strong>uid:gid</strong>.
307312
*/
308313
public String whoAmI() throws IOException, InterruptedException {
314+
if (!launcher.isUnix()) {
315+
// Windows does not support username
316+
return "";
317+
}
309318
ByteArrayOutputStream userId = new ByteArrayOutputStream();
310319
launcher.launch().cmds("id", "-u").quiet(true).stdout(userId).start().joinWithTimeout(CLIENT_TIMEOUT, TimeUnit.SECONDS, launcher.getListener());
311320

@@ -367,6 +376,6 @@ public List<String> getVolumes(@Nonnull EnvVars launchEnv, String containerID) t
367376
if (volumes.isEmpty()) {
368377
return Collections.emptyList();
369378
}
370-
return Arrays.asList(volumes.split("\\n"));
379+
return Arrays.asList(volumes.replace("\\", "/").split("\\n"));
371380
}
372381
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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+
@Override
107+
public String whoAmI() throws IOException, InterruptedException {
108+
try (ByteArrayOutputStream userId = new ByteArrayOutputStream()) {
109+
launcher.launch().cmds("whoami").quiet(true).stdout(userId).start().joinWithTimeout(CLIENT_TIMEOUT, TimeUnit.SECONDS, launcher.getListener());
110+
return userId.toString(Charset.defaultCharset().name()).trim();
111+
}
112+
}
113+
114+
private LaunchResult launch(EnvVars env, boolean quiet, FilePath workDir, String... args) throws IOException, InterruptedException {
115+
return launch(env, quiet, workDir, new ArgumentListBuilder(args));
116+
}
117+
private LaunchResult launch(EnvVars env, boolean quiet, FilePath workDir, ArgumentListBuilder argb) throws IOException, InterruptedException {
118+
if (LOGGER.isLoggable(Level.FINE)) {
119+
LOGGER.log(Level.FINE, "Executing command \"{0}\"", argb);
120+
}
121+
122+
Launcher.ProcStarter procStarter = launcher.launch();
123+
if (workDir != null) {
124+
procStarter.pwd(workDir);
125+
}
126+
127+
LaunchResult result = new LaunchResult();
128+
ByteArrayOutputStream out = new ByteArrayOutputStream();
129+
ByteArrayOutputStream err = new ByteArrayOutputStream();
130+
result.setStatus(procStarter.quiet(quiet).cmds(argb).envs(env).stdout(out).stderr(err).start().joinWithTimeout(CLIENT_TIMEOUT, TimeUnit.SECONDS, launcher.getListener()));
131+
final String charsetName = Charset.defaultCharset().name();
132+
result.setOut(out.toString(charsetName));
133+
result.setErr(err.toString(charsetName));
134+
135+
return result;
136+
}
137+
}

0 commit comments

Comments
 (0)