package com.nirima.jenkins.plugins.docker.launcher; import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.model.ExposedPort; import com.github.dockerjava.api.model.NetworkSettings; import com.github.dockerjava.api.model.PortBinding; import com.github.dockerjava.api.model.Ports; import com.nirima.jenkins.plugins.docker.DockerCloud; import com.nirima.jenkins.plugins.docker.DockerTemplate; import com.nirima.jenkins.plugins.docker.DockerTemplateBase; import com.nirima.jenkins.plugins.docker.utils.PortUtils; import hudson.Extension; import hudson.model.Descriptor; import hudson.model.ItemGroup; import hudson.plugins.sshslaves.SSHConnector; import hudson.plugins.sshslaves.SSHLauncher; import hudson.slaves.ComputerLauncher; import hudson.util.ListBoxModel; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import shaded.com.google.common.annotations.Beta; import shaded.com.google.common.base.Preconditions; import java.io.IOException; import java.net.InetSocketAddress; import java.net.URI; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; /** * Configurable SSH launcher that expected ssh port to be exposed from docker container. */ @Beta public class DockerComputerSSHLauncher extends DockerComputerLauncher { private static final Logger LOGGER = Logger.getLogger(DockerComputerSSHLauncher.class.getName()); // store real UI configuration protected final SSHConnector sshConnector; @DataBoundConstructor public DockerComputerSSHLauncher(SSHConnector sshConnector) { this.sshConnector = sshConnector; } public SSHConnector getSshConnector() { return sshConnector; } public ComputerLauncher getPreparedLauncher(String cloudId, DockerTemplate dockerTemplate, InspectContainerResponse inspect) { final DockerComputerSSHLauncher prepLauncher = new DockerComputerSSHLauncher(null); // don't care, we need only launcher prepLauncher.setLauncher(getSSHLauncher(cloudId, dockerTemplate, inspect)); return prepLauncher; } @Override public void appendContainerConfig(DockerTemplate dockerTemplate, CreateContainerCmd createCmd) { final int sshPort = getSshConnector().port; createCmd.withExposedPorts(new ExposedPort(sshConnector.port)); String[] cmd = dockerTemplate.getDockerTemplateBase().getDockerCommandArray(); if (cmd.length == 0) { //default value to preserve compatibility createCmd.withCmd("bash", "-c", "/usr/sbin/sshd -D -p " + sshPort); } createCmd.getPortBindings().add(PortBinding.parse( "" + sshPort)); } @Override public boolean waitUp(String cloudId, DockerTemplate dockerTemplate, InspectContainerResponse containerInspect) { super.waitUp(cloudId, dockerTemplate, containerInspect); final PortUtils.ConnectionCheck connectionCheck = PortUtils.connectionCheck(getAddressForSSHD(cloudId, containerInspect)) .withRetries(60) .withEveryRetryWaitFor(2, TimeUnit.SECONDS); return (connectionCheck.execute() && connectionCheck.useSSH().execute()); } private SSHLauncher getSSHLauncher(String cloudId, DockerTemplate template, InspectContainerResponse inspect) { Preconditions.checkNotNull(template); Preconditions.checkNotNull(inspect); try { final InetSocketAddress address = getAddressForSSHD(cloudId, inspect); LOGGER.log(Level.INFO, "Creating slave SSH launcher for " + address); return new SSHLauncher(address.getHostString(), address.getPort(), sshConnector.getCredentials(), sshConnector.jvmOptions, sshConnector.javaPath, sshConnector.prefixStartSlaveCmd, sshConnector.suffixStartSlaveCmd, sshConnector.launchTimeoutSeconds); } catch (NullPointerException ex) { throw new RuntimeException("No mapped port 22 in host for SSL. Config=" + inspect, ex); } } /** * Given an inspected container, work out where we talk to the SSH daemon. * * I.E: this is usually the port that has been exposed on the container (22) and mapped * to some value on the container host. * * This might - in the case where Jenkins itself is running on the same cloud - be * a direct connection. * */ protected InetSocketAddress getAddressForSSHD(String cloudId, InspectContainerResponse ir) { // get exposed port ExposedPort sshPort = new ExposedPort(sshConnector.port); String host = null; Integer port = 22; final NetworkSettings networkSettings = ir.getNetworkSettings(); final Ports ports = networkSettings.getPorts(); final Map<ExposedPort, Ports.Binding[]> bindings = ports.getBindings(); // Get the binding that goes to the port that we're interested in (e.g: 22) final Ports.Binding[] sshBindings = bindings.get(sshPort); // Find where it's mapped to for (Ports.Binding b : sshBindings) { // TODO: I don't really follow why docker-java here is capable of // returning a range - surely an exposed port cannot be bound to a *range*? String hps = b.getHostPortSpec(); port = Integer.valueOf(hps); host = b.getHostIp(); } //get address, if docker on localhost, then use local? if (host == null || host.equals("0.0.0.0")) { String url = DockerCloud.getCloudByName(cloudId).getServerUrl(); host = getDockerHostFromCloud(cloudId); if( url.startsWith("unix") && (host == null || host.trim().isEmpty()) ) { // Communicating with local sockets. host = "0.0.0.0"; } else { /* Don't use IP from DOCKER_HOST because it is invalid or we are * connecting to a system that supports a single host abstraction * like Joyent's Triton. */ if (host == null || host.equals("0.0.0.0") || usesSingleHostAbstraction(ir)) { // Try to connect to the container directly (without going through the host) host = networkSettings.getIpAddress(); port = sshConnector.port; } } } return new InetSocketAddress(host, port); } private String getDockerHostFromCloud(String cloudId) { String url; String host; DockerCloud cloud = DockerCloud.getCloudByName(cloudId); url = cloud.getServerUrl(); String dockerHostname = cloud.getDockerHostname(); if (dockerHostname != null && !dockerHostname.trim().isEmpty()) { return dockerHostname; } else { return URI.create(url).getHost(); } } @Override public Descriptor<ComputerLauncher> getDescriptor() { return DESCRIPTOR; } /** * <p>Checks a {@link InspectContainerResponse} object to see if the server * uses a single host abstraction model. If it does, then we return * true.</p> * * <p>A Docker single host abstraction is when an entire fleet of * provisional resources are presented as if they reside on a single host, * but in fact they do not. This allows a user to elastically provision as * many resources as they want without having to worry about filling up a * single host system.</p> * * @param inspect response from Docker API * @return true if the server API supports a single host abstraction */ protected static boolean usesSingleHostAbstraction( InspectContainerResponse inspect) { Preconditions.checkNotNull(inspect); /* Fill this in with more robust logic when more servers supporting * this model become available (e.g. VMWare's project Bonneville and * Microsoft's offering). */ return inspect.getDriver().equals("sdc"); } @Restricted(NoExternalUse.class) public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); @Extension public static final class DescriptorImpl extends Descriptor<ComputerLauncher> { public ListBoxModel doFillCredentialsIdItems(@AncestorInPath ItemGroup context) { return DockerTemplateBase.DescriptorImpl.doFillCredentialsIdItems(context); } public Class getSshConnectorClass() { return SSHConnector.class; } @Override public String getDisplayName() { return "Docker SSH computer launcher"; } } }