Skip to content

Commit

Permalink
Merge pull request #184 from 3shapeAS/features/windows_slaves
Browse files Browse the repository at this point in the history
Features/windows agents with pathing issues fixed
  • Loading branch information
oleg-nenashev authored Oct 9, 2019
2 parents 19ebb47 + 6982db7 commit 6d01b80
Show file tree
Hide file tree
Showing 7 changed files with 291 additions and 60 deletions.
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());
}
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

1 comment on commit 6d01b80

@bluemonki
Copy link

Choose a reason for hiding this comment

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

This update seems to have broken containers on Windows, If I start a container like this:

def compileContainer = docker.image("${env.COMPILE_IMAGE_TAG}").run("--name ${compileContainerName} -v ${escapedWorkspace}:C:/work", "ping -t localhost")

My container starts. But when I come to stop it like this:

compileContainer.stop()

Then it fails, and the build log has a real mess of docker commands in it;

[2019-11-01T09:45:13.214Z] Completed Building (5.4 seconds)

[2019-11-01T09:45:13.674Z]

[2019-11-01T09:45:13.674Z] administrator@WIN-1966SFQ4FES C:\Jenkins\workspace_branches_ZEN-32525-Implement-CI>docker stop administrator@WIN-1966SFQ4FES C:\Jenkins\workspace_branches_ZEN-32525-Implement-CI run -d --name zen-32525-implement-ci_compiler_1572600576646 -v C:/Jenkins/workspace/_branches_ZEN-32525-Implement-CI:C:/work zen-32525-implement-ci_compiler ping -t localhost 1>docker

[2019-11-01T09:45:13.942Z] unknown shorthand flag: 'd' in -d

[2019-11-01T09:45:13.942Z] See 'docker stop --help'.

[2019-11-01T09:45:13.942Z]

[2019-11-01T09:45:13.942Z] administrator@WIN-1966SFQ4FES C:\Jenkins\workspace_branches_ZEN-32525-Implement-CI>46e5dae3b97f6e443203a7e3a9c176257362c1a32b9e1ed23a11846ff878a7e0 && docker rm -f administrator@WIN-1966SFQ4FES C:\Jenkins\workspace_branches_ZEN-32525-Implement-CI run -d --name zen-32525-implement-ci_compiler_1572600576646 -v C:/Jenkins/workspace/_branches_ZEN-32525-Implement-CI:C:/work zen-32525-implement-ci_compiler ping -t localhost 1>docker

[2019-11-01T09:45:13.942Z] '46e5dae3b97f6e443203a7e3a9c176257362c1a32b9e1ed23a11846ff878a7e0' is not recognized as an internal or external command,

[2019-11-01T09:45:13.943Z] operable program or batch file.

[2019-11-01T09:45:13.943Z]

[2019-11-01T09:45:13.943Z] administrator@WIN-1966SFQ4FES C:\Jenkins\workspace_branches_ZEN-32525-Implement-CI>46e5dae3b97f6e443203a7e3a9c176257362c1a32b9e1ed23a11846ff878a7e0

[2019-11-01T09:45:13.943Z] '46e5dae3b97f6e443203a7e3a9c176257362c1a32b9e1ed23a11846ff878a7e0' is not recognized as an internal or external command,

[2019-11-01T09:45:13.943Z] operable program or batch file.

It's almost like the entire run command is taken to be the container id in the stop step?

This worked in the previous version

Please sign in to comment.