package hudson.plugins.promoted_builds; import hudson.EnvVars; import hudson.FilePath; import hudson.console.ConsoleLogFilter; import hudson.console.HyperlinkNote; import hudson.model.Action; import hudson.model.BuildListener; import hudson.model.BuildableItemWithBuildWrappers; import hudson.model.StreamBuildListener; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Cause.UserCause; import hudson.model.Cause.UserIdCause; import hudson.model.Environment; import hudson.model.Node; import hudson.model.ParameterDefinition; import hudson.model.ParametersAction; import hudson.model.ParameterValue; import hudson.model.Result; import hudson.model.TaskListener; import hudson.model.TopLevelItem; import hudson.model.Run; import hudson.model.User; import hudson.plugins.promoted_builds.conditions.ManualCondition; import hudson.plugins.promoted_builds.util.JenkinsHelper; import hudson.security.Permission; import hudson.security.PermissionGroup; import hudson.security.PermissionScope; import hudson.slaves.WorkspaceList; import hudson.slaves.WorkspaceList.Lease; import hudson.tasks.BuildStep; import hudson.tasks.BuildWrapper; import hudson.tasks.BuildTrigger; import jenkins.model.Jenkins; import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.export.Exported; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map.Entry; import java.util.TimeZone; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; /** * Records a promotion process. * * @author Kohsuke Kawaguchi */ public class Promotion extends AbstractBuild<PromotionProcess,Promotion> { public Promotion(PromotionProcess job) throws IOException { super(job); } public Promotion(PromotionProcess job, Calendar timestamp) { super(job, timestamp); } public Promotion(PromotionProcess project, File buildDir) throws IOException { super(project, buildDir); } /** * Gets the build that this promotion promoted. * * @return * null if there's no such object. For example, if the build has already garbage collected. */ @Exported public AbstractBuild<?,?> getTarget() { PromotionTargetAction pta = getAction(PromotionTargetAction.class); return pta.resolve(this); } @Override public AbstractBuild<?,?> getRootBuild() { return getTarget().getRootBuild(); } @Override public String getUrl() { return getTarget().getUrl() + "promotion/" + getParent().getName() + "/promotionBuild/" + getNumber() + "/"; } /** * Gets the {@link Status} object that keeps track of what {@link Promotion}s are * performed for a build, including this {@link Promotion}. */ public Status getStatus() { return getTarget().getAction(PromotedBuildAction.class).getPromotion(getParent().getName()); } @Override public EnvVars getEnvironment(TaskListener listener) throws IOException, InterruptedException { EnvVars e = super.getEnvironment(listener); // Augment environment with target build's information String rootUrl = JenkinsHelper.getInstance().getRootUrl(); AbstractBuild<?, ?> target = getTarget(); if(rootUrl!=null) e.put("PROMOTED_URL",rootUrl+target.getUrl()); e.put("PROMOTED_JOB_NAME", target.getParent().getName()); e.put("PROMOTED_JOB_FULL_NAME", target.getParent().getFullName()); e.put("PROMOTED_NUMBER", Integer.toString(target.getNumber())); e.put("PROMOTED_ID", target.getId()); GlobalBuildPromotedBuilds globalBuildPromotedBuilds = GlobalBuildPromotedBuilds.get(); String dateFormat = globalBuildPromotedBuilds.getDateFormat(); String timeZone = globalBuildPromotedBuilds.getTimeZone(); SimpleDateFormat format = null; TimeZone tz = null; if (dateFormat != null && !StringUtils.isBlank(dateFormat)) { try { format = new SimpleDateFormat(dateFormat); } catch (IllegalArgumentException e1) { LOGGER.log(Level.WARNING, String.format("An illegal date format was introduced: %s. Default ISO 8601 yyyy-MM-dd'T'HH:mmZ will be used", dateFormat), e1); format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mmZ"); } } else { // Per ISO 8601 format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mmZ"); } if (timeZone !=null && !StringUtils.isBlank(timeZone)) { try { tz = TimeZone.getTimeZone(timeZone); } catch (IllegalArgumentException e2) { LOGGER.log(Level.WARNING, String.format("An illegal time zone was introduced: %s. Default GMT time zone will be used", timeZone), e2); tz = TimeZone.getTimeZone("GMT"); } } else { tz = TimeZone.getTimeZone("GMT"); } format.setTimeZone(tz); e.put("PROMOTED_TIMESTAMP", format.format(new Date())); e.put("PROMOTED_DISPLAY_NAME", target.getDisplayName()); e.put("PROMOTED_USER_NAME", getUserName()); e.put("PROMOTED_USER_ID", getUserId()); EnvVars envScm = new EnvVars(); target.getProject().getScm().buildEnvVars( target, envScm ); for ( Entry<String, String> entry : envScm.entrySet() ) { e.put( "PROMOTED_" + entry.getKey(), entry.getValue() ); } // Allow the promotion status to contribute to build environment getStatus().buildEnvVars(this, e); return e; } /** * Get a user name of the person, who triggered the promotion. * The method tries various sources like {@link UserIdCause} or {@link ManualCondition.Badge}. * @return user's name who triggered the promotion, or 'anonymous' if the search fails */ @Nonnull public String getUserName() { // Deprecated, but we still want to support it in order to maintain the compatiibility final UserCause userCause = getCause(UserCause.class); final String nameFromUserCause = userCause != null ? userCause.getUserName() : null; if (nameFromUserCause != null) { return nameFromUserCause; } // Modern UserIdCause final UserIdCause userIdCause = getCause(UserIdCause.class); final String nameFromUserIdCause = userIdCause != null ? userIdCause.getUserName() : null; if (nameFromUserIdCause != null) { return nameFromUserIdCause; } //fallback to badge lookup for compatibility for (PromotionBadge badget : getStatus().getBadges()) { if (badget instanceof ManualCondition.Badge) { final String nameFromBadge = ((ManualCondition.Badge) badget).getUserName(); if (!nameFromBadge.equals(ManualCondition.MISSING_USER_ID_DISPLAY_STRING)) { return nameFromBadge; } } } return Jenkins.ANONYMOUS.getName(); } /** * Gets ID of the {@link User}, who triggered the promotion. * The method tries various sources like {@link UserIdCause} or {@link ManualCondition.Badge}. * @return ID of the user who triggered the promotion. * If the search fails, returns ID of {@link User#getUnknown()}. * @since 2.22 */ @Nonnull public String getUserId() { // Deprecated, but we still want to support it in order to maintain the compatiibility // We try to convert the cause to the user ID by using a search by the full name, not reliable final UserCause userCause = getCause(UserCause.class); final String nameFromUserCause = userCause != null ? userCause.getUserName(): null; final User user = nameFromUserCause != null ? User.get(nameFromUserCause, false, null) : null; if (user != null) { return user.getId(); } // Modern UserIdCause final UserIdCause userIdCause = getCause(UserIdCause.class); final String idFromUserIdCause = userIdCause != null ? userIdCause.getUserId(): null; if (idFromUserIdCause != null) { return idFromUserIdCause; } //fallback to badge lookup for compatibility for (PromotionBadge badget : getStatus().getBadges()) { if (badget instanceof ManualCondition.Badge) { final String idFromBadge = ((ManualCondition.Badge) badget).getUserId(); if (!idFromBadge.equals(ManualCondition.MISSING_USER_ID_DISPLAY_STRING)) { return idFromBadge; } } } return User.getUnknown().getId(); } public List<ParameterValue> getParameterValues(){ List<ParameterValue> values=new ArrayList<ParameterValue>(); ParametersAction parametersAction=getParametersActions(this); if (parametersAction!=null){ ManualCondition manualCondition=(ManualCondition) getProject().getPromotionCondition(ManualCondition.class.getName()); if (manualCondition!=null){ for (ParameterValue pvalue:parametersAction.getParameters()){ if (manualCondition.getParameterDefinition(pvalue.getName())!=null){ values.add(pvalue); } } } return values; } //fallback to badge lookup for compatibility for (PromotionBadge badget:getStatus().getBadges()){ if (badget instanceof ManualCondition.Badge){ return ((ManualCondition.Badge) badget).getParameterValues(); } } return Collections.emptyList(); } /** * Gets parameter definitions from the {@link ManualCondition}. * @return List of parameter definitions to be presented. * May be empty if there is no {@link ManualCondition}. */ @Nonnull public List<ParameterDefinition> getParameterDefinitionsWithValue(){ List<ParameterDefinition> definitions=new ArrayList<ParameterDefinition>(); ManualCondition manualCondition=(ManualCondition) getProject().getPromotionCondition(ManualCondition.class.getName()); if (manualCondition == null) { return definitions; } for (ParameterValue pvalue:getParameterValues()){ ParameterDefinition pdef=manualCondition.getParameterDefinition(pvalue.getName()); if (pdef == null) { // We cannot do anything with such missing definitions. // May happen only in the case of the wrong form submission continue; } definitions.add(pdef.copyWithDefaultValue(pvalue)); } return definitions; } public void run() { getStatus().addPromotionAttempt(this); run(new RunnerImpl(this)); } protected class RunnerImpl extends AbstractRunner { final Promotion promotionRun; RunnerImpl(final Promotion promotionRun) { this.promotionRun = promotionRun; } @Override protected Lease decideWorkspace(Node n, WorkspaceList wsl) throws InterruptedException, IOException { String customWorkspace = Promotion.this.getProject().getCustomWorkspace(); if (customWorkspace != null) { final FilePath rootPath = n.getRootPath(); if (rootPath == null) { throw new IOException("Cannot retrieve the root path of the node " + n); } // we allow custom workspaces to be concurrently used between jobs. return Lease.createDummyLease( rootPath.child(getEnvironment(listener).expand(customWorkspace))); } TopLevelItem item = (TopLevelItem)getTarget().getProject(); FilePath workspace = n.getWorkspaceFor(item); if (workspace == null) { throw new IOException("Cannot retrieve workspace for " + item + " on the node " + n); } return wsl.allocate(workspace, promotionRun); } protected Result doRun(BuildListener listener) throws Exception { AbstractBuild<?, ?> target = getTarget(); OutputStream logger = listener.getLogger(); AbstractProject rootProject = project.getRootProject(); // Global log filters for (ConsoleLogFilter filter : ConsoleLogFilter.all()) { logger = filter.decorateLogger(target, logger); } // Project specific log filters if (rootProject instanceof BuildableItemWithBuildWrappers) { BuildableItemWithBuildWrappers biwbw = (BuildableItemWithBuildWrappers) rootProject; for (BuildWrapper bw : biwbw.getBuildWrappersList()) { logger = bw.decorateLogger(target, logger); } } listener = new StreamBuildListener(logger); listener.getLogger().println( Messages.Promotion_RunnerImpl_Promoting( HyperlinkNote.encodeTo('/' + target.getUrl(), target.getFullDisplayName()) ) ); // start with SUCCESS, unless someone makes it a failure setResult(Result.SUCCESS); if(!preBuild(listener,project.getBuildSteps())) return Result.FAILURE; try { List<ParameterValue> params=getParameterValues(); if (params!=null){ for(ParameterValue value : params) { BuildWrapper wrapper=value.createBuildWrapper(Promotion.this); if (wrapper!=null){ Environment e = wrapper.setUp(Promotion.this, launcher, listener); if(e==null) return Result.FAILURE; buildEnvironments.add(e); } } } if(!build(listener,project.getBuildSteps(),target)) return Result.FAILURE; return null; } finally { boolean failed = false; for(int i = buildEnvironments.size()-1; i >= 0; i--) { if (!buildEnvironments.get(i).tearDown(Promotion.this,listener)) { failed=true; } } if(failed) return Result.FAILURE; } } protected void post2(BuildListener listener) throws Exception { if(getResult()== Result.SUCCESS) getStatus().onSuccessfulPromotion(Promotion.this); // persist the updated build record getTarget().save(); if (getResult() == Result.SUCCESS) { // we should evaluate any other pending promotions in case // they had a condition on this promotion PromotedBuildAction pba = getTarget().getAction(PromotedBuildAction.class); for (PromotionProcess pp : pba.getPendingPromotions()) { pp.considerPromotion2(getTarget()); } // tickle PromotionTriggers for (AbstractProject<?,?> p : JenkinsHelper.getInstance().getAllItems(AbstractProject.class)) { PromotionTrigger pt = p.getTrigger(PromotionTrigger.class); if (pt!=null) pt.consider(Promotion.this); } } } private boolean build(final BuildListener listener, final List<BuildStep> steps, final Run promotedBuild) throws IOException, InterruptedException { for( BuildStep bs : steps ) { if ( bs instanceof BuildTrigger) { BuildTrigger bt = (BuildTrigger)bs; for(AbstractProject p : bt.getChildProjects()) { listener.getLogger().println( Messages.Promotion_RunnerImpl_SchedulingBuild( HyperlinkNote.encodeTo('/' + p.getUrl(), p.getDisplayName()) ) ); p.scheduleBuild(0, new PromotionCause(promotionRun, promotedBuild)); } } else if(!bs.perform(Promotion.this, launcher, listener)) { listener.getLogger().println("failed build " + bs + " " + getResult()); return false; } else { listener.getLogger().println("build " + bs + " " + getResult()); } } return true; } private boolean preBuild(BuildListener listener, List<BuildStep> steps) { for( BuildStep bs : steps ) { if(!bs.prebuild(Promotion.this,listener)) { listener.getLogger().println("failed pre build " + bs + " " + getResult()); return false; } } return true; } } public static final PermissionGroup PERMISSIONS = new PermissionGroup(Promotion.class, Messages._Promotion_Permissions_Title()); public static final Permission PROMOTE = new Permission(PERMISSIONS, "Promote", Messages._Promotion_PromotePermission_Description(), Jenkins.ADMINISTER, PermissionScope.RUN); @Override public int hashCode() { return this.getId().hashCode(); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final Promotion other = (Promotion) obj; return this.getId().equals(other.getId()); } /** * Factory method for creating {@link ParametersAction} * @param parameters * @return */ public static ParametersAction createParametersAction(List<ParameterValue> parameters){ return new ParametersAction(parameters); } public static ParametersAction getParametersActions(Promotion build){ return build.getAction(ParametersAction.class); } /** * Combine the target build parameters with the promotion build parameters * @param actions * @param build * @param promotionParams * @deprecated Use {@link PromotionParametersAction} with constructor instead. */ @Deprecated public static void buildParametersAction(@Nonnull List<Action> actions, @Nonnull AbstractBuild<?, ?> build, @CheckForNull List<ParameterValue> promotionParams) { // Create list of actions to pass to scheduled build actions.add(PromotionParametersAction.buildFor(build, promotionParams)); } private static final Logger LOGGER = Logger.getLogger(Promotion.class.getName()); /** * Action, which stores promotion parameters. * This class allows defining custom parameters filtering logic, which is * important for versions after the SECURITY-170 fix. * @since TODO */ @Restricted(NoExternalUse.class) public static class PromotionParametersAction extends ParametersAction { private List<ParameterValue> unfilteredParameters; private PromotionParametersAction(List<ParameterValue> params) { // Pass the parameters upstairs super(params); unfilteredParameters = params; } @Override public List<ParameterValue> getParameters() { return Collections.unmodifiableList(filter(unfilteredParameters)); } private List<ParameterValue> filter(List<ParameterValue> params) { // buildToBePromoted::getParameters() invokes the secured method, hence all // parameters from the promoted build are safe. return params; } public static PromotionParametersAction buildFor( @Nonnull AbstractBuild<?, ?> buildToBePromoted, @CheckForNull List<ParameterValue> promotionParams) { if (promotionParams == null) { promotionParams = new ArrayList<ParameterValue>(); } List<ParameterValue> params = new ArrayList<ParameterValue>(); //Add the target build parameters first, if the same parameter is not being provided by the promotion build List<ParametersAction> parameters = buildToBePromoted.getActions(ParametersAction.class); for (ParametersAction paramAction : parameters) { for (ParameterValue pvalue : paramAction.getParameters()) { if (!promotionParams.contains(pvalue)) { params.add(pvalue); } } } //Add all the promotion build parameters params.addAll(promotionParams); // Create list of actions to pass to scheduled build return new PromotionParametersAction(params); } } }