/* * The MIT License * * Copyright (c) 2012-2013 IKEDA Yasuyuki * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package jp.ikedam.jenkins.plugins.jobcopy_builder; import java.util.List; import hudson.Extension; import hudson.XmlFile; import hudson.EnvVars; import hudson.Launcher; import hudson.DescriptorExtensionList; import hudson.matrix.MatrixProject; import hudson.model.TopLevelItem; import hudson.model.BuildListener; import hudson.model.Job; import hudson.model.AbstractBuild; import hudson.model.AbstractItem; import hudson.model.AbstractProject; import hudson.model.Descriptor; import hudson.util.ComboBoxModel; import hudson.util.FormValidation; import hudson.tasks.Builder; import hudson.tasks.BuildStepDescriptor; import jenkins.model.Jenkins; import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import java.io.InputStream; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.Serializable; import javax.xml.transform.stream.StreamSource; /** * A build step to copy a job. * * You can specify additional operations that is performed when copying, * and the operations can be extended with plugins using Extension Points. */ public class JobcopyBuilder extends Builder implements Serializable { private static final long serialVersionUID = 1L; private String fromJobName; /** * Returns the name of job to be copied from. * * Variable expressions will be expanded. * * @return the name of job to be copied from */ public String getFromJobName() { return fromJobName; } private String toJobName; /** * Returns the name of job to be copied to. * * Variable expressions will be expanded. * * @return the name of job to be copied to */ public String getToJobName() { return toJobName; } private boolean overwrite = false; /** * Returns whether to overwrite an existing job. * * If the copied-to job is already exists, * jobcopy build step works as following depending on this value. * <table> * <tr> * <th>isOverwrite</th> * <th>behavior</th> * </tr> * <tr> * <td>true</td> * <td>Overwrite the configuration of the existing job.</td> * </tr> * <tr> * <td>false</td> * <td>Build fails.</td> * </tr> * </table> * * @return whether to overwrite an existing job. */ public boolean isOverwrite() { return overwrite; } private List<JobcopyOperation> jobcopyOperationList; /** * Returns the list of operations. * * @return the list of operations */ public List<JobcopyOperation> getJobcopyOperationList() { return jobcopyOperationList; } private List<AdditionalFileset> additionalFilesetList; /** * Retuns a list of sets of files to copy additional to JOBNAME/config.xml. * * @return the additionalFilesetList */ public List<AdditionalFileset> getAdditionalFilesetList() { return additionalFilesetList; } /** * Constructor to instantiate from parameters in the job configuration page. * * When instantiating from the saved configuration, * the object is directly serialized with XStream, * and no constructor is used. * * @param fromJobName a name of a job to be copied from. may contains variable expressions. * @param toJobName a name of a job to be copied to. may contains variable expressions. * @param overwrite whether to overwrite if the job to be copied to is already existing. * @param jobcopyOperationList * the list of operations to be performed when copying. * @param additionalFilesetList * the list of sets of files to copy additional to JOBNAME/config.xml. */ @DataBoundConstructor public JobcopyBuilder(String fromJobName, String toJobName, boolean overwrite, List<JobcopyOperation> jobcopyOperationList, List<AdditionalFileset> additionalFilesetList) { this.fromJobName = StringUtils.trim(fromJobName); this.toJobName = StringUtils.trim(toJobName); this.overwrite = overwrite; this.jobcopyOperationList = jobcopyOperationList; this.additionalFilesetList = additionalFilesetList; } /** * Performs the build step. * * @param build * @param launcher * @param listener * @return whether the process succeeded. * @throws IOException * @throws InterruptedException * @see hudson.tasks.BuildStepCompatibilityLayer#perform(hudson.model.AbstractBuild, hudson.Launcher, hudson.model.BuildListener) */ @Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException { EnvVars env = build.getEnvironment(listener); if(StringUtils.isBlank(getFromJobName())) { listener.getLogger().println("From Job Name is not specified"); return false; } if(StringUtils.isBlank(getToJobName())) { listener.getLogger().println("To Job Name is not specified"); return false; } // Expand the variable expressions in job names. String fromJobNameExpanded = env.expand(getFromJobName()); String toJobNameExpanded = env.expand(getToJobName()); if(StringUtils.isBlank(fromJobNameExpanded)) { listener.getLogger().println("From Job Name got to a blank"); return false; } if(StringUtils.isBlank(toJobNameExpanded)) { listener.getLogger().println("To Job Name got to a blank"); return false; } listener.getLogger().println(String.format("Copying %s to %s", fromJobNameExpanded, toJobNameExpanded)); // Reteive the job to be copied from. TopLevelItem fromJob = Jenkins.getInstance().getItem(fromJobNameExpanded); if(fromJob == null) { listener.getLogger().println("Error: Item was not found."); return false; } else if(!(fromJob instanceof Job<?,?>)) { listener.getLogger().println("Error: Item was found, but is not a job."); return false; } // Check whether the job to be copied to is already exists. TopLevelItem toJob = Jenkins.getInstance().getItem(toJobNameExpanded); if(toJob != null){ listener.getLogger().println(String.format("Already exists: %s", toJobNameExpanded)); if(!isOverwrite()){ return false; } if(!(toJob instanceof AbstractItem)) { listener.getLogger().println("Only AbstractItem can be overwritten: please delete manually, and run copy again"); return false; } } // Retrieve the config.xml of the job copied from. // TODO: what happens if this runs on a slave node? listener.getLogger().println(String.format("Fetching configuration of %s...", fromJobNameExpanded)); XmlFile file = ((Job<?,?>)fromJob).getConfigFile(); String jobConfigXmlString = file.asString(); String encoding = file.sniffEncoding(); listener.getLogger().println("Original xml:"); listener.getLogger().println(jobConfigXmlString); // Apply additional operations to the retrieved XML. if(getJobcopyOperationList() != null) { for(JobcopyOperation operation: getJobcopyOperationList()) { jobConfigXmlString = operation.perform(jobConfigXmlString, encoding, env, listener.getLogger()); if(jobConfigXmlString == null) { return false; } } } listener.getLogger().println("Copied xml:"); listener.getLogger().println(jobConfigXmlString); if(toJob == null) { // Create the job copied to. listener.getLogger().println(String.format("Creating %s", toJobNameExpanded)); InputStream is = new ByteArrayInputStream(jobConfigXmlString.getBytes(encoding)); toJob = Jenkins.getInstance().createProjectFromXML(toJobNameExpanded, is); if(toJob == null) { listener.getLogger().println(String.format("Failed to create %s", toJobNameExpanded)); return false; } } else { listener.getLogger().println(String.format("Updating %s", toJobNameExpanded)); AbstractItem target = (AbstractItem)toJob; InputStream is = new ByteArrayInputStream(jobConfigXmlString.getBytes(encoding)); String combinationFilter = null; if(target instanceof MatrixProject) { MatrixProject matrix = (MatrixProject)target; // Workaround for the case combinationFilter is removed. // In that case, updateByXml does not update combinationFilter, // for combinationFilter is not written in XML. // So reset it here in advance. // It will be overwritten if defined. combinationFilter = matrix.getCombinationFilter(); matrix.setCombinationFilter(null); } try { target.updateByXml(new StreamSource(is)); } catch(IOException e) { if(combinationFilter != null) { // recover combinationFilter. MatrixProject matrix = (MatrixProject)target; matrix.setCombinationFilter(combinationFilter); } throw e; } } boolean failed = false; if(getAdditionalFilesetList() != null && !getAdditionalFilesetList().isEmpty()) { listener.getLogger().println("Copying Additional Files..."); for(AdditionalFileset fileset: getAdditionalFilesetList()) { if(!fileset.perform(toJob, fromJob, env, listener.getLogger())) { failed = true; } } // Do null update to reload the configuration. AbstractItem target = (AbstractItem)toJob; target.updateByXml(new StreamSource(target.getConfigFile().readRaw())); } // add the information of jobs copied from and to to the build. build.addAction(new CopiedjobinfoAction(fromJob, toJob, failed)); return true; } /** * The internal class to work with views. * * The following files are used (put in main/resource directory in the source tree). * <dl> * <dt>config.jelly</dt> * <dd>shown as a part of a job configuration page.</dd> * </dl> */ @Extension public static final class DescriptorImpl extends BuildStepDescriptor<Builder> { /** * Returns the display name * * Displayed in the "Add build step" dropdown in a job configuration page. * * @return the display name * @see hudson.model.Descriptor#getDisplayName() */ @Override public String getDisplayName() { return Messages.JobCopyBuilder_DisplayName(); } /** * Test whether this build step can be applied to the specified job type. * * This build step works for any type of jobs, for always returns true. * * @param jobType the type of the job to be tested. * @return true * @see hudson.tasks.BuildStepDescriptor#isApplicable(java.lang.Class) */ @SuppressWarnings("rawtypes") @Override public boolean isApplicable(Class<? extends AbstractProject> jobType) { return true; } /** * Returns all the available JobcopyOperation. * * Used for the contents of "Add Copy Operation" dropdown. * * @return the list of JobcopyOperation */ public DescriptorExtensionList<JobcopyOperation,Descriptor<JobcopyOperation>> getJobcopyOperationDescriptors() { return JobcopyOperation.all(); } /** * Returns the list of jobs. * * Used for the autocomplete of From Job Name. * * @return the list of jobs */ public ComboBoxModel doFillFromJobNameItems() { return new ComboBoxModel(Jenkins.getInstance().getTopLevelItemNames()); } /** * Returns whether the value contains variable. * * @param value value to be tested. * @return whether the value contains variable */ private boolean containsVariable(String value) { if(StringUtils.isBlank(value) || !value.contains("$")){ // apparently contains no variable. return false; } return true; } /** * Validate "From Job Name" or "To Job Name" field. * * Returns as following: * <table> * <tr> * <th>jobName</th> * <th>warnIfExists</th> * <th>warnIfNotExists</th> * <th>Returns</th> * </tr> * <tr> * <td>Blank</th> * <th>any</th> * <th>any</th> * <td>error</td> * </tr> * <tr> * <td>value containing variables</th> * <th>any</th> * <th>any</th> * <td>ok</td> * </tr> * <tr> * <td>existing job</th> * <th>false</th> * <th>any</th> * <td>ok</td> * </tr> * <tr> * <td>existing job</th> * <th>true</th> * <th>any</th> * <td>warning</td> * </tr> * <tr> * <td>non existing job</th> * <th>any</th> * <th>false</th> * <td>ok</td> * </tr> * <tr> * <td>non existing job</th> * <th>any</th> * <th>true</th> * <td>warning</td> * </tr> * </table> * * @param jobName * @param warnIfExists * @param warnIfNotExists * @return */ public FormValidation doCheckJobName(String jobName, boolean warnIfExists, boolean warnIfNotExists) { jobName = StringUtils.trim(jobName); if(StringUtils.isBlank(jobName)) { return FormValidation.error(Messages.JobCopyBuilder_JobName_empty()); } if(containsVariable(jobName)) { return FormValidation.ok(); } TopLevelItem job = Jenkins.getInstance().getItem(jobName); if(job != null) { // job exists if(warnIfExists) { return FormValidation.warning(Messages.JobCopyBuilder_JobName_exists()); } } else { // job does not exist if(warnIfNotExists) { return FormValidation.warning(Messages.JobCopyBuilder_JobName_notExists()); } } return FormValidation.ok(); } /** * Validate "From Job Name" field. * * @param fromJobName * @return */ public FormValidation doCheckFromJobName(@QueryParameter String fromJobName) { return doCheckJobName(fromJobName, false, true); } /** * Validate "To Job Name" field. * * @param toJobName * @param overwrite * @return FormValidation object. */ public FormValidation doCheckToJobName(@QueryParameter String toJobName, @QueryParameter boolean overwrite) { return doCheckJobName(toJobName, !overwrite, false); } } }