package com.nirima.jenkins.plugins.docker;
import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsNameProvider;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.*;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.*;
import com.github.dockerjava.api.exception.DockerException;
import com.github.dockerjava.api.model.AuthConfig;
import com.github.dockerjava.api.model.Container;
import com.github.dockerjava.api.model.Image;
import com.github.dockerjava.api.model.Version;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.core.DockerClientConfig;
import com.github.dockerjava.core.NameParser;
import com.github.dockerjava.core.command.PullImageResultCallback;
import com.nirima.jenkins.plugins.docker.client.ClientBuilderForPlugin;
import com.nirima.jenkins.plugins.docker.client.ClientConfigBuilderForPlugin;
import com.nirima.jenkins.plugins.docker.client.DockerCmdExecConfig;
import com.nirima.jenkins.plugins.docker.client.DockerCmdExecConfigBuilderForPlugin;
import com.nirima.jenkins.plugins.docker.launcher.DockerComputerLauncher;
import com.nirima.jenkins.plugins.docker.utils.DockerDirectoryCredentials;
import com.nirima.jenkins.plugins.docker.utils.JenkinsUtils;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.model.*;
import hudson.security.ACL;
import hudson.slaves.Cloud;
import hudson.slaves.ComputerLauncher;
import hudson.slaves.NodeProvisioner;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import shaded.com.google.common.base.MoreObjects;
import shaded.com.google.common.base.Preconditions;
import shaded.com.google.common.base.Predicate;
import shaded.com.google.common.base.Throwables;
import shaded.com.google.common.collect.Iterables;
import javax.annotation.CheckForNull;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.Callable;
import static org.bouncycastle.crypto.tls.ConnectionEnd.client;
/**
* Docker Cloud configuration. Contains connection configuration,
* {@link DockerTemplate} contains configuration for running docker image.
*
* @author magnayn
*/
public class DockerCloud extends Cloud {
private static final Logger LOGGER = LoggerFactory.getLogger(DockerCloud.class);
private List<DockerTemplate> templates;
private transient HashMap<Long, DockerTemplate> jobTemplates;
private String serverUrl;
private int connectTimeout;
public final int readTimeout;
public final String version;
public final String credentialsId;
public final String dockerHostname;
private transient DockerClient connection;
/**
* Total max allowed number of containers
*/
private int containerCap = 100;
/**
* Is this cloud actually a swarm?
*/
private transient Boolean _isSwarm;
/**
* Is this cloud running Joyent Triton?
*/
private transient Boolean _isTriton;
/**
* Track the count per image name for images currently being
* provisioned, but not necessarily reported yet by docker.
*/
private static final HashMap<String, Integer> provisionedImages = new HashMap<>();
/**
* Indicate if docker host used to run container is exposed inside container as DOCKER_HOST environment variable
*/
private Boolean exposeDockerHost;
@Deprecated
public DockerCloud(String name,
List<? extends DockerTemplate> templates,
String serverUrl,
String containerCapStr,
int connectTimeout,
int readTimeout,
String credentialsId,
String version,
String dockerHostname) {
super(name);
Preconditions.checkNotNull(serverUrl);
this.version = version;
this.credentialsId = credentialsId;
this.serverUrl = sanitizeUrl(serverUrl);
this.connectTimeout = connectTimeout;
this.readTimeout = readTimeout;
this.dockerHostname = dockerHostname;
if (templates != null) {
this.templates = new ArrayList<>(templates);
} else {
this.templates = Collections.emptyList();
}
if (containerCapStr.equals("")) {
setContainerCap(Integer.MAX_VALUE);
} else {
setContainerCap(Integer.parseInt(containerCapStr));
}
}
@DataBoundConstructor
public DockerCloud(String name,
List<? extends DockerTemplate> templates,
String serverUrl,
int containerCap,
int connectTimeout,
int readTimeout,
String credentialsId,
String version,
String dockerHostname) {
super(name);
Preconditions.checkNotNull(serverUrl);
this.version = version;
this.credentialsId = credentialsId;
this.serverUrl = sanitizeUrl(serverUrl);
this.connectTimeout = connectTimeout;
this.readTimeout = readTimeout;
this.dockerHostname = dockerHostname;
if (templates != null) {
this.templates = new ArrayList<>(templates);
} else {
this.templates = Collections.emptyList();
}
setContainerCap(containerCap);
}
public int getConnectTimeout() {
return connectTimeout;
}
public String getServerUrl() {
return serverUrl;
}
public String getDockerHostname() {
return dockerHostname;
}
/**
* @deprecated use {@link #getContainerCap()}
*/
@Deprecated
public String getContainerCapStr() {
if (containerCap == Integer.MAX_VALUE) {
return "";
} else {
return String.valueOf(containerCap);
}
}
public int getContainerCap() {
return containerCap;
}
public void setContainerCap(int containerCap) {
this.containerCap = containerCap;
}
protected String sanitizeUrl(String url) {
if( url == null )
return null;
return url.replace("http:", "tcp:")
.replace("https:","tcp:");
}
/**
* Connects to Docker.
*
* @return Docker client.
*/
public synchronized DockerClient getClient() {
if (connection == null) {
final DockerClientConfig clientConfig = ClientConfigBuilderForPlugin.dockerClientConfig()
.forCloud(this)
.build();
final DockerCmdExecConfig execConfig = DockerCmdExecConfigBuilderForPlugin.builder()
.forCloud(this)
.build();
connection = ClientBuilderForPlugin.builder()
.withDockerClientConfig(clientConfig)
.withDockerCmdExecConfig(execConfig)
.build();
}
return connection;
}
/**
* Decrease the count of slaves being "provisioned".
*/
private void decrementAmiSlaveProvision(String ami) {
synchronized (provisionedImages) {
int currentProvisioning;
try {
currentProvisioning = provisionedImages.get(ami);
} catch (NullPointerException npe) {
return;
}
provisionedImages.put(ami, Math.max(currentProvisioning - 1, 0));
}
}
@Override
public synchronized Collection<NodeProvisioner.PlannedNode> provision(Label label, int excessWorkload) {
try {
LOGGER.info("Asked to provision {} slave(s) for: {}", new Object[]{excessWorkload, label});
List<NodeProvisioner.PlannedNode> r = new ArrayList<>();
final List<DockerTemplate> templates = getTemplates(label);
while (excessWorkload > 0 && !templates.isEmpty()) {
final DockerTemplate t = templates.get(0); // get first
LOGGER.info("Will provision '{}', for label: '{}', in cloud: '{}'",
t.getDockerTemplateBase().getImage(), label, getDisplayName());
try {
if (!addProvisionedSlave(t)) {
templates.remove(t);
continue;
}
} catch (Exception e) {
LOGGER.warn("Bad template '{}' in cloud '{}': '{}'. Trying next template...",
t.getDockerTemplateBase().getImage(), getDisplayName(), e.getMessage(), e);
templates.remove(t);
continue;
}
r.add(new NodeProvisioner.PlannedNode(
t.getDockerTemplateBase().getDisplayName(),
Computer.threadPoolForRemoting.submit(new Callable<Node>() {
public Node call() throws Exception {
try {
return provisionWithWait(t);
} catch (Exception ex) {
LOGGER.error("Error in provisioning; template='{}' for cloud='{}'",
t, getDisplayName(), ex);
throw Throwables.propagate(ex);
} finally {
decrementAmiSlaveProvision(t.getDockerTemplateBase().getImage());
}
}
}),
t.getNumExecutors())
);
excessWorkload -= t.getNumExecutors();
}
return r;
} catch (Exception e) {
LOGGER.error("Exception while provisioning for label: '{}', cloud='{}'", label, getDisplayName(), e);
return Collections.emptyList();
}
}
/**
* Run docker container
*/
public static String runContainer(DockerTemplate dockerTemplate,
DockerClient dockerClient,
DockerComputerLauncher launcher)
throws DockerException, IOException {
final DockerTemplateBase dockerTemplateBase = dockerTemplate.getDockerTemplateBase();
CreateContainerCmd containerConfig = dockerClient.createContainerCmd(dockerTemplateBase.getImage());
dockerTemplateBase.fillContainerConfig(containerConfig);
// contribute launcher specific options
if (launcher != null) {
launcher.appendContainerConfig(dockerTemplate, containerConfig);
}
// create
CreateContainerResponse response = containerConfig.exec();
String containerId = response.getId();
// start
StartContainerCmd startCommand = dockerClient.startContainerCmd(containerId);
startCommand.exec();
return containerId;
}
/**
* for publishers/builders. Simply runs container in docker cloud
*/
public static String runContainer(DockerTemplateBase dockerTemplateBase,
DockerClient dockerClient,
DockerComputerLauncher launcher) {
CreateContainerCmd containerConfig = dockerClient.createContainerCmd(dockerTemplateBase.getImage());
dockerTemplateBase.fillContainerConfig(containerConfig);
// // contribute launcher specific options
// if (launcher != null) {
// launcher.appendContainerConfig(dockerTemplateBase, containerConfig);
// }
// create
CreateContainerResponse response = containerConfig.exec();
String containerId = response.getId();
// start
StartContainerCmd startCommand = dockerClient.startContainerCmd(containerId);
startCommand.exec();
return containerId;
}
private void pullImage(DockerTemplate dockerTemplate) throws IOException {
final String imageName = dockerTemplate.getDockerTemplateBase().getImage();
List<Image> images = getClient().listImagesCmd().exec();
NameParser.ReposTag repostag = NameParser.parseRepositoryTag(imageName);
// if image was specified without tag, then treat as latest
final String fullImageName = repostag.repos + ":" + (repostag.tag.isEmpty() ? "latest" : repostag.tag);
boolean imageExists = Iterables.any(images, new Predicate<Image>() {
@Override
public boolean apply(Image image) {
if (image == null || image.getRepoTags() == null) {
return false;
} else {
return Arrays.asList(image.getRepoTags()).contains(fullImageName);
}
}
});
boolean pull = imageExists ?
dockerTemplate.getPullStrategy().pullIfExists(imageName) :
dockerTemplate.getPullStrategy().pullIfNotExists(imageName);
if (pull) {
LOGGER.info("Pulling image '{}' {}. This may take awhile...", imageName,
imageExists ? "again" : "since one was not found");
long startTime = System.currentTimeMillis();
PullImageCmd imgCmd = getClient().pullImageCmd(imageName);
AuthConfig authConfig = JenkinsUtils.getAuthConfigFor(imageName);
if( authConfig != null ) {
imgCmd.withAuthConfig(authConfig);
}
PullImageResultCallback cmd = imgCmd.exec(new PullImageResultCallback());
// Work-around for API issue in docker.
if( DockerPluginConfiguration.get().getPullFix()) {
try {
cmd.awaitCompletion();
} catch (InterruptedException e) {
throw new RuntimeException("Interruption whilst pulling",e);
}
} else {
cmd.awaitSuccess();
}
long pullTime = System.currentTimeMillis() - startTime;
LOGGER.info("Finished pulling image '{}', took {} ms", imageName, pullTime);
}
}
private DockerSlave provisionWithWait(DockerTemplate dockerTemplate) throws IOException, Descriptor.FormException {
pullImage(dockerTemplate);
LOGGER.info("Trying to run container for {}", dockerTemplate.getDockerTemplateBase().getImage());
final String containerId = runContainer(dockerTemplate, getClient(), dockerTemplate.getLauncher());
InspectContainerResponse ir;
try {
ir = getClient().inspectContainerCmd(containerId).exec();
} catch (DockerException ex) {
getClient().removeContainerCmd(containerId).withForce(true).exec();
throw ex;
}
// Build a description up:
String nodeDescription = "Docker Node [" + dockerTemplate.getDockerTemplateBase().getImage() + " on ";
try {
nodeDescription += getDisplayName();
} catch (Exception ex) {
nodeDescription += "???";
}
nodeDescription += "]";
String slaveName = containerId.substring(0, 12);
try {
slaveName = getDisplayName() + "-" + slaveName;
} catch (Exception ex) {
LOGGER.warn("Error fetching cloud name");
}
dockerTemplate.getLauncher().waitUp(getDisplayName(), dockerTemplate, ir);
final ComputerLauncher launcher = dockerTemplate.getLauncher().getPreparedLauncher(getDisplayName(), dockerTemplate, ir);
if( isSwarm() ) {
return new DockerSwarmSlave(this, ir, slaveName, nodeDescription, launcher, containerId, dockerTemplate, getDisplayName());
} else {
return new DockerSlave(slaveName, nodeDescription, launcher, containerId, dockerTemplate, getDisplayName());
}
}
@Override
public boolean canProvision(Label label) {
return getTemplate(label) != null;
}
@CheckForNull
public DockerTemplate getTemplate(String template) {
for (DockerTemplate t : templates) {
if (t.getDockerTemplateBase().getImage().equals(template)) {
return t;
}
}
return null;
}
/**
* Gets first {@link DockerTemplate} that has the matching {@link Label}.
*/
@CheckForNull
public DockerTemplate getTemplate(Label label) {
List<DockerTemplate> templates = getTemplates(label);
if (!templates.isEmpty()) {
return templates.get(0);
}
return null;
}
/**
* Add a new template to the cloud
*/
public synchronized void addTemplate(DockerTemplate t) {
templates.add(t);
}
/**
* Adds a template which is temporary provided and bound to a specific job.
*
* @param jobId Unique id (per master) of the job to which the template is bound.
* @param template The template to bound to a specific job.
*/
public synchronized void addJobTemplate(long jobId, DockerTemplate template) {
jobTemplates.put(jobId, template);
}
/**
* Removes a template which is bound to a specific job.
*
* @param jobId Id of the job.
*/
public synchronized void removeJobTemplate(long jobId) {
if (jobTemplates.remove(jobId) == null) {
LOGGER.warn("Couldn't remove template for job with id: {}", jobId);
}
}
public List<DockerTemplate> getTemplates() {
List<DockerTemplate> t = new ArrayList<DockerTemplate>(templates);
t.addAll(getJobTemplates().values());
return t;
}
/**
* Multiple amis can have the same label.
*
* @return Templates matched to requested label assuming slave Mode
*/
public List<DockerTemplate> getTemplates(Label label) {
ArrayList<DockerTemplate> dockerTemplates = new ArrayList<>();
for (DockerTemplate t : templates) {
if (label == null && t.getMode() == Node.Mode.NORMAL) {
dockerTemplates.add(t);
}
if (label != null && label.matches(t.getLabelSet())) {
dockerTemplates.add(t);
}
}
// add temporary templates matched to requested label
for (DockerTemplate template : getJobTemplates().values()) {
if (label != null && label.matches(template.getLabelSet())) {
dockerTemplates.add(template);
}
}
return dockerTemplates;
}
/**
* Private method to ensure that the map of job specific templates is initialized.
*
* @return The map of job specific templates.
*/
private HashMap<Long, DockerTemplate> getJobTemplates() {
if (jobTemplates == null) {
jobTemplates = new HashMap<Long, DockerTemplate>();
}
return jobTemplates;
}
/**
* Remove Docker template
*/
public synchronized void removeTemplate(DockerTemplate t) {
templates.remove(t);
}
/**
* Counts the number of instances in Docker currently running that are using the specified image.
*
* @param imageName If null, then all instances are counted.
* <p/>
* This includes those instances that may be started outside Hudson.
*/
public int countCurrentDockerSlaves(final String imageName) throws Exception {
int count = 0;
List<Container> containers = getClient().listContainersCmd().exec();
if (imageName == null) {
count = containers.size();
} else {
for (Container container : containers) {
String containerImage = container.getImage();
if (containerImage.equals(imageName)) {
count++;
}
}
}
return count;
}
/**
* Check not too many already running.
*/
private synchronized boolean addProvisionedSlave(DockerTemplate t) throws Exception {
String ami = t.getDockerTemplateBase().getImage();
int amiCap = t.instanceCap;
int estimatedTotalSlaves = countCurrentDockerSlaves(null);
int estimatedAmiSlaves = countCurrentDockerSlaves(ami);
synchronized (provisionedImages) {
int currentProvisioning = 0;
if (provisionedImages.containsKey(ami)) {
currentProvisioning = provisionedImages.get(ami);
}
for (int amiCount : provisionedImages.values()) {
estimatedTotalSlaves += amiCount;
}
estimatedAmiSlaves += currentProvisioning;
if (estimatedTotalSlaves >= getContainerCap()) {
LOGGER.info("Not Provisioning '{}'; Server '{}' full with '{}' container(s)", ami, name, getContainerCap());
return false; // maxed out
}
if (amiCap != 0 && estimatedAmiSlaves >= amiCap) {
LOGGER.info("Not Provisioning '{}'. Instance limit of '{}' reached on server '{}'", ami, amiCap, name);
return false; // maxed out
}
LOGGER.info("Provisioning '{}' number '{}' on '{}'; Total containers: '{}'",
ami, estimatedAmiSlaves, name, estimatedTotalSlaves);
provisionedImages.put(ami, currentProvisioning + 1);
return true;
}
}
public static DockerCloud getCloudByName(String name) {
return (DockerCloud) Jenkins.getInstance().getCloud(name);
}
public Object readResolve() {
//Xstream is not calling readResolve() for nested Describable's
for (DockerTemplate template : getTemplates()) {
template.readResolve();
}
// This change will bite a lot of people otherwise.
serverUrl = sanitizeUrl(serverUrl);
return this;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("name", name)
.add("serverUrl", serverUrl)
.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DockerCloud that = (DockerCloud) o;
if (containerCap != that.containerCap) return false;
if (connectTimeout != that.connectTimeout) return false;
if (readTimeout != that.readTimeout) return false;
if (templates != null ? !templates.equals(that.templates) : that.templates != null) return false;
if (serverUrl != null ? !serverUrl.equals(that.serverUrl) : that.serverUrl != null) return false;
if (version != null ? !version.equals(that.version) : that.version != null) return false;
if (credentialsId != null ? !credentialsId.equals(that.credentialsId) : that.credentialsId != null)
return false;
return !(connection != null ? !connection.equals(that.connection) : that.connection != null);
}
/* package */ boolean isSwarm() {
Version remoteVersion = getClient().versionCmd().exec();
// Cache the return.
if( _isSwarm == null ) {
_isSwarm = remoteVersion.getVersion().startsWith("swarm");
}
return _isSwarm;
}
public boolean isTriton() {
Version remoteVersion = getClient().versionCmd().exec();
if( _isTriton == null ) {
_isTriton = remoteVersion.getOperatingSystem().equals("solaris");
}
return _isTriton;
}
public boolean isExposeDockerHost() {
// if null (i.e migration from previous installation) consider true for backward compatibility
return exposeDockerHost != null ? exposeDockerHost : true;
}
@DataBoundSetter
public void setExposeDockerHost(boolean exposeDockerHost) {
this.exposeDockerHost = exposeDockerHost;
}
@Extension
public static class DescriptorImpl extends Descriptor<Cloud> {
@Override
public String getDisplayName() {
return "Docker";
}
public FormValidation doTestConnection(
@QueryParameter String serverUrl,
@QueryParameter String credentialsId,
@QueryParameter String version,
@QueryParameter Integer readTimeout,
@QueryParameter Integer connectTimeout
) throws IOException, ServletException, DockerException {
try {
final DockerClientConfig clientConfig = ClientConfigBuilderForPlugin.dockerClientConfig()
.forServer(serverUrl, version)
.withCredentials(credentialsId)
.build();
final DockerCmdExecConfig execConfig = DockerCmdExecConfigBuilderForPlugin.builder()
.withReadTimeout(readTimeout)
.withConnectTimeout(connectTimeout)
.build();
DockerClient dc = ClientBuilderForPlugin.builder()
.withDockerClientConfig(clientConfig)
.withDockerCmdExecConfig(execConfig)
.build();
Version verResult = dc.versionCmd().exec();
return FormValidation.ok("Version = " + verResult.getVersion() + ", API Version = " + verResult.getApiVersion());
} catch (Exception e) {
return FormValidation.error(e, e.getMessage());
}
}
public ListBoxModel doFillCredentialsIdItems(@AncestorInPath ItemGroup context) {
List<StandardCertificateCredentials> credentials = CredentialsProvider.lookupCredentials(StandardCertificateCredentials.class, context, ACL.SYSTEM,Collections.<DomainRequirement>emptyList());
List<DockerDirectoryCredentials> c2 = CredentialsProvider.lookupCredentials(DockerDirectoryCredentials.class, context, ACL.SYSTEM,Collections.<DomainRequirement>emptyList());
return new StandardListBoxModel()
.withEmptySelection()
.withMatching(CredentialsMatchers.always(), credentials)
.withMatching(CredentialsMatchers.always(), c2);
}
}
}