package org.jenkinsci.plugins.dockerbuildstep; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.exception.DockerException; import com.github.dockerjava.api.command.DockerCmdExecFactory; import com.github.dockerjava.api.model.AuthConfig; import com.github.dockerjava.core.DockerClientBuilder; import com.github.dockerjava.core.DefaultDockerClientConfig; import com.github.dockerjava.core.LocalDirectorySSLConfig; import com.github.dockerjava.core.SSLConfig; import com.github.dockerjava.jaxrs.DockerCmdExecFactoryImpl; import hudson.AbortException; import hudson.DescriptorExtensionList; import hudson.Extension; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.Builder; import hudson.util.FormValidation; import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.jenkinsci.plugins.dockerbuildstep.cmd.DockerCommand; import org.jenkinsci.plugins.dockerbuildstep.cmd.DockerCommand.DockerCommandDescriptor; import org.jenkinsci.plugins.dockerbuildstep.log.ConsoleLogger; import org.jenkinsci.plugins.dockerbuildstep.util.Resolver; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import javax.net.ssl.SSLContext; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.util.logging.Level; import java.util.logging.Logger; import static org.apache.commons.lang.StringUtils.isBlank; import static org.apache.commons.lang.StringUtils.isEmpty; /** * Build step which executes various Docker commands via Docker REST API. * * @author vjuranek */ public class DockerBuilder extends Builder { private DockerCommand dockerCmd; @DataBoundConstructor public DockerBuilder(DockerCommand dockerCmd) { this.dockerCmd = dockerCmd; } public DockerCommand getDockerCmd() { return dockerCmd; } @Override public boolean perform(@SuppressWarnings("rawtypes") AbstractBuild build, Launcher launcher, BuildListener listener) throws AbortException { ConsoleLogger clog = new ConsoleLogger(listener); if (getDescriptor().getDockerClient(build, null) == null) { clog.logError("docker client is not initialized, command '" + dockerCmd.getDescriptor().getDisplayName() + "' was aborted. Check Jenkins server log which Docker client wasn't initialized"); throw new AbortException("Docker client wasn't initialized."); } try { dockerCmd.execute(build, clog); } catch (DockerException e) { clog.logError("command '" + dockerCmd.getDescriptor().getDisplayName() + "' failed: " + e.getMessage()); LOGGER.severe("Failed to execute Docker command " + dockerCmd.getDescriptor().getDisplayName() + ": " + e.getMessage()); throw new AbortException(e.getMessage()); } return true; } @Override public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } @Extension public static final class DescriptorImpl extends BuildStepDescriptor<Builder> { private String dockerUrl; private String dockerVersion; private String dockerCertPath; public DescriptorImpl() { load(); if (isEmpty(dockerUrl)) { LOGGER.warning("Docker URL is not set, docker client won't be initialized"); return; } try { getDockerClient(null, null); } catch (Exception e) { LOGGER.warning("Cannot create Docker client: " + e.getCause()); } } private static DockerClient createDockerClient(String dockerUrl, String dockerVersion, String dockerCertPath, AuthConfig authConfig) { // TODO JENKINS-26512 SSLConfig dummySSLConf = (new SSLConfig() { public SSLContext getSSLContext() throws KeyManagementException, UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException { return null; } }); if (dockerCertPath != null) { dummySSLConf = new LocalDirectorySSLConfig(dockerCertPath); } DefaultDockerClientConfig.Builder configBuilder = new DefaultDockerClientConfig.Builder().withDockerHost(dockerUrl) .withApiVersion(dockerVersion).withCustomSslConfig(dummySSLConf); if (authConfig != null) { configBuilder.withRegistryUsername(authConfig.getUsername()) .withRegistryEmail(authConfig.getEmail()) .withRegistryPassword(authConfig.getPassword()) .withRegistryUrl(authConfig.getRegistryAddress()); } ClassLoader classLoader = Jenkins.getInstance().getPluginManager().uberClassLoader; // using jaxrs/jersey implementation here (netty impl is also available) DockerCmdExecFactory dockerCmdExecFactory = new DockerCmdExecFactoryImpl() .withConnectTimeout(1000) .withMaxTotalConnections(1) .withMaxPerRouteConnections(1); return DockerClientBuilder.getInstance(configBuilder).withDockerCmdExecFactory(dockerCmdExecFactory).build(); } public FormValidation doTestConnection(@QueryParameter String dockerUrl, @QueryParameter String dockerVersion, @QueryParameter String dockerCertPath) { LOGGER.fine(String.format("Trying to get client for %s and version %s and cert path %s", dockerUrl, dockerVersion, dockerCertPath)); try { this.dockerUrl = dockerUrl; this.dockerVersion = dockerVersion; this.dockerCertPath = dockerCertPath; DockerClient dockerClient = getDockerClient(null, null); dockerClient.pingCmd().exec(); } catch (Exception e) { LOGGER.log(Level.WARNING, e.getMessage(), e); return FormValidation.error("Something went wrong, cannot connect to " + dockerUrl + ", cause: " + e.getCause()); } return FormValidation.ok("Connected to " + dockerUrl); } public boolean isApplicable(@SuppressWarnings("rawtypes") Class<? extends AbstractProject> aClass) { return true; } public String getDisplayName() { return "Execute Docker command"; } @Override public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { dockerUrl = formData.getString("dockerUrl"); dockerVersion = formData.getString("dockerVersion"); dockerCertPath = formData.getString("dockerCertPath"); if (isBlank(dockerUrl)) { LOGGER.severe("Docker URL is empty, Docker build test plugin cannot work without Docker URL being set up properly"); // JENKINS-23733 doen't block user to save the config if admin decides so return true; } save(); try { getDockerClient(null, null); } catch (Exception e) { LOGGER.warning("Cannot create Docker client: " + e.getCause()); } return super.configure(req, formData); } public String getDockerUrl() { return dockerUrl; } public String getDockerVersion() { return dockerVersion; } public String getDockerCertPath() { return dockerCertPath; } public DockerClient getDockerClient(AuthConfig authConfig) { // Reason to return a new DockerClient each time this function is called: // - It is a legitimate scenario that different jobs or different build steps // in the same job may need to use one credential to connect to one // docker registry but needs another credential to connect to another docker // registry. // - Recent docker-java client made some changes so that it requires valid // AuthConfig to be provided when DockerClient is created for certain commands // when auth is needed. We don't have control on how docker-java client is // implemented. // So to satisfy thread safety on the returned DockerClient // (when different AuthConfig are are needed), it is better to return a new // instance each time this function is called. return createDockerClient(dockerUrl, dockerVersion, dockerCertPath, authConfig); } public DockerClient getDockerClient(AbstractBuild<?, ?> build, AuthConfig authConfig) { String dockerUrlRes = build == null ? Resolver.envVar(dockerUrl) : Resolver.buildVar(build, dockerUrl); String dockerVersionRes = build == null ? Resolver.envVar(dockerVersion) : Resolver.buildVar(build, dockerVersion); String dockerCertPathRes = build == null ? Resolver.envVar(dockerCertPath) : Resolver.buildVar(build, dockerCertPath); return createDockerClient(dockerUrlRes, dockerVersionRes, dockerCertPathRes, authConfig); } public DescriptorExtensionList<DockerCommand, DockerCommandDescriptor> getCmdDescriptors() { return DockerCommand.all(); } } private static Logger LOGGER = Logger.getLogger(DockerBuilder.class.getName()); }