package org.jenkinsci.plugins.openshift; import com.openshift.client.IApplication; import com.openshift.client.IHttpClient.ISSLCertificateCallback; import hudson.AbortException; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.BuildListener; import hudson.model.Result; import hudson.remoting.VirtualChannel; import hudson.tasks.BuildStep; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Builder; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import net.sf.json.JSONNull; import net.sf.json.JSONObject; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.eclipse.jgit.api.errors.GitAPIException; import org.jenkinsci.plugins.openshift.OpenShiftV2Client.DeploymentType; import org.jenkinsci.plugins.openshift.OpenShiftV2Client.ValidationResult; import org.jenkinsci.plugins.openshift.util.JenkinsLogger; import org.jenkinsci.plugins.openshift.util.Utils; import org.jenkinsci.plugins.tokenmacro.TokenMacro; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import javax.net.ssl.SSLSession; import java.io.File; import java.io.FileFilter; import java.io.FilenameFilter; import java.io.IOException; import java.net.URL; import java.util.*; import java.util.logging.Logger; import static org.jenkinsci.plugins.openshift.util.Utils.*; /** * @author Siamak Sadeghianfar */ public class DeployApplication extends Builder implements BuildStep { private static final String WORK_DIR = "/openshift-deployer-workdir"; private static final String BINARY_TAR_NAME = "app.tar.gz"; private static final Logger LOG = Logger.getLogger(DeployApplication.class .getName()); private String serverName; private String cartridges; private String domain; private String gearProfile; private String appName; private String deploymentPackage; private String environmentVariables; private Boolean autoScale; private DeploymentType deploymentType = DeploymentType.GIT; private String openshiftDirectory; @DataBoundConstructor public DeployApplication(String serverName, String appName, String cartridges, String domain, String gearProfile, String deploymentPackage, String environmentVariables, Boolean autoScale, DeploymentType deploymentType, String openshiftDirectory) { this.serverName = serverName; this.appName = appName; this.cartridges = cartridges; this.domain = domain; this.gearProfile = gearProfile; this.deploymentPackage = deploymentPackage; this.environmentVariables = environmentVariables; this.autoScale = autoScale; this.deploymentType = deploymentType; this.openshiftDirectory = openshiftDirectory; } @Override public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { if (build != null && build.getResult() != null && build.getResult().isWorseThan(Result.SUCCESS)) { abort(listener, "Build is not success : will not try to deploy."); } if (isEmpty(appName)) { abort(listener, "Application name is not specified."); } if (isEmpty(cartridges)) { abort(listener, "Cartridges are not specified."); } if (isEmpty(deploymentPackage)) { abort(listener, "Deployment path is not specified."); } try { // find deployment unit List deployments = findDeployments(build, listener); if (deployments.isEmpty()) { abort(listener, "No packages found to deploy to OpenShift."); } else { log(listener, "Deployments found: " + deployments); } Server server = findServer(serverName); if (server == null) { abort(listener, "No OpenShift server is selected or none are defined in Jenkins Configuration."); } log(listener, "Deploying to OpenShift at http://" + server.getBrokerAddress() + ". Be patient! It might take a minute..."); OpenShiftV2Client client = new OpenShiftV2Client( server.getBrokerAddress(), server.getUsername(), server.getPassword()); String targetDomain = domain; if (isEmpty(targetDomain)) { // pick the domain if only one exists List domains = client.getDomains(); if (domains.size() > 1) { abort(listener, "Specify the user domain. " + domains.size() + " domains found on the account."); } else if (domains.isEmpty()) { abort(listener, "No domains exist. Create a domain first."); } targetDomain = domains.get(0); } IApplication app; if (isEmpty(environmentVariables)) { app = client.getOrCreateApp(appName, targetDomain, Arrays.asList(cartridges.split(" ")), gearProfile, null, autoScale); } else { Map mapOfEnvironmentVariables = new HashMap(); for (String environmentVariable : Arrays .asList(environmentVariables.split(" "))) { if (environmentVariable.contains("=")) { String[] parts = environmentVariable.split("="); mapOfEnvironmentVariables.put(parts[0], parts[1]); } else { abort(listener, "Invalid environment variable: " + environmentVariable); } } app = client.getOrCreateApp(appName, targetDomain, Arrays.asList(cartridges.split(" ")), gearProfile, mapOfEnvironmentVariables, autoScale); } deploy(deployments, app, build, listener); } catch (Exception e) { abort(listener, e); } return true; } private void deploy(List deployments, IApplication app, AbstractBuild build, BuildListener listener) throws GitAPIException, IOException { if (deployments == null || deployments.isEmpty()) { abort(listener, "Deployment package list is empty."); } if (deploymentType == DeploymentType.BINARY) { doBinaryDeploy(deployments.get(0), app, build, listener); } else { doGitDeploy(deployments, app, build, listener); } log(listener, "Application deployed to " + app.getApplicationUrl()); } private void doBinaryDeploy(String deployment, IApplication app, AbstractBuild build, final BuildListener listener) throws IOException { // reconfigure app for binary deploy if (!app.getDeploymentType().equalsIgnoreCase( DeploymentType.BINARY.name())) { app.setDeploymentType(DeploymentType.BINARY.toString() .toLowerCase()); } // deploy SSHClient sshClient = new SSHClient(app); sshClient.setLogger(new JenkinsLogger(listener)); sshClient.setSSHPrivateKey(Utils.getSSHPrivateKey()); sshClient.deploy(getBinaryDeploymentFile(build, deployment, listener)); } private File getBinaryDeploymentFile(AbstractBuild build, String deployment, BuildListener listener) throws IOException { if (isURL(deployment)) { File baseDir = createBaseDir(build); File dest = new File(baseDir.getAbsolutePath() + "/" + BINARY_TAR_NAME); LOG.fine("Downloading the deployment binary to '" + dest.getAbsolutePath() + "'"); copyURLToFile(new URL(deployment), dest, 10000, 10000); return dest; } else { File baseDir = createBaseDir(build); File dest = new File(baseDir.getAbsolutePath() + "/" + BINARY_TAR_NAME); log(listener, "Creating local file for deployment: " + dest.getAbsolutePath()); FilePath remote = new FilePath(build.getWorkspace().getChannel(), deployment); FilePath destFile = new FilePath(dest); try { log(listener, "Copying deployment file to local file: " + dest.getAbsolutePath()); log(listener, " Source: " + remote); log(listener, " Target: " + dest.getAbsolutePath()); remote.copyTo(destFile); } catch (InterruptedException e1) { abort(listener, "Exception copying file to master host: " + e1.getMessage()); } return dest; } } private void doGitDeploy(List deployments, IApplication app, AbstractBuild build, BuildListener listener) throws GitAPIException, IOException { File baseDir = createBaseDir(build); String commitMsg = "deployment added for Jenkins build " + build.getDisplayName() + "#" + build.getNumber(); String relativeDeployPath; if (cartridges.contains("jbossews")) { relativeDeployPath = "/webapps"; // tomcat } else { relativeDeployPath = "/deployments"; // jboss/wildfly } String dotOpenshiftDirectory = null; if (!StringUtils.isEmpty(openshiftDirectory)) { if (new File(openshiftDirectory).isAbsolute()) dotOpenshiftDirectory = openshiftDirectory; else dotOpenshiftDirectory = build.getWorkspace() + File.separator + openshiftDirectory; } GitClient gitClient = new GitClient(app); gitClient.setLogger(new JenkinsLogger(listener)); gitClient.deploy(deployments, baseDir, relativeDeployPath, commitMsg, dotOpenshiftDirectory); } private File createBaseDir(AbstractBuild build) throws IOException { File baseDir = new File(build.getWorkspace() + WORK_DIR); if (baseDir.exists()) { FileUtils.deleteDirectory(baseDir); } baseDir.mkdirs(); return baseDir; } @SuppressWarnings("deprecation") private List findDeployments(AbstractBuild build, BuildListener listener) throws AbortException { List deployments = new ArrayList(); if (isURL(deploymentPackage)) { try { deployments.add(TokenMacro.expand(build, listener, deploymentPackage)); } catch (Exception e) { throw new AbortException(e.getMessage()); } } else { String filePath = build.getWorkspace() + File.separator + deploymentPackage; VirtualChannel channel = build.getWorkspace().getChannel(); log(listener, "Using hudson.FilePath for resolving content for deploy:\n Channel: " + channel + " \n FilePath: " + filePath); FilePath dir = new FilePath(channel, filePath); try { if (!dir.exists()) { abort(listener, "Directory '" + dir + "' doesn't exist. No deployments found!"); } } catch (IOException e) { throw new AbortException(e.getMessage()); } catch (InterruptedException e) { throw new AbortException(e.getMessage()); } // Let us handle directories try { if (dir.isDirectory()) { String includes = null; if (deploymentType == DeploymentType.BINARY) { includes = "*.tar.gz"; } else { includes = "*.ear,*.war"; } FilePath[] deploymentFiles = dir.list(includes); for (FilePath file : deploymentFiles) { deployments.add(file.toString()); log(listener, "Adding " + file.toString() + " to Deployment List"); } } // Handle single Files else if (!dir.isDirectory() && (dir.toString().toLowerCase().endsWith(".ear") || dir .toString().toLowerCase().endsWith(".war"))) { deployments.add(dir.toString()); log(listener, "Adding " + dir.toString() + " to Deployment List"); } } catch (IOException e) { throw new AbortException(e.getMessage()); } catch (InterruptedException e) { throw new AbortException(e.getMessage()); } } // If we cannot find any deployments we should abort to avoid // NullPointers if (deployments.isEmpty()) { abort(listener, "No Deployments found! (configuredValue: " + deploymentPackage + ")"); } return deployments; } public boolean isBinaryDeploy() { return deploymentType == DeploymentType.BINARY; } public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.BUILD; } public String getServerName() { return serverName; } public String getCartridges() { return cartridges; } public String getDomain() { return domain; } public String getGearProfile() { return gearProfile; } public String getAppName() { return appName; } public String getDeploymentPackage() { return deploymentPackage; } public String getEnvironmentVariables() { return environmentVariables; } public Boolean autoScale() { return autoScale; } public DeploymentType getDeploymentType() { return deploymentType; } public static class TrustingISSLCertificateCallback implements ISSLCertificateCallback { public boolean allowCertificate( java.security.cert.X509Certificate[] certs) { return true; } public boolean allowHostname(String hostname, SSLSession session) { return true; } } @Extension public static class DeployApplicationDescriptor extends AbstractDescriptor { private final String DEFAULT_PUBLICKEY_PATH = System .getProperty("user.home") + "/.ssh/id_rsa.pub"; private List servers = new ArrayList(); public String publicKeyPath; public DeployApplicationDescriptor() { super(DeployApplication.class); load(); } @Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException { Object s = json.get("servers"); if (!JSONNull.getInstance().equals(s)) { servers = req.bindJSONToList(Server.class, s); } else { servers = null; } publicKeyPath = json.getString("publicKeyPath"); save(); return super.configure(req, json); } @Override public String getDisplayName() { return Utils.getBuildStepName("Deploy Application"); } public List getServers() { return servers; } public String getPublicKeyPath() { return isEmpty(publicKeyPath) ? DEFAULT_PUBLICKEY_PATH : publicKeyPath; } public FormValidation doCheckLogin( @QueryParameter("brokerAddress") final String brokerAddress, @QueryParameter("username") final String username, @QueryParameter("password") final String password) { OpenShiftV2Client client = new OpenShiftV2Client(brokerAddress, username, password); ValidationResult result = client.validate(); if (result.isValid()) { return FormValidation.ok("Success"); } else { return FormValidation.error(result.getMessage()); } } public FormValidation doUploadSSHKeys( @QueryParameter("brokerAddress") final String brokerAddress, @QueryParameter("username") final String username, @QueryParameter("password") final String password, @QueryParameter("publicKeyPath") final String publicKeyPath) { OpenShiftV2Client client = new OpenShiftV2Client(brokerAddress, username, password); try { if (publicKeyPath == null) { return FormValidation .error("Specify the path to SSH public key."); } File file = new File(publicKeyPath); if (!file.exists()) { return FormValidation .error("Specified SSH public key doesn't exist: " + publicKeyPath); } if (client.sshKeyExists(file)) { return FormValidation.ok("SSH public key already exists."); } else { client.uploadSSHKey(file); return FormValidation .ok("SSH Public key uploaded successfully."); } } catch (IOException e) { e.printStackTrace(); return FormValidation.error(e.getMessage()); } } public ListBoxModel doFillGearProfileItems( @QueryParameter("serverName") final String serverName) { ListBoxModel items = new ListBoxModel(); Server server = findServer(serverName); if (server == null) { return items; } OpenShiftV2Client client = new OpenShiftV2Client( server.getBrokerAddress(), server.getUsername(), server.getPassword()); for (String gearProfile : client.getGearProfiles()) { items.add(gearProfile, gearProfile); } return items; } public FormValidation doCheckPublicKeyPath( @QueryParameter("publicKeyPath") String path) { File file = new File(path); if (!file.exists()) { return FormValidation.error("Public key doesn't exist at " + path); } return FormValidation.ok(); } } }