Skip to content

Commit

Permalink
[JENKINS-29760] Docker in Docker support (and other similar advanced …
Browse files Browse the repository at this point in the history
…use cases)
  • Loading branch information
ndeloof committed Aug 11, 2015
1 parent 6dc62b7 commit aaebd9f
Show file tree
Hide file tree
Showing 7 changed files with 61 additions and 16 deletions.
Expand Up @@ -28,6 +28,7 @@ public class BuiltInContainer implements BuildBadgeAction, EnvironmentContributi
/* package */ String image;

/* package */ transient String container;

private transient boolean enable;
private final transient Docker docker;
private List<Integer> ports = new ArrayList<Integer>();
Expand Down
Expand Up @@ -146,15 +146,14 @@ public void kill(String container) throws IOException, InterruptedException {
throw new RuntimeException("Failed to remove docker container "+container);
}

public String runDetached(String image, String workdir, Map<String, String> volumes, Map<Integer, Integer> ports, Map<String, String> links, EnvVars environment, String user, String... command) throws IOException, InterruptedException {
public String runDetached(String image, String workdir, Map<String, String> volumes, Map<Integer, Integer> ports, Map<String, String> links, EnvVars environment, String... command) throws IOException, InterruptedException {

ArgumentListBuilder args = dockerCommand()
.add("run", "--tty", "--detach");
if (privileged) {
args.add( "--privileged");
}
args.add("--user", user)
.add("--workdir", workdir);
args.add("--workdir", workdir);
for (Map.Entry<String, String> volume : volumes.entrySet()) {
args.add("--volume", volume.getKey() + ":" + volume.getValue() + ":rw" );
}
Expand All @@ -164,13 +163,15 @@ public String runDetached(String image, String workdir, Map<String, String> volu
for (Map.Entry<String, String> link : links.entrySet()) {
args.add("--link", link.getKey() + ":" + link.getValue());
}
/*
for (Map.Entry<String, String> e : environment.entrySet()) {
if ("HOSTNAME".equals(e.getKey())) {
continue;
}
args.add("--env");
args.addMasked(e.getKey()+"="+e.getValue());
}
*/
args.add(image).add(command);

ByteArrayOutputStream out = new ByteArrayOutputStream();
Expand All @@ -186,11 +187,13 @@ public String runDetached(String image, String workdir, Map<String, String> volu
return out.toString("UTF-8").trim();
}

public void executeIn(String container, Launcher.ProcStarter starter) throws IOException, InterruptedException {
public void executeIn(String container, String userId, Launcher.ProcStarter starter) throws IOException, InterruptedException {
List<String> originalCmds = starter.cmds();

ArgumentListBuilder args = dockerCommand()
.add("exec", "--tty", container);
.add("exec", "--tty")
.add("--user", userId)
.add(container);

boolean[] originalMask = starter.masks();
for (int i = 0; i < originalCmds.size(); i++) {
Expand Down
Expand Up @@ -50,17 +50,22 @@ public class DockerBuildWrapper extends BuildWrapper {

private final boolean privileged;

private String group;

private String command;

@DataBoundConstructor
public DockerBuildWrapper(DockerImageSelector selector, String dockerInstallation, DockerServerEndpoint dockerHost, String dockerRegistryCredentials, boolean verbose, boolean privileged,
List<Volume> volumes) {
List<Volume> volumes, String group, String command) {
this.selector = selector;
this.dockerInstallation = dockerInstallation;
this.dockerHost = dockerHost;
this.dockerRegistryCredentials = dockerRegistryCredentials;
this.verbose = verbose;
this.privileged = privileged;
this.volumes = volumes;
this.group = group;
this.command = command;
}

public DockerImageSelector getSelector() {
Expand Down Expand Up @@ -91,13 +96,21 @@ public List<Volume> getVolumes() {
return volumes;
}

public String getGroup() {
return group;
}

public String getCommand() {
return command;
}

@Override
public Launcher decorateLauncher(final AbstractBuild build, final Launcher launcher, final BuildListener listener) throws IOException, InterruptedException, Run.RunnerAbortedException {
final Docker docker = new Docker(dockerHost, dockerInstallation, dockerRegistryCredentials, build, launcher, listener, verbose, privileged);
final BuiltInContainer runInContainer = new BuiltInContainer(docker);
build.addAction(runInContainer);

DockerDecoratedLauncher decorated = new DockerDecoratedLauncher(selector, launcher, runInContainer, build);
DockerDecoratedLauncher decorated = new DockerDecoratedLauncher(selector, launcher, runInContainer, build, whoAmI(launcher));
return decorated;
}

Expand Down Expand Up @@ -132,7 +145,7 @@ public Environment setUp(AbstractBuild build, final Launcher launcher, BuildList
}
}

runInContainer.container = startBuildContainer(runInContainer, build, listener, whoAmI(launcher));
runInContainer.container = startBuildContainer(runInContainer, build, listener);
listener.getLogger().println("Docker container " + runInContainer.container + " started to host the build");
}

Expand All @@ -149,7 +162,7 @@ public boolean tearDown(AbstractBuild build, BuildListener listener) throws IOEx



private String startBuildContainer(BuiltInContainer runInContainer, AbstractBuild build, BuildListener listener, String userId) throws IOException {
private String startBuildContainer(BuiltInContainer runInContainer, AbstractBuild build, BuildListener listener) throws IOException {
try {
EnvVars environment = build.getEnvironment(listener);

Expand All @@ -158,8 +171,8 @@ private String startBuildContainer(BuiltInContainer runInContainer, AbstractBuil
Map<String, String> links = new HashMap<String, String>();

return runInContainer.getDocker().runDetached(runInContainer.image, workdir,
runInContainer.getVolumes(build), runInContainer.getPortsMap(), links, environment, userId,
"/bin/cat"); // Command expected to hung until killed
runInContainer.getVolumes(build), runInContainer.getPortsMap(), links, environment,
command.split(" ")); // Command expected to hung until killed

} catch (InterruptedException e) {
throw new RuntimeException("Interrupted");
Expand All @@ -169,10 +182,15 @@ private String startBuildContainer(BuiltInContainer runInContainer, AbstractBuil
private String whoAmI(Launcher launcher) throws IOException, InterruptedException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
launcher.launch().cmds("id", "-u").stdout(bos).quiet(true).join();
String uid = bos.toString().trim();

ByteArrayOutputStream bos2 = new ByteArrayOutputStream();
launcher.launch().cmds("id", "-g").stdout(bos2).quiet(true).join();
return bos.toString().trim()+":"+bos2.toString().trim();
String gid = group;
if (group == null) {
ByteArrayOutputStream bos2 = new ByteArrayOutputStream();
launcher.launch().cmds("id", "-g").stdout(bos2).quiet(true).join();
gid = bos2.toString().trim();
}
return uid+":"+gid;

}

Expand Down Expand Up @@ -228,6 +246,7 @@ private Object readResolve() {
if (exposeDocker) {
this.volumes.add(new Volume("/var/run/docker.sock","/var/run/docker.sock"));
}
if (command == null) command = "/bin/cat";
return this;
}
}
Expand Up @@ -19,12 +19,15 @@ public class DockerDecoratedLauncher extends Launcher.DecoratedLauncher {
private final DockerImageSelector selector;
private final BuiltInContainer runInContainer;
private final AbstractBuild build;
private final String userId;

public DockerDecoratedLauncher(DockerImageSelector selector, Launcher launcher, BuiltInContainer runInContainer, AbstractBuild build) throws IOException, InterruptedException {
public DockerDecoratedLauncher(DockerImageSelector selector, Launcher launcher, BuiltInContainer runInContainer,
AbstractBuild build, String userId) throws IOException, InterruptedException {
super(launcher);
this.selector = selector;
this.runInContainer = runInContainer;
this.build = build;
this.userId = userId;
}

public Proc launch(String[] cmd, boolean[] mask, String[] env, InputStream in, OutputStream out, FilePath workDir) throws IOException {
Expand All @@ -38,7 +41,7 @@ public Proc launch(ProcStarter starter) throws IOException {
if (!runInContainer.isEnabled()) return super.launch(starter);

try {
runInContainer.getDocker().executeIn(runInContainer.container, starter);
runInContainer.getDocker().executeIn(runInContainer.container, userId, starter);
} catch (InterruptedException e) {
throw new IOException("Caught InterruptedException", e);
}
Expand Down
Expand Up @@ -52,6 +52,12 @@ THE SOFTWARE.
<f:entry field="verbose" title="Verbose">
<f:checkbox/>
</f:entry>
<f:entry field="group" title="User group">
<f:textbox/>
</f:entry>
<f:entry field="command" title="Container start command">
<f:textbox default="/bin/cat"/>
</f:entry>
</f:advanced>

</f:nested>
Expand Down
@@ -0,0 +1,6 @@
This advanced option let you configure the command to be ran when the container is started. This command
<b>must</b> hang so the container is alive, while build steps are executed using <code>docker exec</code>.
<p>
You should not change this option until you have some advanced requirements. For sample using
<a href="https://github.com/jpetazzo/dind">Docker in Docker</a> will require to configure command as
<code>wrapdocker /bin/cat</code>.
@@ -0,0 +1,7 @@
The user to run build <em>has</em> to be the same as the Jenkins slave user so files created in workspace
have adequate owner and permission set. Side effect is you can't configure the user to run inside docker
(until Docker do support <a href="https://github.com/docker/docker/issues/7906">user namespace</a>)
<p>
For many usages this restriction can be frustrating, so as a workaround you can configure user's group ID
to gain extra privileges in your container. For sample, if you want to run
<a href="https://github.com/jpetazzo/dind">Docker in Docker</a>, you can set group as '<code>docker</code>'.

0 comments on commit aaebd9f

Please sign in to comment.