/*
* The MIT License
*
* Copyright (c) 2015, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
package it.dockins.dockerslaves.drivers;
import hudson.Launcher;
import hudson.Proc;
import hudson.model.Slave;
import hudson.model.TaskListener;
import hudson.org.apache.tools.tar.TarOutputStream;
import hudson.slaves.CommandLauncher;
import hudson.slaves.SlaveComputer;
import hudson.util.ArgumentListBuilder;
import hudson.util.VersionNumber;
import it.dockins.dockerslaves.Container;
import it.dockins.dockerslaves.DockerComputer;
import it.dockins.dockerslaves.DockerSlave;
import it.dockins.dockerslaves.ProvisionQueueListener;
import it.dockins.dockerslaves.hints.MemoryHint;
import it.dockins.dockerslaves.hints.VolumeHint;
import it.dockins.dockerslaves.spec.Hint;
import it.dockins.dockerslaves.spi.DockerDriver;
import it.dockins.dockerslaves.spi.DockerHostConfig;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.tools.tar.TarEntry;
import org.apache.tools.tar.TarInputStream;
import org.jenkinsci.plugins.docker.commons.credentials.DockerServerEndpoint;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import static it.dockins.dockerslaves.DockerSlave.SLAVE_ROOT;
/**
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
* @author <a href="mailto:yoann.dubreuil@gmail.com">Yoann Dubreuil</a>
*/
public class CliDockerDriver extends DockerDriver {
private final static boolean verbose = Boolean.getBoolean(DockerDriver.class.getName()+".verbose");;
private final DockerHostConfig dockerHost;
private final VersionNumber version;
public CliDockerDriver(DockerHostConfig dockerHost) throws IOException, InterruptedException {
this.dockerHost = dockerHost;
// Also acts as sanity check to ensure host and credentials are well set
version = new VersionNumber(serverVersion(TaskListener.NULL));
}
@Override
public void close() throws IOException {
dockerHost.close();
}
@Override
public String createVolume(TaskListener listener) throws IOException, InterruptedException {
ArgumentListBuilder args = new ArgumentListBuilder()
.add("volume", "create");
ByteArrayOutputStream out = new ByteArrayOutputStream();
Launcher launcher = new Launcher.LocalLauncher(listener);
int status = launchDockerCLI(launcher, args)
.stdout(out).stderr(launcher.getListener().getLogger()).join();
final String volume = out.toString(UTF_8).trim();
if (status != 0) {
throw new IOException("Failed to create docker volume");
}
return volume;
}
@Override
public boolean hasVolume(TaskListener listener, String name) throws IOException, InterruptedException {
if (StringUtils.isEmpty(name)) {
return false;
}
ArgumentListBuilder args = new ArgumentListBuilder()
.add("volume", "inspect", "-f", "'{{.Name}}'", name);
ByteArrayOutputStream out = new ByteArrayOutputStream();
Launcher launcher = new Launcher.LocalLauncher(listener);
int status = launchDockerCLI(launcher, args)
.stdout(out).stderr(launcher.getListener().getLogger()).join();
if (status != 0) {
return false;
} else {
return true;
}
}
@Override
public boolean hasContainer(TaskListener listener, String id) throws IOException, InterruptedException {
if (StringUtils.isEmpty(id)) {
return false;
}
ArgumentListBuilder args = new ArgumentListBuilder()
.add("inspect", "-f", "'{{.Id}}'", id);
ByteArrayOutputStream out = new ByteArrayOutputStream();
Launcher launcher = new Launcher.LocalLauncher(listener);
int status = launchDockerCLI(launcher, args)
.stdout(out).stderr(launcher.getListener().getLogger()).join();
if (status != 0) {
return false;
} else {
return true;
}
}
@Override
public Container launchRemotingContainer(TaskListener listener, String image, String volume, DockerComputer computer) throws IOException, InterruptedException {
// Create a container for remoting
ArgumentListBuilder args = new ArgumentListBuilder()
.add("create", "--interactive")
// We disable container logging to sdout as we rely on this one as transport for jenkins remoting
.add("--log-driver=none")
.add("--env", "TMPDIR="+ SLAVE_ROOT+".tmp")
.add("--user", "10000:10000")
.add("--volume", volume+":"+ SLAVE_ROOT)
.add(image)
.add("java")
// set TMP directory within the /home/jenkins/ volume so it can be shared with other containers
.add("-Djava.io.tmpdir="+ SLAVE_ROOT+".tmp")
.add("-jar").add(SLAVE_ROOT+"slave.jar");
ByteArrayOutputStream out = new ByteArrayOutputStream();
Launcher launcher = new Launcher.LocalLauncher(listener);
int status = launchDockerCLI(launcher, args)
.stdout(out).stderr(launcher.getListener().getLogger()).join();
String containerId = out.toString(UTF_8).trim();
if (status != 0) {
throw new IOException("Failed to create docker image");
}
// Inject current slave.jar to ensure adequate version running
putFileContent(launcher, containerId, DockerSlave.SLAVE_ROOT, "slave.jar", new Slave.JnlpJar("slave.jar").readFully());
Container remotingContainer = new Container(image, containerId);
// Run container in interactive mode to establish channel over stdin/stdout
args = new ArgumentListBuilder()
.add("start")
.add("--interactive", "--attach", remotingContainer.getId());
prependArgs(args);
new CommandLauncher(args.toString(), dockerHost.getEnvironment()).launch(computer, listener);
return remotingContainer;
}
@Override
public Container launchBuildContainer(TaskListener listener, String image, Container remotingContainer, List<Hint> hints) throws IOException, InterruptedException {
Container buildContainer = new Container(image);
ArgumentListBuilder args = new ArgumentListBuilder()
.add("create")
.add("--env", "TMPDIR="+SLAVE_ROOT+".tmp")
.add("--workdir", SLAVE_ROOT)
.add("--volumes-from", remotingContainer.getId())
.add("--net=container:" + remotingContainer.getId())
.add("--ipc=container:" + remotingContainer.getId())
.add("--user", "10000:10000");
applyHints(hints, args);
args.add(buildContainer.getImageName());
args.add("/trampoline", "wait");
ByteArrayOutputStream out = new ByteArrayOutputStream();
Launcher launcher = new Launcher.LocalLauncher(listener);
int status = launchDockerCLI(launcher, args)
.stdout(out).stderr(launcher.getListener().getLogger()).join();
final String containerId = out.toString(UTF_8).trim();
buildContainer.setId(containerId);
if (status != 0) {
throw new IOException("Failed to run docker image");
}
injectJenkinsUnixGroup(launcher, containerId);
injectJenkinsUnixUser(launcher, containerId);
injectTrampoline(launcher, containerId);
status = launchDockerCLI(launcher, new ArgumentListBuilder()
.add("start", containerId)).stdout(out).stderr(launcher.getListener().getLogger()).join();
if (status != 0) {
throw new IOException("Failed to run docker image");
}
return buildContainer;
}
private void applyHints(List<Hint> hints, ArgumentListBuilder args) {
if (hints == null) return;
for (Hint hint : hints) {
if (hint instanceof MemoryHint) {
args.add("-m", ((MemoryHint) hint).getMemory());
} else if (hint instanceof VolumeHint) {
args.add("-v", ((VolumeHint) hint).getVolume());
} else {
// unsupported hint, just ignored
}
}
}
protected void injectJenkinsUnixGroup(Launcher launcher, String containerId) throws IOException, InterruptedException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
getFileContent(launcher, containerId, "/etc/group", out);
out.write("jenkins:x:10000:\n".getBytes(StandardCharsets.UTF_8));
putFileContent(launcher, containerId, "/etc", "group", out.toByteArray());
}
protected void injectJenkinsUnixUser(Launcher launcher, String containerId) throws IOException, InterruptedException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
getFileContent(launcher, containerId, "/etc/passwd", out);
out.write("jenkins:x:10000:10000::/home/jenkins:/bin/false\n".getBytes(StandardCharsets.UTF_8));
putFileContent(launcher, containerId, "/etc", "passwd", out.toByteArray());
}
protected void injectTrampoline(Launcher launcher, String containerId) throws IOException, InterruptedException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
IOUtils.copy(getClass().getResourceAsStream("/it/dockins/dockerslaves/trampoline"),out);
putFileContent(launcher, containerId, "/", "trampoline", out.toByteArray(), 555);
}
protected void getFileContent(Launcher launcher, String containerId, String filename, OutputStream outputStream) throws IOException, InterruptedException {
ArgumentListBuilder args = new ArgumentListBuilder()
.add("cp", containerId + ":" + filename, "-");
ByteArrayOutputStream out = new ByteArrayOutputStream();
int status = launchDockerCLI(launcher, args)
.stdout(out).stderr(launcher.getListener().getLogger()).join();
if (status != 0) {
throw new IOException("Failed to get file");
}
TarInputStream tar = new TarInputStream(new ByteArrayInputStream(out.toByteArray()));
tar.getNextEntry();
tar.copyEntryContents(outputStream);
tar.close();
}
protected int putFileContent(Launcher launcher, String containerId, String path, String filename, byte[] content) throws IOException, InterruptedException {
return putFileContent(launcher, containerId, path, filename, content, null);
}
protected int putFileContent(Launcher launcher, String containerId, String path, String filename, byte[] content, Integer mode) throws IOException, InterruptedException {
TarEntry entry = new TarEntry(filename);
entry.setUserId(0);
entry.setGroupId(0);
entry.setSize(content.length);
if (mode != null) {
entry.setMode(mode);
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
TarOutputStream tar = new TarOutputStream(out);
tar.putNextEntry(entry);
tar.write(content);
tar.closeEntry();
tar.close();
ArgumentListBuilder args = new ArgumentListBuilder()
.add("cp", "-", containerId + ":" + path);
return launchDockerCLI(launcher, args)
.stdin(new ByteArrayInputStream(out.toByteArray()))
.stderr(launcher.getListener().getLogger()).join();
}
@Override
public Proc execInContainer(TaskListener listener, String containerId, Launcher.ProcStarter starter) throws IOException, InterruptedException {
ArgumentListBuilder args = new ArgumentListBuilder()
.add("exec", containerId);
if (starter.pwd() != null) {
args.add("/trampoline", "cdexec", starter.pwd().getRemote());
}
args.add("env").add(starter.envs());
List<String> originalCmds = starter.cmds();
boolean[] originalMask = starter.masks();
for (int i = 0; i < originalCmds.size(); i++) {
boolean masked = originalMask == null ? false : i < originalMask.length ? originalMask[i] : false;
args.add(originalCmds.get(i), masked);
}
Launcher launcher = new Launcher.LocalLauncher(listener);
Launcher.ProcStarter procStarter = launchDockerCLI(launcher, args);
if (starter.stdout() != null) {
procStarter.stdout(starter.stdout());
}
return procStarter.start();
}
@Override
public void removeContainer(TaskListener listener, Container instance) throws IOException, InterruptedException {
ArgumentListBuilder args = new ArgumentListBuilder()
.add("rm", "-f", instance.getId());
ByteArrayOutputStream out = new ByteArrayOutputStream();
Launcher launcher = new Launcher.LocalLauncher(listener);
int status = launchDockerCLI(launcher, args)
.stdout(out).stderr(launcher.getListener().getLogger()).join();
if (status != 0) {
throw new IOException("Failed to remove container " + instance.getId());
}
}
private static final Logger LOGGER = Logger.getLogger(ProvisionQueueListener.class.getName());
@Override
public Container launchSideContainer(TaskListener listener, String image, Container remotingContainer, List<Hint> hints) throws IOException, InterruptedException {
ArgumentListBuilder args = new ArgumentListBuilder()
.add("create")
.add("--volumes-from", remotingContainer.getId())
.add("--net=container:" + remotingContainer.getId())
.add("--ipc=container:" + remotingContainer.getId());
applyHints(hints, args);
args.add(image);
ByteArrayOutputStream out = new ByteArrayOutputStream();
Launcher launcher = new Launcher.LocalLauncher(listener);
int status = launchDockerCLI(launcher, args)
.stdout(out).stderr(launcher.getListener().getLogger()).join();
final String containerId = out.toString(UTF_8).trim();
if (status != 0) {
throw new IOException("Failed to run docker image");
}
launchDockerCLI(launcher, new ArgumentListBuilder()
.add("start", containerId)).start();
return new Container(image, containerId);
}
@Override
public void pullImage(TaskListener listener, String image) throws IOException, InterruptedException {
ArgumentListBuilder args = new ArgumentListBuilder()
.add("pull")
.add(image);
Launcher launcher = new Launcher.LocalLauncher(listener);
int status = launchDockerCLI(launcher, args)
.stdout(launcher.getListener().getLogger()).join();
if (status != 0) {
throw new IOException("Failed to pull image " + image);
}
}
@Override
public boolean checkImageExists(TaskListener listener, String image) throws IOException, InterruptedException {
ArgumentListBuilder args = new ArgumentListBuilder()
.add("inspect")
.add("-f", "'{{.Id}}'")
.add(image);
ByteArrayOutputStream out = new ByteArrayOutputStream();
Launcher launcher = new Launcher.LocalLauncher(listener);
return launchDockerCLI(launcher, args)
.stdout(out).stderr(launcher.getListener().getLogger()).join() == 0;
}
@Override
public void buildDockerfile(TaskListener listener, String dockerfilePath, String tag, boolean pull) throws IOException, InterruptedException {
String pullOption = "--pull=";
if (pull) {
pullOption += "true";
} else {
pullOption += "false";
}
ArgumentListBuilder args = new ArgumentListBuilder()
.add("build")
.add(pullOption)
.add("-t", tag)
.add(dockerfilePath);
Launcher launcher = new Launcher.LocalLauncher(listener);
int status = launchDockerCLI(launcher, args)
.stdout(launcher.getListener().getLogger()).join();
if (status != 0) {
throw new IOException("Failed to build docker image from Dockerfile " + dockerfilePath);
}
}
@Override
public String serverVersion(TaskListener listener) throws IOException, InterruptedException {
ArgumentListBuilder args = new ArgumentListBuilder()
.add("version", "-f", "{{.Server.Version}}");
ByteArrayOutputStream out = new ByteArrayOutputStream();
Launcher launcher = new Launcher.LocalLauncher(listener);
int status = launchDockerCLI(launcher, args)
.stdout(out).stderr(launcher.getListener().getLogger()).join();
final String version = out.toString(UTF_8).trim();
if (status != 0) {
throw new IOException("Failed to connect to docker API");
}
return version;
}
VersionNumber SWARM = new VersionNumber("1.12");
VersionNumber INFO_FORMAT = new VersionNumber("1.13");
public boolean usesSwarmMode(TaskListener listener) throws IOException, InterruptedException {
if (version.isOlderThan(SWARM)) return false;
ArgumentListBuilder args = new ArgumentListBuilder()
.add("docker", "info");
if (!version.isOlderThan(INFO_FORMAT)) {
args.add("--format", "{{.Swarm}}");
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
Launcher launcher = new Launcher.LocalLauncher(listener);
int status = launchDockerCLI(launcher, args)
.stdout(out).stderr(launcher.getListener().getLogger()).join();
if (status != 0) {
throw new IOException("Failed to connect to docker API");
}
return out.toString(UTF_8).contains("Swarm: active");
}
public void prependArgs(ArgumentListBuilder args){
final DockerServerEndpoint endpoint = dockerHost.getEndpoint();
if (endpoint.getUri() != null) {
args.prepend("-H", endpoint.getUri());
} else {
LOGGER.log(Level.FINE, "no specified docker host");
}
args.prepend("docker");
}
private Launcher.ProcStarter launchDockerCLI(Launcher launcher, ArgumentListBuilder args) {
prependArgs(args);
return launcher.launch()
.envs(dockerHost.getEnvironment())
.cmds(args)
.quiet(!verbose);
}
public static final String UTF_8 = StandardCharsets.UTF_8.name();
}