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.text.StrTokenizer; 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.MacroEvaluationException; 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.IOException; import java.util.*; import java.util.logging.Logger; import static java.util.Collections.singletonList; import static org.apache.commons.lang3.StringUtils.isEmpty; import static org.apache.commons.lang3.text.StrMatcher.quoteMatcher; import static org.apache.commons.lang3.text.StrMatcher.spaceMatcher; import static org.jenkinsci.plugins.openshift.util.Utils.*; /** * @author Siamak Sadeghianfar <ssadeghi@redhat.com> */ public class DeployApplication extends Builder implements BuildStep { private static final String WORK_DIR = "/openshift-deployer-workdir"; 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<String> 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<String> 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(expandedAppName(build, listener), targetDomain, Arrays.asList(cartridges.split(" ")), gearProfile, null, autoScale); } else { Map<String, String> mapOfEnvironmentVariables = parseEnvironmentVariables(listener); app = client.getOrCreateApp(expandedAppName(build, listener), targetDomain, Arrays.asList(cartridges.split(" ")), gearProfile, mapOfEnvironmentVariables, autoScale); } deploy(deployments, app, build, listener); } catch (Exception e) { abort(listener, e); } return true; } private Map<String, String> parseEnvironmentVariables(final BuildListener listener) throws AbortException { Map<String, String> mapOfEnvironmentVariables = new HashMap<String, String>(); for (String environmentVariable : new StrTokenizer(environmentVariables, spaceMatcher(), quoteMatcher()).getTokenList()) { if (environmentVariable.contains("=")) { String[] parts = environmentVariable.split("=", 2); mapOfEnvironmentVariables.put(parts[0], parts[1]); } else { abort(listener, "Invalid environment variable: " + environmentVariable); } } return mapOfEnvironmentVariables; } private void deploy(List<String> 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()); } // copy deployments to master from the slave node or URLs File baseDir = createBaseDirOnMaster(build); List<String> localDeployments = Utils.copyDeploymenstToMaster(build, listener, singletonList(deployment), baseDir, deploymentType); // deploy SSHClient sshClient = new SSHClient(app); sshClient.setLogger(new JenkinsLogger(listener)); sshClient.setSSHPrivateKey(Utils.getSSHPrivateKey()); sshClient.deploy(new File(localDeployments.get(0))); } private void doGitDeploy(List<String> deployments, IApplication app, AbstractBuild<?, ?> build, BuildListener listener) throws GitAPIException, IOException { File baseDir = createBaseDirOnMaster(build); String commitMsg = "deployment added for Jenkins build " + build.getDisplayName() + "#" + build.getNumber(); // set deployment dir based on cartridge type String relativeDeployPath; if (cartridges.contains("jbossews")) { relativeDeployPath = "/webapps"; // tomcat } else { relativeDeployPath = "/deployments"; // jboss/wildfly } // set .openshift dir String dotOpenshiftDir = null; if(!isEmpty(openshiftDirectory)) { if (new File(openshiftDirectory).isAbsolute()) { dotOpenshiftDir = openshiftDirectory; } else { dotOpenshiftDir = build.getWorkspace() + File.separator + openshiftDirectory; } if (!Utils.runingOnMaster()) { String localDotOpenShiftDir = baseDir + File.separator + ".openshift"; copyFileFromSlaveToMaster(build, dotOpenshiftDir, localDotOpenShiftDir); dotOpenshiftDir = localDotOpenShiftDir; } } // copy deployments to master from the slave node or URL List<String> localDeployments = copyDeploymenstToMaster(build, listener, deployments, baseDir, deploymentType); // set git base dir File gitBaseDir = new File(baseDir, "git"); // git deploy GitClient gitClient = new GitClient(app); gitClient.setLogger(new JenkinsLogger(listener)); gitClient.deploy(localDeployments, gitBaseDir, relativeDeployPath, commitMsg, dotOpenshiftDir); } private File createBaseDirOnMaster(AbstractBuild<?, ?> build) throws IOException { String baseDirPath = Utils.getBuildWorkspaceOnMaster(build) + WORK_DIR; File baseDir = new File(baseDirPath); if (baseDir.exists()) { FileUtils.deleteDirectory(baseDir); } baseDir.mkdirs(); return baseDir; } private List<String> findDeployments(AbstractBuild<?, ?> build, BuildListener listener) throws AbortException { List<String> deployments = new ArrayList<String>(); if (isURL(deploymentPackage)) { try { deployments.add(expandedDeploymentPackage(build, listener)); } catch (Exception e) { throw new AbortException(e.getMessage()); } } else { VirtualChannel channel = build.getWorkspace().getChannel(); String filePath = null; if (new File(deploymentPackage).isAbsolute()) { filePath = deploymentPackage; } else { filePath = build.getWorkspace() + File.separator + deploymentPackage; } FilePath dir = new FilePath(channel, filePath); LOG.fine("Using hudson.FilePath for resolving content for deploy:\n Channel: " + channel + " \n FilePath: " + filePath); try { if (!dir.exists()) { abort(listener, "Directory '" + dir + "' doesn't exist. No deployments found!"); } } catch (Exception e) { throw new AbortException(e.getMessage()); } 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.getRemote()); LOG.fine("Adding " + file.getRemote() + " to deployment list"); } } else if (!dir.isDirectory() && (dir.getRemote().toLowerCase().endsWith(".ear") || dir.getRemote().toLowerCase().endsWith(".war") || dir.getRemote().toLowerCase().endsWith(".tar.gz"))) { // Handle single Files deployments.add(dir.getRemote()); LOG.fine("Adding " + dir.getRemote() + " to the deployment list"); } } catch (Exception 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; } private String expandedAppName(final AbstractBuild<?, ?> build, final BuildListener listener) throws AbortException { return expandAll(build, listener, appName); } private String expandedCartridges(final AbstractBuild<?, ?> build, final BuildListener listener) throws AbortException { return expandAll(build, listener, cartridges); } private String expandedDeploymentPackage(final AbstractBuild<?, ?> build, final BuildListener listener) throws AbortException { return expandAll(build, listener, deploymentPackage); } private String expandAll(final AbstractBuild<?, ?> build, final BuildListener listener, String stringWithMacro) throws AbortException { try { return TokenMacro.expandAll(build, listener, stringWithMacro); } catch (MacroEvaluationException e) { throw new AbortException(e.getMessage()); } catch (InterruptedException e) { throw new AbortException(e.getMessage()); } catch (IOException e) { throw new AbortException(e.getMessage()); } } 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<Server> servers = new ArrayList<Server>(); 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<Server> 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, @QueryParameter("domain") final String domain) { 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(domain)) { 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(); } } }