package com.nirima.jenkins.plugins.docker;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.PushImageCmd;
import com.github.dockerjava.api.exception.DockerException;
import com.github.dockerjava.api.model.AuthConfig;
import com.github.dockerjava.api.model.Identifier;
import com.github.dockerjava.api.model.PushResponseItem;
import com.github.dockerjava.core.NameParser;
import com.github.dockerjava.core.command.PullImageResultCallback;
import com.github.dockerjava.core.command.PushImageResultCallback;
import com.nirima.jenkins.plugins.docker.action.DockerBuildAction;
import hudson.Extension;
import hudson.model.*;
import hudson.model.queue.CauseOfBlockage;
import hudson.slaves.*;
import jenkins.model.Jenkins;
import org.jenkinsci.plugins.tokenmacro.TokenMacro;
import shaded.com.google.common.base.MoreObjects;
import shaded.com.google.common.base.Preconditions;
import shaded.com.google.common.base.Strings;
import javax.annotation.CheckForNull;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import static com.nirima.jenkins.plugins.docker.utils.JenkinsUtils.getAuthConfigFor;
import static com.nirima.jenkins.plugins.docker.utils.LogUtils.printResponseItemToListener;
import static org.apache.commons.lang.StringUtils.isEmpty;
public class DockerSlave extends AbstractCloudSlave {
private static final Logger LOGGER = Logger.getLogger(DockerSlave.class.getName());
public DockerTemplate dockerTemplate;
// remember container id
@CheckForNull private String containerId;
// remember cloud name
@CheckForNull private String cloudId;
private transient Run theRun;
public DockerSlave(DockerTemplate dockerTemplate, String containerId,
String name, String nodeDescription,
String remoteFS, int numExecutors, Mode mode,
String labelString, ComputerLauncher launcher,
RetentionStrategy retentionStrategy,
List<? extends NodeProperty<?>> nodeProperties)
throws Descriptor.FormException, IOException {
super(name, nodeDescription, remoteFS, numExecutors, mode, labelString, launcher, retentionStrategy, nodeProperties);
Preconditions.checkNotNull(dockerTemplate);
Preconditions.checkNotNull(containerId);
setDockerTemplate(dockerTemplate);
this.containerId = containerId;
}
public DockerSlave(String slaveName, String nodeDescription, ComputerLauncher launcher, String containerId,
DockerTemplate dockerTemplate, String cloudId)
throws IOException, Descriptor.FormException {
super(slaveName,
nodeDescription, //description
dockerTemplate.getRemoteFs(),
dockerTemplate.getNumExecutors(),
dockerTemplate.getMode(),
dockerTemplate.getLabelString(),
launcher,
dockerTemplate.getRetentionStrategyCopy(),
Collections.<NodeProperty<?>>emptyList()
);
setContainerId(containerId);
setDockerTemplate(dockerTemplate);
setCloudId(cloudId);
}
public String getContainerId() {
return containerId;
}
public void setContainerId(String containerId) {
this.containerId = containerId;
}
public String getCloudId() {
return cloudId;
}
public void setCloudId(String cloudId) {
this.cloudId = cloudId;
}
public DockerTemplate getDockerTemplate() {
return dockerTemplate;
}
public void setDockerTemplate(DockerTemplate dockerTemplate) {
this.dockerTemplate = dockerTemplate;
}
public DockerCloud getCloud() {
final Cloud cloud = Jenkins.getInstance().getCloud(getCloudId());
if (cloud == null) {
throw new RuntimeException("Docker template " + dockerTemplate + " has no assigned Cloud.");
}
if (cloud.getClass() != DockerCloud.class) {
throw new RuntimeException("Assigned cloud is not DockerCloud");
}
return (DockerCloud) cloud;
}
@Override
public String getDisplayName() {
return name;
}
public void setRun(Run run) {
this.theRun = run;
}
@Override
public DockerComputer createComputer() {
return new DockerComputer(this);
}
@Override
public CauseOfBlockage canTake(Queue.BuildableItem item) {
if (item.task instanceof Queue.FlyweightTask) {
return new CauseOfBlockage() {
public String getShortDescription() {
return "Don't run FlyweightTask on Docker node";
}
};
}
return super.canTake(item);
}
public boolean containerExistsInCloud() {
try {
DockerClient client = getClient();
client.inspectContainerCmd(containerId).exec();
return true;
} catch (Exception ex) {
return false;
}
}
@Override
protected void _terminate(TaskListener listener) throws IOException, InterruptedException {
try {
toComputer().disconnect(new DockerOfflineCause());
LOGGER.log(Level.INFO, "Disconnected computer");
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Can't disconnect", e);
}
if (containerId != null) {
try {
DockerClient client = getClient();
client.stopContainerCmd(getContainerId()).exec();
LOGGER.log(Level.INFO, "Stopped container {0}", getContainerId());
} catch (Exception ex) {
LOGGER.log(Level.SEVERE, "Failed to stop instance " + getContainerId() + " for slave " + name + " due to exception", ex.getMessage());
LOGGER.log(Level.SEVERE, "Causing exception for failure on stopping the instance was", ex);
}
// If the run was OK, then do any tagging here
if (theRun != null) {
try {
slaveShutdown(listener);
LOGGER.log(Level.INFO, "Shutdowned slave for {0}", getContainerId());
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Failure to slaveShutdown instance " + getContainerId() + " for slave " + name, e);
LOGGER.log(Level.SEVERE, "Causing exception for failure on slaveShutdown was", e);
}
}
try {
DockerClient client = getClient();
client.removeContainerCmd(containerId)
.withRemoveVolumes(getDockerTemplate().isRemoveVolumes())
.exec();
LOGGER.log(Level.INFO, "Removed container {0}", getContainerId());
} catch (Exception ex) {
LOGGER.log(Level.SEVERE, "Failed to remove instance " + getContainerId() + " for slave " + name + " due to exception: " + ex.getMessage());
LOGGER.log(Level.SEVERE, "Causing exception for failre on removing instance was", ex);
}
} else {
LOGGER.log(Level.SEVERE, "ContainerId is absent, no way to remove/stop container");
}
}
private void slaveShutdown(final TaskListener listener) throws DockerException, IOException {
// The slave has stopped. Should we commit / tag / push ?
if (!getJobProperty().tagOnCompletion) {
addJenkinsAction(null);
return;
}
DockerClient client = getClient();
// Commit
String tag_image = client.commitCmd(containerId)
.withRepository(theRun.getParent().getDisplayName())
.withTag(theRun.getDisplayName().replace("#", "b")) // allowed only ([a-zA-Z_][a-zA-Z0-9_]*)
.withAuthor("Jenkins")
.exec();
// Tag it with the jenkins name
addJenkinsAction(tag_image);
// SHould we add additional tags?
try {
String tagToken = getAdditionalTag(listener);
if (!Strings.isNullOrEmpty(tagToken)) {
final NameParser.ReposTag reposTag = NameParser.parseRepositoryTag(tagToken);
final String commitTag = isEmpty(reposTag.tag) ? "latest" : reposTag.tag;
getClient().tagImageCmd(tag_image, reposTag.repos, commitTag).withForce().exec();
addJenkinsAction(tagToken);
if (getJobProperty().pushOnSuccess) {
Identifier identifier = Identifier.fromCompoundString(tagToken);
PushImageResultCallback resultCallback = new PushImageResultCallback() {
public void onNext(PushResponseItem item) {
printResponseItemToListener(listener, item);
super.onNext(item);
}
};
try {
PushImageCmd cmd = getClient().pushImageCmd(identifier);
AuthConfig authConfig = getAuthConfigFor(tagToken);
if( authConfig != null ) {
cmd.withAuthConfig(authConfig);
}
cmd.exec(resultCallback).awaitSuccess();
} catch(DockerException ex) {
LOGGER.log(Level.SEVERE, "Exception pushing docker image. Check that the destination registry is running.", ex);
throw ex;
}
}
}
} catch (Exception ex) {
LOGGER.log(Level.SEVERE, "Could not add additional tags", ex);
}
if (getJobProperty().cleanImages) {
client.removeImageCmd(tag_image)
.withForce(true)
.exec();
}
}
private String getAdditionalTag(TaskListener listener) {
// Do a macro expansion on the addJenkinsAction token
// Job property
String tagToken = getJobProperty().additionalTag;
// Do any macro expansions
try {
if (!Strings.isNullOrEmpty(tagToken))
tagToken = TokenMacro.expandAll((AbstractBuild) theRun, listener, tagToken);
} catch (Exception e) {
LOGGER.log(Level.WARNING, "can't expand macroses", e);
}
return tagToken;
}
/**
* Add a built on docker action.
*/
private void addJenkinsAction(String tag_image) throws IOException {
theRun.addAction(new DockerBuildAction(getCloud().getServerUrl(), containerId, tag_image, dockerTemplate.remoteFsMapping));
theRun.save();
}
public DockerClient getClient() {
return getCloud().getClient();
}
private DockerJobProperty getJobProperty() {
try {
DockerJobProperty p = (DockerJobProperty) ((AbstractBuild) theRun).getProject().getProperty(DockerJobProperty.class);
if (p != null)
return p;
} catch (Exception ex) {
// Don't care.
}
// Safe default
return new DockerJobProperty(false, null, false, true, null);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("name", name)
.add("containerId", containerId)
.add("template", dockerTemplate)
.toString();
}
@Extension
public static final class DescriptorImpl extends SlaveDescriptor {
@Override
public String getDisplayName() {
return "Docker Slave";
}
@Override
public boolean isInstantiable() {
return false;
}
}
}