package com.nirima.jenkins.plugins.docker.launcher;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.exception.DockerException;
import com.github.dockerjava.api.command.CreateContainerCmd;
import com.github.dockerjava.api.command.ExecCreateCmdResponse;
import com.github.dockerjava.api.command.InspectContainerResponse;
import com.github.dockerjava.api.exception.NotFoundException;
import com.github.dockerjava.core.command.ExecStartResultCallback;
import com.nirima.jenkins.plugins.docker.DockerComputer;
import com.nirima.jenkins.plugins.docker.DockerTemplate;
import hudson.Extension;
import hudson.model.Descriptor;
import hudson.model.TaskListener;
import hudson.slaves.ComputerLauncher;
import hudson.slaves.JNLPLauncher;
import hudson.slaves.SlaveComputer;
import hudson.util.TimeUnit2;
import jenkins.model.Jenkins;
import org.apache.commons.io.Charsets;
import org.apache.commons.io.IOUtils;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import shaded.com.google.common.annotations.Beta;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
/**
* JNLP launcher. Doesn't require open ports on docker host.
* <p/>
* Steps:
* - runs container with nop command
* - as launch action executes jnlp connection to master
*
* @author Kanstantsin Shautsou
*/
@Beta
public class DockerComputerJNLPLauncher extends DockerComputerLauncher {
private static final Logger LOGGER = LoggerFactory.getLogger(DockerComputerJNLPLauncher.class);
/**
* Configured from UI
*/
protected JNLPLauncher jnlpLauncher;
protected long launchTimeout = 120; //seconds
protected String user;
@DataBoundConstructor
public DockerComputerJNLPLauncher(JNLPLauncher jnlpLauncher) {
this.jnlpLauncher = jnlpLauncher;
}
public JNLPLauncher getJnlpLauncher() {
return jnlpLauncher;
}
@DataBoundSetter
public void setUser(String user) {
this.user = user;
}
public String getUser() {
return user;
}
@Override
public boolean isLaunchSupported() {
return true;
}
@Override
public void launch(SlaveComputer computer, TaskListener listener) throws IOException, InterruptedException {
final PrintStream logger = listener.getLogger();
final DockerComputer dockerComputer = (DockerComputer) computer;
final String containerId = dockerComputer.getContainerId();
final String rootUrl = Jenkins.getInstance().getRootUrl();
final DockerClient connect = dockerComputer.getCloud().getClient();
final DockerTemplate dockerTemplate = dockerComputer.getNode().getDockerTemplate();
// exec jnlp connection in running container
// TODO implement PID 1 replacement
// String cdCmd = "cd " + dockerTemplate.getRemoteFs();
// String wgetSlaveCmd = "wget " + rootUrl + "jnlpJars/slave.jar -O slave.jar";
// String jnlpConnectCmd = "java -jar slave.jar "
// + "-jnlpUrl " + rootUrl + dockerComputer.getUrl() + "slave-agent.jnlp ";
//// + "-secret " + dockerComputer.getJnlpMac();
//
// String[] connectCmd = {
// "bash", "-c", cdCmd + " && " + wgetSlaveCmd + " && " + jnlpConnectCmd
// };
String startCmd =
"cat << EOF > /tmp/config.sh.tmp && cd /tmp && mv config.sh.tmp config.sh\n" +
"JENKINS_URL=\"" + rootUrl + "\"\n" +
"JENKINS_USER=\"" + getUser() + "\"\n" +
"JENKINS_HOME=\"" + dockerTemplate.getRemoteFs() + "\"\n" +
"COMPUTER_URL=\"" + dockerComputer.getUrl() + "\"\n" +
"COMPUTER_SECRET=\"" + dockerComputer.getJnlpMac() + "\"\n" +
"EOF" + "\n";
try {
// LOGGER.info("Creating jnlp connection command '{}' for '{}'", Arrays.toString(connectCmd), containerId);
// logger.println("Creating jnlp connection command '" + Arrays.toString(connectCmd) + "' for '" + containerId + "'");
final ExecCreateCmdResponse response = connect.execCreateCmd(containerId)
.withTty(true)
.withAttachStdin(true)
.withAttachStderr(true)
.withAttachStdout(true)
// .withCmd(connectCmd)
.withCmd("/bin/bash", "-cxe", startCmd.replace("$", "\\$"))
.exec();
LOGGER.info("Starting connection command for {}", containerId);
logger.println("Starting connection command for " + containerId);
try {
connect.execStartCmd(response.getId())
.withDetach(true)
.withTty(true)
.exec( new ExecStartResultCallback(null,null));
} catch (NotFoundException ex) {
listener.error("Can't execute command: " + ex.getMessage());
LOGGER.error("Can't execute jnlp connection command: '{}'", ex.getMessage());
}
} catch (Exception ex) {
listener.error("Can't execute command: " + ex.getMessage());
LOGGER.error("Can't execute jnlp connection command: '{}'", ex.getMessage());
throw ex;
}
LOGGER.info("Successfully executed jnlp connection for '{}'", containerId);
logger.println("Successfully executed jnlp connection for " + containerId);
final long launchTime = System.currentTimeMillis();
while (!dockerComputer.isOnline() &&
TimeUnit2.SECONDS.toMillis(launchTimeout) > System.currentTimeMillis() - launchTime) {
logger.println("Waiting slave connection...");
Thread.sleep(1000);
}
if (!dockerComputer.isOnline()) {
LOGGER.info("Launch timeout, termintaing slave based on '{}'", containerId);
logger.println("Launch timeout, termintaing slave.");
dockerComputer.getNode().terminate();
throw new IOException("Can't connect slave to jenkins");
}
LOGGER.info("Launched slave '{}' based on '{}'", dockerComputer.getName(), containerId);
logger.println("Launched slave for " + containerId);
}
@Override
public ComputerLauncher getPreparedLauncher(String cloudId, DockerTemplate template, InspectContainerResponse containerInspectResponse) {
DockerComputerJNLPLauncher dockerComputerJNLPLauncher = new DockerComputerJNLPLauncher(getJnlpLauncher());
DockerComputerJNLPLauncher launcher = (DockerComputerJNLPLauncher) template.getLauncher();
dockerComputerJNLPLauncher.setUser(launcher.getUser());
return dockerComputerJNLPLauncher;
}
@Override
public void appendContainerConfig(DockerTemplate dockerTemplate, CreateContainerCmd createContainerCmd) throws IOException {
try (InputStream istream = DockerComputerJNLPLauncher.class.getResourceAsStream("DockerComputerJNLPLauncher/init.sh")) {
final String initCmd = IOUtils.toString(istream, Charsets.UTF_8);
if (initCmd == null) {
throw new IllegalStateException("Resource file 'init.sh' not found");
}
// createContainerCmd.withCmd("/bin/sh"); // nop
// wait for params
createContainerCmd.withCmd("/bin/bash",
"-cxe",
"cat << EOF >> /tmp/init.sh && chmod +x /tmp/init.sh && exec /tmp/init.sh\n" +
initCmd.replace("$", "\\$") + "\n" +
"EOF" + "\n"
);
}
// final String homeDir = dockerTemplate.getRemoteFs();
// if (isNotBlank(homeDir)) {
// createContainerCmd.withWorkingDir(homeDir);
// }
//
// final String user = dockerTemplate.getUser();
// if (isNotBlank(user)) {
// createContainerCmd.withUser(user);
// }
createContainerCmd.withTty(true);
createContainerCmd.withStdinOpen(true);
}
@Override
public boolean waitUp(String cloudId, DockerTemplate dockerTemplate, InspectContainerResponse ir) {
return super.waitUp(cloudId, dockerTemplate, ir);
}
@Override
public Descriptor<ComputerLauncher> getDescriptor() {
return DESCRIPTOR;
}
@Restricted(NoExternalUse.class)
public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
@Extension
public static class DescriptorImpl extends Descriptor<ComputerLauncher> {
public Class getJNLPLauncher() {
return JNLPLauncher.class;
}
@Override
public String getDisplayName() {
return "(Experimental) Docker JNLP launcher";
}
}
}