/* * The MIT License * * Copyright (c) 2004-2011, Sun Microsystems, Inc., Alan Harder * * 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 hudson.plugins.copyartifact; import com.thoughtworks.xstream.converters.UnmarshallingContext; import hudson.AbortException; import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.Util; import hudson.console.HyperlinkNote; import hudson.diagnosis.OldDataMonitor; import hudson.init.InitMilestone; import hudson.init.Initializer; import hudson.matrix.MatrixBuild; import hudson.matrix.MatrixProject; import hudson.maven.MavenBuild; import hudson.maven.MavenModuleSet; import hudson.maven.MavenModuleSetBuild; import hudson.model.*; import hudson.model.listeners.ItemListener; import hudson.security.ACL; import hudson.security.SecurityRealm; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.Builder; import hudson.util.DescribableList; import hudson.util.FormValidation; import hudson.util.VariableResolver; import hudson.util.XStream2; import java.io.IOException; import java.io.PrintStream; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.model.Jenkins; import jenkins.tasks.SimpleBuildStep; import org.acegisecurity.Authentication; import org.acegisecurity.GrantedAuthority; import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; import javax.annotation.Nonnull; import javax.annotation.Nullable; /** * Build step to copy artifacts from another project. * @author Alan Harder */ public class CopyArtifact extends Builder implements SimpleBuildStep { // specifies upgradeCopyArtifact is needed to work. private static boolean upgradeNeeded = false; private static Logger LOGGER = Logger.getLogger(CopyArtifact.class.getName()); private static final BuildSelector DEFAULT_BUILD_SELECTOR = new StatusBuildSelector(true); @Deprecated private String projectName; private String project; private String parameters; private String filter, target; private String excludes; private /*almost final*/ BuildSelector selector; @Deprecated private transient Boolean stable; private Boolean flatten, optional; private boolean doNotFingerprintArtifacts; private String resultVariableSuffix; @Deprecated public CopyArtifact(String projectName, String parameters, BuildSelector selector, String filter, String target, boolean flatten, boolean optional) { this(projectName, parameters, selector, filter, target, flatten, optional, true); } @Deprecated public CopyArtifact(String projectName, String parameters, BuildSelector selector, String filter, String target, boolean flatten, boolean optional, boolean fingerprintArtifacts) { this(projectName, parameters, selector, filter, null, target, flatten, optional, fingerprintArtifacts); } @Deprecated public CopyArtifact(String projectName, String parameters, BuildSelector selector, String filter, String excludes, String target, boolean flatten, boolean optional, boolean fingerprintArtifacts) { this(projectName); setParameters(parameters); setFilter(filter); setTarget(target); setExcludes(excludes); if (selector == null) { selector = DEFAULT_BUILD_SELECTOR; } setSelector(selector); setFlatten(flatten); setOptional(optional); setFingerprintArtifacts(fingerprintArtifacts); } @DataBoundConstructor public CopyArtifact(String projectName) { // check the permissions only if we can StaplerRequest req = Stapler.getCurrentRequest(); if (req!=null) { AbstractProject<?,?> p = req.findAncestorObject(AbstractProject.class); if (p != null) { ItemGroup<?> context = p.getParent(); // Prevents both invalid values and access to artifacts of projects which this user cannot see. // If value is parameterized, it will be checked when build runs. Jenkins jenkins = Jenkins.getInstance(); if (projectName.indexOf('$') < 0 && (jenkins == null || jenkins.getItem(projectName, context, Job.class) == null)) projectName = ""; // Ignore/clear bad value to avoid ugly 500 page } } this.project = projectName; // Apply defaults to all other properties. setParameters(null); setFilter(null); setTarget(null); setExcludes(null); setSelector(DEFAULT_BUILD_SELECTOR); setFlatten(false); setOptional(false); setFingerprintArtifacts(false); setResultVariableSuffix(null); } @DataBoundSetter public void setParameters(String parameters) { this.parameters = Util.fixEmptyAndTrim(parameters); } @DataBoundSetter public void setFilter(String filter) { this.filter = Util.fixNull(filter).trim(); } @DataBoundSetter public void setTarget(String target) { this.target = Util.fixNull(target).trim(); } @DataBoundSetter public void setExcludes(String excludes) { this.excludes = Util.fixNull(excludes).trim(); } @DataBoundSetter public void setSelector(@Nonnull BuildSelector selector) { this.selector = selector; } @DataBoundSetter public void setFlatten(boolean flatten) { this.flatten = flatten ? Boolean.TRUE : null; } @DataBoundSetter public void setOptional(boolean optional) { this.optional = optional ? Boolean.TRUE : null; } @DataBoundSetter public void setFingerprintArtifacts(boolean fingerprintArtifacts) { this.doNotFingerprintArtifacts = !fingerprintArtifacts; } /** * Set the suffix for variables to store copying results. * * @param resultVariableSuffix Variable suffix to use. */ @DataBoundSetter public void setResultVariableSuffix(String resultVariableSuffix) { this.resultVariableSuffix = Util.fixEmptyAndTrim(resultVariableSuffix); } // Upgrade data from old format public static class ConverterImpl extends XStream2.PassthruConverter<CopyArtifact> { public ConverterImpl(XStream2 xstream) { super(xstream); } @Override protected void callback(CopyArtifact obj, UnmarshallingContext context) { if (obj.selector == null) { obj.selector = new StatusBuildSelector(obj.stable != null && obj.stable); OldDataMonitor.report(context, "1.355"); // Core version# when CopyArtifact 1.2 released } if (obj.isUpgradeNeeded()) { // A Copy Artifact to be upgraded. // For information of the containing project is needed, // The upgrade will be performed by upgradeCopyArtifact. setUpgradeNeeded(); } } } private static synchronized void setUpgradeNeeded() { if (!upgradeNeeded) { LOGGER.info("Upgrade for Copy Artifact is scheduled."); upgradeNeeded = true; } } // get all CopyArtifacts configured to AbstractProject. This works both for Project and MatrixProject. private static List<CopyArtifact> getCopyArtifactsInProject(AbstractProject<?,?> project) throws IOException { DescribableList<Builder,Descriptor<Builder>> list = project instanceof Project ? ((Project<?,?>)project).getBuildersList() : (project instanceof MatrixProject ? ((MatrixProject)project).getBuildersList() : null); if (list == null) return Collections.emptyList(); return list.getAll(CopyArtifact.class); } @Initializer(after=InitMilestone.JOB_LOADED) public static void upgradeCopyArtifact() { if (!upgradeNeeded) { return; } Jenkins jenkins = Jenkins.getInstance(); if (jenkins == null) { LOGGER.log(Level.SEVERE, "Called for initializing, but Jenkins instance is unavailable."); return; } upgradeNeeded = false; boolean isUpgraded = false; for (AbstractProject<?,?> project: jenkins.getAllItems(AbstractProject.class)) { try { for (CopyArtifact target: getCopyArtifactsInProject(project)) { try { if (target.upgradeIfNecessary(project)) { isUpgraded = true; } } catch(IOException e) { LOGGER.log(Level.SEVERE, String.format("Failed to upgrade CopyArtifact in %s", project.getFullName()), e); } } } catch (IOException e) { LOGGER.log(Level.SEVERE, String.format("Failed to upgrade CopyArtifact in %s", project.getFullName()), e); } } if (!isUpgraded) { // No CopyArtifact is upgraded. LOGGER.warning("Update of CopyArtifact is scheduled, but no CopyArtifact to upgrade was found!"); } } public String getProjectName() { return project; } public String getParameters() { return parameters; } @Deprecated public BuildSelector getBuildSelector() { return selector; } public BuildSelector getSelector() { return selector; } public String getFilter() { return filter; } public String getExcludes() { return excludes; } public String getTarget() { return target; } public boolean isFlatten() { return flatten != null && flatten; } public boolean isOptional() { return optional != null && optional; } /** * @return the suffix for variables to store copying results. */ public String getResultVariableSuffix() { return resultVariableSuffix; } private boolean upgradeIfNecessary(AbstractProject<?,?> job) throws IOException { if (isUpgradeNeeded()) { Jenkins jenkins = Jenkins.getInstance(); if (jenkins == null) { LOGGER.log(Level.SEVERE, "upgrading copyartifact is required for {0} but Jenkins instance is unavailable", job.getDisplayName()); return false; } int i = projectName.lastIndexOf('/'); if (i != -1 && projectName.indexOf('=', i) != -1 && /* not matrix */jenkins.getItem(projectName, job.getParent(), Job.class) == null) { project = projectName.substring(0, i); parameters = projectName.substring(i + 1); } else { project = projectName; parameters = null; } LOGGER.log(Level.INFO, "Split {0} into {1} with parameters {2}", new Object[] {projectName, project, parameters}); projectName = null; job.save(); return true; } else { return false; } } private boolean isUpgradeNeeded() { return (projectName != null); } public boolean isFingerprintArtifacts() { return !doNotFingerprintArtifacts; } @Override public void perform(@Nonnull Run<?, ?> build, @Nonnull FilePath workspace, @Nonnull Launcher launcher, @Nonnull TaskListener listener) throws InterruptedException, IOException { Jenkins jenkins = Jenkins.getInstance(); if (jenkins == null) { throw new AbortException("Jenkins instance is unavailable."); } if (build instanceof AbstractBuild) { upgradeIfNecessary(((AbstractBuild)build).getProject()); } EnvVars env = build.getEnvironment(listener); if (build instanceof AbstractBuild) { env.putAll(((AbstractBuild)build).getBuildVariables()); // Add in matrix axes.. } else { // Abstract#getEnvironment(TaskListener) put build parameters to // environments, but Run#getEnvironment(TaskListener) doesn't. // That means we can't retrieve build parameters from WorkflowRun // as it is a subclass of Run, not of AbstractBuild. // We need expand build parameters manually. // See JENKINS-26694, JENKINS-30357 for details. for(ParametersAction pa: build.getActions(ParametersAction.class)) { // We have to extract parameters manally as ParametersAction#buildEnvVars // (overrides EnvironmentContributingAction#buildEnvVars) // is applicable only for AbstractBuild. for(ParameterValue pv: pa.getParameters()) { pv.buildEnvironment(build, env); } } } PrintStream console = listener.getLogger(); String expandedProject = project, expandedFilter = filter; String expandedExcludes = getExcludes(); expandedProject = env.expand(project); Job<?, ?> job = jenkins.getItem(expandedProject, getItemGroup(build), Job.class); if (job != null && !expandedProject.equals(project) // If projectName is parameterized, need to do permission check on source project. && !canReadFrom(job, build)) { job = null; // Disallow access } if (job == null) { throw new AbortException(Messages.CopyArtifact_MissingProject(expandedProject)); } Run src = selector.getBuild(job, env, parameters != null ? new ParametersBuildFilter(env.expand(parameters)) : new BuildFilter(), build); if (src == null) { String message = Messages.CopyArtifact_MissingBuild(expandedProject); if (isOptional()) { // just return without an error console.println(message); return; } else { // Fail build if copy is not optional throw new AbortException(message); } } FilePath targetDir = workspace, baseTargetDir = targetDir; targetDir.mkdirs(); // being a SimpleBuildStep guarantees it will have a workspace, but the physical dir might not yet exist. // Add info about the selected build into the environment EnvAction envData = build.getAction(EnvAction.class); if (envData == null) { envData = new EnvAction(); build.addAction(envData); } envData.add(build, src, expandedProject, getResultVariableSuffix()); if (target.length() > 0) targetDir = new FilePath(targetDir, env.expand(target)); expandedFilter = env.expand(filter); if (expandedFilter.trim().length() == 0) expandedFilter = "**"; expandedExcludes = env.expand(expandedExcludes); if (StringUtils.isBlank(expandedExcludes)) { expandedExcludes = null; } Copier copier = jenkins.getExtensionList(Copier.class).get(0).clone(); if (jenkins.getPlugin("maven-plugin") != null && (src instanceof MavenModuleSetBuild) ) { // use classes in the "maven-plugin" plugin as might not be installed // Copy artifacts from the build (ArchiveArtifacts build step) boolean ok = perform(src, build, expandedFilter, expandedExcludes, targetDir, baseTargetDir, copier, console); // Copy artifacts from all modules of this Maven build (automatic archiving) for (Iterator<MavenBuild> it = ((MavenModuleSetBuild)src).getModuleLastBuilds().values().iterator(); it.hasNext(); ) { // for(Run r: ....values()) causes upcasting and loading MavenBuild compiled with jdk 1.6. // SEE https://wiki.jenkins-ci.org/display/JENKINS/Tips+for+optional+dependencies for details. Run<?,?> r = it.next(); ok |= perform(r, build, expandedFilter, expandedExcludes, targetDir, baseTargetDir, copier, console); } if (!ok) { throw new AbortException(Messages.CopyArtifact_FailedToCopy(expandedProject, expandedFilter)); } } else if (src instanceof MatrixBuild) { boolean ok = false; // Copy artifacts from all configurations of this matrix build // Use MatrixBuild.getExactRuns if available for (Run r : ((MatrixBuild) src).getExactRuns()) // Use subdir of targetDir with configuration name (like "jdk=java6u20") ok |= perform(r, build, expandedFilter, expandedExcludes, targetDir.child(r.getParent().getName()), baseTargetDir, copier, console); if (!ok) { throw new AbortException(Messages.CopyArtifact_FailedToCopy(expandedProject, expandedFilter)); } } else { if (!perform(src, build, expandedFilter, expandedExcludes, targetDir, baseTargetDir, copier, console)) { throw new AbortException(Messages.CopyArtifact_FailedToCopy(expandedProject, expandedFilter)); } } } private boolean canReadFrom(Job<?, ?> job, Run<?, ?> build) { Job<?, ?> fromJob = job; Job<?, ?> toJob = build.getParent(); if (CopyArtifactPermissionProperty.canCopyArtifact(getRootProject(toJob), getRootProject(fromJob))) { return true; } Authentication a = Jenkins.getAuthentication(); if (!ACL.SYSTEM.equals(a)) { // if the build does not run on SYSTEM authorization, // Jenkins is configured to use QueueItemAuthenticator. // In this case, builds are configured to run with a proper authorization // (for example, builds run with the authorization of the user who triggered the build), // and we should check access permission with that authorization. // QueueItemAuthenticator is available from Jenkins 1.520. // See also JENKINS-14999, JENKINS-16956, JENKINS-18285. boolean b = job.getACL().hasPermission(Item.READ); if (!b) LOGGER.fine(String.format("Refusing to copy artifact from %s to %s because %s lacks Item.READ access",job,build, a)); return b; } // for the backward compatibility, // test the permission as an anonymous authenticated user. boolean b = job.getACL().hasPermission( new UsernamePasswordAuthenticationToken("authenticated", "", new GrantedAuthority[]{ SecurityRealm.AUTHENTICATED_AUTHORITY }), Item.READ); if (!b) LOGGER.fine(String.format("Refusing to copy artifact from %s to %s because 'authenticated' lacks Item.READ access",job,build)); return b; } private static Job<?, ?> getRootProject(Job<?, ?> job) { if (job instanceof AbstractProject) { return ((AbstractProject<?,?>)job).getRootProject(); } else { return job; } } // retrieve the "folder" (jenkins root if no folder used) for this build private static ItemGroup getItemGroup(Run<?, ?> build) { return getRootProject(build.getParent()).getParent(); } private boolean perform(Run src, Run<?,?> dst, String expandedFilter, String expandedExcludes, FilePath targetDir, FilePath baseTargetDir, Copier copier, PrintStream console) throws IOException, InterruptedException { FilePath srcDir = selector.getSourceDirectory(src, console); if (srcDir == null) { return isOptional(); // Fail build unless copy is optional } copier.initialize(src, dst, srcDir, baseTargetDir); try { int cnt; if (!isFlatten()) cnt = copier.copyAll(srcDir, expandedFilter, expandedExcludes, targetDir, isFingerprintArtifacts()); else { targetDir.mkdirs(); // Create target if needed FilePath[] list = srcDir.list(expandedFilter, expandedExcludes, false); for (FilePath file : list) copier.copyOne(file, new FilePath(targetDir, file.getName()), isFingerprintArtifacts()); cnt = list.length; } console.println(Messages.CopyArtifact_Copied(cnt, HyperlinkNote.encodeTo('/'+ src.getParent().getUrl(), src.getParent().getFullDisplayName()), HyperlinkNote.encodeTo('/'+src.getUrl(), Integer.toString(src.getNumber())))); // Fail build if 0 files copied unless copy is optional return cnt > 0 || isOptional(); } finally { copier.end(); } } /** * Tests whether specified variable name is valid. * Package scope for testing purpose. * * @param variableName * @return true if <code>variableName</code> is valid as a variable name. */ static boolean isValidVariableName(final String variableName) { if(StringUtils.isBlank(variableName)) { return false; } // The pattern for variables are defined in hudson.Util.VARIABLE. // It's not exposed unfortunately and tests the variable // by actually expanding that. final String expected = "GOOD"; String expanded = Util.replaceMacro( String.format("${%s}", variableName), new VariableResolver<String>() { @Override public String resolve(String name) { if(variableName.equals(name)) { return expected; } return null; } } ); return expected.equals(expanded); } @Extension public static final class DescriptorImpl extends BuildStepDescriptor<Builder> { public FormValidation doCheckProjectName( @AncestorInPath Job<?,?> anc, @QueryParameter String value) { // JENKINS-32526: Check that it behaves gracefully for an unknown context if (anc == null) return FormValidation.ok(Messages.CopyArtifact_AncestorIsNull()); // Require CONFIGURE permission on this project if (!anc.hasPermission(Item.CONFIGURE)) return FormValidation.ok(); Jenkins jenkins = Jenkins.getInstance(); if (jenkins == null) { // validation is useless if Jenkins is no longer available. return FormValidation.ok(); } FormValidation result; Item item = jenkins.getItem(value, anc.getParent()); if (item != null) if (jenkins.getPlugin("maven-plugin") != null && item instanceof MavenModuleSet) { result = FormValidation.warning(Messages.CopyArtifact_MavenProject()); } else { result = (item instanceof MatrixProject) ? FormValidation.warning(Messages.CopyArtifact_MatrixProject()) : FormValidation.ok(); } else if (value.indexOf('$') >= 0) result = FormValidation.warning(Messages.CopyArtifact_ParameterizedName()); else { Job<?,?> nearest = Items.findNearest(Job.class, value, anc.getParent()); if (nearest != null) { result = FormValidation.error( hudson.tasks.Messages.BuildTrigger_NoSuchProject( value, nearest.getName())); } else { result = FormValidation.error(hudson.tasks.Messages.BuildTrigger_NoProjectSpecified()); } } return result; } public FormValidation doCheckResultVariableSuffix(@QueryParameter String value) { value = Util.fixEmptyAndTrim(value); if (value == null) { // optional field. return FormValidation.ok(); } if (!isValidVariableName(value)) { return FormValidation.error(Messages.CopyArtifact_InvalidVariableName()); } return FormValidation.ok(); } public boolean isApplicable(Class<? extends AbstractProject> clazz) { return true; } public String getDisplayName() { return Messages.CopyArtifact_DisplayName(); } } // Listen for project renames and update property here if needed. @Extension public static final class ListenerImpl extends ItemListener { @Override public void onRenamed(Item item, String oldName, String newName) { String oldFullName = Items.getCanonicalName(item.getParent(), oldName); String newFullName = Items.getCanonicalName(item.getParent(), newName); Jenkins jenkins = Jenkins.getInstance(); if (jenkins == null) { LOGGER.log(Level.SEVERE, "Jenkins instance is no longer available."); return; } for (AbstractProject<?,?> project : jenkins.getAllItems(AbstractProject.class)) { try { for (CopyArtifact ca : getCopiers(project)) { String projectName = ca.getProjectName(); if (projectName == null) { // JENKINS-27475 (not sure why this happens). continue; } String suffix = ""; // Support rename for "MatrixJobName/AxisName=value" type of name int i = projectName.indexOf('='); if (i > 0) { int end = projectName.substring(0,i).lastIndexOf('/'); suffix = projectName.substring(end); projectName = projectName.substring(0, end); } ItemGroup context = project.getParent(); String newProjectName = Items.computeRelativeNamesAfterRenaming(oldFullName, newFullName, projectName, context); if (!projectName.equals(newProjectName)) { ca.project = newProjectName + suffix; project.save(); } } } catch (IOException ex) { Logger.getLogger(ListenerImpl.class.getName()).log(Level.WARNING, "Failed to resave project " + project.getName() + " for project rename in CopyArtifact build step (" + oldName + " =>" + newName + ")", ex); } } } private static List<CopyArtifact> getCopiers(AbstractProject<?,?> project) throws IOException { List<CopyArtifact> copiers = getCopyArtifactsInProject(project); for (CopyArtifact copier : copiers) { copier.upgradeIfNecessary(project); } return copiers; } } private static class EnvAction implements EnvironmentContributingAction { // Decided not to record this data in build.xml, so marked transient: private transient Map<String,String> data = new HashMap<String,String>(); @Nullable private String calculateDefaultSuffix(@Nonnull Run<?,?> build, @Nonnull Run<?,?> src, @Nonnull String projectName) { ItemGroup<?> ctx = getItemGroup(build); Job<?,?> item = src.getParent(); // Use full name if configured with absolute path // and relative otherwise projectName = projectName.startsWith("/") ? item.getFullName() : item.getRelativeNameFrom(ctx); if (projectName == null) { // this is a case when the copying project doesn't belong to Jenkins item tree. // (e.g. promotion for Promoted Builds plugin) LOGGER.log( Level.WARNING, "Failed to calculate a relative path of {0} from {2}", new Object[] { item.getFullName(), ctx.getFullName(), } ); return null; } return projectName.toUpperCase().replaceAll("[^A-Z]+", "_"); // Only use letters and _ } private void add( @Nonnull Run<?,?> build, @Nonnull Run<?,?> src, @Nonnull String projectName, @Nullable String resultVariableSuffix ) { if (data==null) return; if (!isValidVariableName(resultVariableSuffix)) { resultVariableSuffix = calculateDefaultSuffix(build, src, projectName); if (resultVariableSuffix == null) { return; } } data.put( String.format("COPYARTIFACT_BUILD_NUMBER_%s", resultVariableSuffix), Integer.toString(src.getNumber()) ); } public void buildEnvVars(AbstractBuild<?,?> build, EnvVars env) { if (data!=null) env.putAll(data); } public String getIconFileName() { return null; } public String getDisplayName() { return null; } public String getUrlName() { return null; } } }