package hudson.plugins.promoted_builds;
import antlr.ANTLRException;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.BulkChange;
import hudson.EnvVars;
import hudson.Extension;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.AutoCompletionCandidates;
import hudson.model.ParameterValue;
import hudson.model.Cause;
import hudson.model.Cause.UserCause;
import hudson.model.DependencyGraph;
import hudson.model.Describable;
import hudson.model.Descriptor;
import hudson.model.Descriptor.FormException;
import hudson.model.Failure;
import hudson.model.FreeStyleProject;
import hudson.model.ItemGroup;
import hudson.model.JDK;
import hudson.model.Job;
import hudson.model.Label;
import hudson.model.ParameterDefinition;
import hudson.model.ParametersAction;
import hudson.model.ParametersDefinitionProperty;
import hudson.model.PermalinkProjectAction.Permalink;
import hudson.model.Queue.Item;
import hudson.model.Run;
import hudson.model.Saveable;
import hudson.model.StringParameterValue;
import hudson.model.labels.LabelAtom;
import hudson.model.labels.LabelExpression;
import hudson.plugins.promoted_builds.conditions.ManualCondition.ManualApproval;
import hudson.plugins.promoted_builds.util.JenkinsHelper;
import hudson.security.ACL;
import hudson.tasks.BuildStep;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Builder;
import hudson.tasks.Publisher;
import hudson.util.DescribableList;
import hudson.util.FormValidation;
import jenkins.model.Jenkins;
import jenkins.util.TimeDuration;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
/**
* A dummy {@link AbstractProject} to carry out promotion operations.
*
* @author Kohsuke Kawaguchi
*/
public final class PromotionProcess extends AbstractProject<PromotionProcess,Promotion> implements Saveable, Describable<PromotionProcess> {
/**
* {@link PromotionCondition}s. All have to be met for a build to be promoted.
*/
public final DescribableList<PromotionCondition,PromotionConditionDescriptor> conditions =
new DescribableList<PromotionCondition, PromotionConditionDescriptor>(this);
/**
* The icon that represents this promotion process. This is the name of
* the GIF icon that can be found in ${rootURL}/plugin/promoted-builds/icons/16x16/
* and ${rootURL}/plugin/promoted-builds/icons/32x32/, e.g. <code>"star-gold"</code>.
*/
public String icon;
/**
* The label that promotion process can be run on.
*/
public String assignedLabel;
/**
* Tells if this promotion should be hidden.
*/
public String isVisible;
private List<BuildStep> buildSteps = new ArrayList<BuildStep>();
/*package*/ PromotionProcess(JobPropertyImpl property, String name) {
super(property, name);
}
/*package*/ PromotionProcess(ItemGroup parent, String name) {
super(parent, name);
}
/**
* Creates unconnected {@link PromotionProcess} instance from the JSON configuration.
* This is mostly only useful for capturing its configuration in XML format.
* @param req Request
* @param o JSON object with source data
* @throws FormException form submission issue, includes form validation
* @throws IOException {@link PromotionProcess} creation issue
* @return Parsed promotion process
*/
public static PromotionProcess fromJson(StaplerRequest req, JSONObject o) throws FormException, IOException {
String name = o.getString("name");
try {
Jenkins.checkGoodName(name);
} catch (Failure f) {
throw new Descriptor.FormException(f.getMessage(), name);
}
PromotionProcess p = new PromotionProcess(null,name);
BulkChange bc = new BulkChange(p);
try {
p.configure(req, o); // apply configuration. prevent it from trying to save to disk while we do this
} finally {
bc.abort();
}
return p;
}
@Override
public void doSetName(String name) {
super.doSetName(name);
}
/*package*/ void configure(StaplerRequest req, JSONObject c) throws Descriptor.FormException, IOException {
// apply configuration
conditions.rebuild(req,c.optJSONObject("conditions"), PromotionCondition.all());
buildSteps = (List)Descriptor.newInstancesFromHeteroList(
req, c, "buildStep", (List) PromotionProcess.getAll());
icon = c.getString("icon");
if (c.optBoolean("hasAssignedLabel")) {
assignedLabel = Util.fixEmptyAndTrim(c.optString("assignedLabelString"));
} else {
assignedLabel = null;
}
isVisible = c.getString("isVisible");
save();
}
/**
* Returns the root project value.
*
* @return the root project value.
*/
@Override
public AbstractProject getRootProject() {
return getParent().getOwner().getRootProject();
}
@Override
public JobPropertyImpl getParent() {
return (JobPropertyImpl)super.getParent();
}
/**
* Gets the owner {@link AbstractProject} that configured {@link JobPropertyImpl} as
* a job property.
* @return Current owner project
*/
public AbstractProject<?,?> getOwner() {
return getParent().getOwner();
}
@Override public ACL getACL() {
return getOwner().getACL();
}
/**
* JENKINS-27716: Since 1.585, the promotion must explicitly indicate that
* it can be disabled. Otherwise, promotions which trigger automatically
* upon build completion will execute, even if they're archived.
*/
@Override public boolean supportsMakeDisabled() {
return true;
}
/**
* Get the promotion condition by referencing it fully qualified class name
* @param promotionClassName Class name of {@link Promotion}
* @return Promotion condition if exists
*/
@CheckForNull
public PromotionCondition getPromotionCondition(String promotionClassName) {
for (PromotionCondition condition : conditions) {
if (condition.getClass().getName().equals(promotionClassName)) {
return condition;
}
}
return null;
}
public DescribableList<Publisher, Descriptor<Publisher>> getPublishersList() {
// TODO: extract from the buildsSteps field? Or should I separate builders and publishers?
return new DescribableList<Publisher,Descriptor<Publisher>>(this);
}
protected Class<Promotion> getBuildClass() {
return Promotion.class;
}
public List<BuildStep> getBuildSteps() {
return buildSteps;
}
/**
* Gets the textual representation of the assigned label as it was entered by the user.
* @return Assigned label string
*/
@Override
public String getAssignedLabelString() {
if (assignedLabel == null) return null;
try {
LabelExpression.parseExpression(assignedLabel);
return assignedLabel;
} catch (ANTLRException e) {
// must be old label or host name that includes whitespace or other unsafe chars
return LabelAtom.escape(assignedLabel);
}
}
@Override public Label getAssignedLabel() {
// Really would like to run on the exact node that the promoted build ran on,
// not just the same label.. but at least this works if job is tied to one node:
if (assignedLabel == null) return getOwner().getAssignedLabel();
return JenkinsHelper.getInstance().getLabel(assignedLabel);
}
@Override public JDK getJDK() {
return getOwner().getJDK();
}
/**
* Gets the customWorkspace of the owner project.
*
* Support for FreeStyleProject only.
* @return customWorkspace
*/
@CheckForNull
public String getCustomWorkspace() {
AbstractProject<?, ?> p = getOwner();
if (p instanceof FreeStyleProject)
return ((FreeStyleProject) p).getCustomWorkspace();
return null;
}
/**
* Get the icon name, without the extension. It will always return a non null
* and non empty string, as <code>"star-gold"</code> is used for compatibility
* for older promotions configurations.
*
* @return the icon name
*/
public String getIcon() {
return getIcon(icon);
}
public String getIsVisible(){
return isVisible;
}
public boolean isVisible(){
if (isVisible == null) return true;
AbstractProject<?, ?> job = getOwner();
if (job == null) return true;
String expandedIsVisible = isVisible;
EnvVars environment = getDefaultParameterValuesAsEnvVars(job);
if (environment != null){
expandedIsVisible = environment.expand(expandedIsVisible);
}
if (expandedIsVisible == null){
return true;
}
if (expandedIsVisible.toLowerCase().equals("false")){
return false;
}
return true;
}
private static EnvVars getDefaultParameterValuesAsEnvVars(AbstractProject owner) {
EnvVars envVars = null;
ParametersDefinitionProperty parametersDefinitionProperty = (ParametersDefinitionProperty)owner.getProperty(ParametersDefinitionProperty.class);
if (parametersDefinitionProperty!=null){
envVars = new EnvVars();
for (ParameterDefinition parameterDefinition: parametersDefinitionProperty.getParameterDefinitions()){
ParameterValue defaultParameterValue = parameterDefinition.getDefaultParameterValue();
if (defaultParameterValue!=null){
if (defaultParameterValue instanceof StringParameterValue){
envVars.put(parameterDefinition.getName(), ((StringParameterValue)defaultParameterValue).value);
}
}
}
EnvVars.resolve(envVars);
}
return envVars;
}
/**
* Handle compatibility with pre-1.8 configs.
*
* @param sIcon
* the name of the icon used by this promotion; if null or empty,
* we return the gold icon for compatibility with previous releases
* @return the icon file name for this promotion
*/
@Nonnull
private static String getIcon(@CheckForNull String sIcon) {
if ((sIcon == null) || sIcon.equals(""))
return "star-gold";
else
return sIcon;
}
/**
* Get the badges of conditions that were passed for this promotion for the build
* @param build The build to be checked
* @return List of generated promotion badges
*/
@Nonnull
public List<PromotionBadge> getMetQualifications(AbstractBuild<?,?> build) {
List<PromotionBadge> badges = new ArrayList<PromotionBadge>();
for (PromotionCondition cond : conditions) {
PromotionBadge b = cond.isMet(this, build);
if (b != null)
badges.add(b);
}
return badges;
}
/**
* Get the conditions that have not been met for this promotion for the build
* @param build Build to be checked
* @return List of unmet promotion conditions
*/
@Nonnull
public List<PromotionCondition> getUnmetConditions(AbstractBuild<?,?> build) {
List<PromotionCondition> unmetConditions = new ArrayList<PromotionCondition>();
for (PromotionCondition cond : conditions) {
if (cond.isMet(this, build) == null)
unmetConditions.add(cond);
}
return unmetConditions;
}
/**
* Checks if all the conditions to promote a build is met.
*
* @param build Build to be checked
* @return
* {@code null} if promotion conditions are not met.
* otherwise returns a list of badges that record how the promotion happened.
*/
@CheckForNull
public Status isMet(AbstractBuild<?,?> build) {
List<PromotionBadge> badges = new ArrayList<PromotionBadge>();
for (PromotionCondition cond : conditions) {
PromotionBadge b = cond.isMet(this, build);
if(b==null)
return null;
badges.add(b);
}
return new Status(this,badges);
}
/**
* @deprecated
* Use {@link #considerPromotion2(AbstractBuild)}
*/
@Deprecated
public boolean considerPromotion(AbstractBuild<?,?> build) throws IOException {
return considerPromotion2(build)!=null;
}
/**
* Checks if the build is promotable, and if so, promote it.
*
* @param build Build to be promoted
* @return
* {@code null} if the build was not promoted, otherwise Future that kicks in when the build is completed.
* @throws IOException
*/
@CheckForNull
public Future<Promotion> considerPromotion2(AbstractBuild<?, ?> build) throws IOException {
LOGGER.fine("Considering the promotion of "+build+" via "+getName()+" without parmeters");
// If the build has manual approvals, use the parameters from it
List<ParameterValue> params = new ArrayList<ParameterValue>();
List<ManualApproval> approvals = build.getActions(ManualApproval.class);
for (ManualApproval approval : approvals) {
if (approval.name.equals(getName())) {
LOGGER.fine("Getting parameters from existing manual promotion");
params = approval.badge.getParameterValues();
LOGGER.finer("Using paramters: "+params.toString());
}
}
return considerPromotion2(build, params);
}
@CheckForNull
public Future<Promotion> considerPromotion2(AbstractBuild<?,?> build, List<ParameterValue> params) throws IOException {
if (!isActive())
return null; // not active
PromotedBuildAction a = build.getAction(PromotedBuildAction.class);
// if it's already promoted, no need to do anything.
if(a!=null && a.contains(this))
return null;
LOGGER.fine("Considering the promotion of "+build+" via "+getName()+" with parameters");
Status qualification = isMet(build);
if(qualification==null)
return null; // not this time
LOGGER.fine("Promotion condition of "+build+" is met: "+qualification);
Future<Promotion> f = promote2(build, new UserCause(), qualification, params); // TODO: define promotion cause
if (f==null)
LOGGER.warning(build+" qualifies for a promotion but the queueing failed.");
return f;
}
public void promote(AbstractBuild<?,?> build, Cause cause, PromotionBadge... badges) throws IOException {
promote2(build,cause,new Status(this,Arrays.asList(badges)));
}
/**
* @deprecated
* Use {@link #promote2(AbstractBuild, Cause, Status)}
*/
public void promote(AbstractBuild<?,?> build, Cause cause, Status qualification) throws IOException {
promote2(build,cause,qualification);
}
/**
* Promote the given build by using the given qualification.
*
* @param build Build to promote
* @param cause Why the build is promoted?
* @param qualification Initial promotion status
* @return Future to track the completion of the promotion.
* @throws IOException Promotion failure
*/
public Future<Promotion> promote2(AbstractBuild<?,?> build, Cause cause, Status qualification) throws IOException {
return promote2(build, cause, qualification, null);
}
/**
* Promote the given build by using the given qualification.
*
* @param build Build to promote
* @param cause Why the build is promoted?
* @param qualification Initial promotion status
* @param params Promotion parameters
* @return Future to track the completion of the promotion.
* @throws IOException Promotion failure
*/
public Future<Promotion> promote2(AbstractBuild<?,?> build, Cause cause, Status qualification, List<ParameterValue> params) throws IOException {
PromotedBuildAction a = build.getAction(PromotedBuildAction.class);
// build is qualified for a promotion.
if(a!=null) {
a.add(qualification);
} else {
build.addAction(new PromotedBuildAction(build,qualification));
build.save();
}
// schedule promotion activity.
return scheduleBuild2(build,cause, params);
}
/**
* @deprecated
* You need to be using {@link #scheduleBuild(AbstractBuild)}
*/
@Deprecated
public boolean scheduleBuild() {
return super.scheduleBuild();
}
public boolean scheduleBuild(@Nonnull AbstractBuild<?,?> build) {
return scheduleBuild(build,new UserCause());
}
/**
* @param build Target build
* @param cause Promotion cause
* @return {@code true} if scheduling is successful
* @deprecated
* Use {@link #scheduleBuild2(AbstractBuild, Cause)}
*/
@Deprecated
public boolean scheduleBuild(@Nonnull AbstractBuild<?,?> build, @Nonnull Cause cause) {
return scheduleBuild2(build,cause)!=null;
}
/**
* Schedules the promotion.
* @param build Target build
* @param cause Promotion cause
* @param params Parameters to be passed
* @return Future result or {@code null} if the promotion cannot be scheduled
*/
@CheckForNull
public Future<Promotion> scheduleBuild2(@Nonnull AbstractBuild<?,?> build,
Cause cause, @CheckForNull List<ParameterValue> params) {
List<Action> actions = new ArrayList<Action>();
actions.add(Promotion.PromotionParametersAction.buildFor(build, params));
actions.add(new PromotionTargetAction(build));
// remember what build we are promoting
return super.scheduleBuild2(0, cause, actions.toArray(new Action[actions.size()]));
}
@Override
public void doBuild(StaplerRequest req, StaplerResponse rsp, @QueryParameter TimeDuration delay) throws IOException, ServletException {
throw HttpResponses.error(404, "Promotion processes may not be built directly");
}
public Future<Promotion> scheduleBuild2(@Nonnull AbstractBuild<?,?> build, @Nonnull Cause cause) {
return scheduleBuild2(build, cause, null);
}
public boolean isInQueue(@Nonnull AbstractBuild<?,?> build) {
for (Item item : JenkinsHelper.getInstance().getQueue().getItems(this))
if (item.getAction(PromotionTargetAction.class).resolve(this)==build)
return true;
return false;
}
//
// these are dummy implementations to implement abstract methods.
// need to think about what the implications are.
//
@Override
public boolean isFingerprintConfigured() {
return false;
}
@Override
protected void buildDependencyGraph(DependencyGraph graph) {
throw new UnsupportedOperationException();
}
public static List<Descriptor<? extends BuildStep>> getAll() {
List<Descriptor<? extends BuildStep>> list = new ArrayList<Descriptor<? extends BuildStep>>();
addTo(Builder.all(), list);
addTo(Publisher.all(), list);
return list;
}
private static void addTo(List<? extends Descriptor<? extends BuildStep>> source, List<Descriptor<? extends BuildStep>> list) {
for (Descriptor<? extends BuildStep> d : source) {
if (d instanceof BuildStepDescriptor) {
BuildStepDescriptor bsd = (BuildStepDescriptor) d;
if(bsd.isApplicable(PromotionProcess.class))
list.add(d);
}
}
}
public Permalink asPermalink() {
return new Permalink() {
@Override
public String getDisplayName() {
return Messages.PromotionProcess_PermalinkDisplayName(PromotionProcess.this.getDisplayName());
}
@Override
public String getId() {
return PromotionProcess.this.getName();
}
@Override
public Run<?, ?> resolve(Job<?, ?> job) {
String id = getId();
for( Run<?,?> build : job.getBuilds() ) {
PromotedBuildAction a = build.getAction(PromotedBuildAction.class);
if(a!=null && a.contains(id))
return build;
}
return null;
}
};
}
public DescriptorImpl getDescriptor() {
return (DescriptorImpl)JenkinsHelper.getInstance().getDescriptorOrDie(getClass());
}
@Override
public String getShortUrl() {
// Must be overridden since JobPropertyImpl.getUrlChildPrefix is "" not "process" as you might expect (also see e50f0f5 in 1.519)
return "process/" + Util.rawEncode(getName()) + '/';
}
public boolean isActive() {
return !isDisabled();
}
@Extension
public static class DescriptorImpl extends Descriptor<PromotionProcess> {
@Override
public String getDisplayName() {
return "Promotion Process";
}
public FormValidation doCheckLabelString(@QueryParameter String value) {
if (Util.fixEmpty(value)==null)
return FormValidation.ok(); // nothing typed yet
try {
Label.parseExpression(value);
} catch (ANTLRException e) {
return FormValidation.error(e,
Messages.JobPropertyImpl_LabelString_InvalidBooleanExpression(e.getMessage()));
}
// TODO: if there's an atom in the expression that is empty, report it
if (JenkinsHelper.getInstance().getLabel(value).isEmpty())
return FormValidation.warning(Messages.JobPropertyImpl_LabelString_NoMatch());
return FormValidation.ok();
}
public AutoCompletionCandidates doAutoCompleteAssignedLabelString(@QueryParameter String value) {
AutoCompletionCandidates c = new AutoCompletionCandidates();
Set<Label> labels = JenkinsHelper.getInstance().getLabels();
List<String> queries = new AutoCompleteSeeder(value).getSeeds();
for (String term : queries) {
for (Label l : labels) {
if (l.getName().startsWith(term)) {
c.add(l.getName());
}
}
}
return c;
}
/**
* Utility class for taking the current input value and computing a list
* of potential terms to match against the list of defined labels.
*/
static class AutoCompleteSeeder {
private String source;
AutoCompleteSeeder(String source) {
this.source = source;
}
List<String> getSeeds() {
ArrayList<String> terms = new ArrayList();
boolean trailingQuote = source.endsWith("\"");
boolean leadingQuote = source.startsWith("\"");
boolean trailingSpace = source.endsWith(" ");
if (trailingQuote || (trailingSpace && !leadingQuote)) {
terms.add("");
} else {
if (leadingQuote) {
int quote = source.lastIndexOf('"');
if (quote == 0) {
terms.add(source.substring(1));
} else {
terms.add("");
}
} else {
int space = source.lastIndexOf(' ');
if (space > -1) {
terms.add(source.substring(space + 1));
} else {
terms.add(source);
}
}
}
return terms;
}
}
// exposed for Jelly
public List<PromotionConditionDescriptor> getApplicableConditions(AbstractProject<?,?> p) {
return p==null ? PromotionCondition.all() : PromotionCondition.getApplicableTriggers(p);
}
public List<PromotionConditionDescriptor> getApplicableConditions(Object context) {
return PromotionCondition.all();
}
// exposed for Jelly
public List<Descriptor<? extends BuildStep>> getApplicableBuildSteps() {
return PromotionProcess.getAll();
}
@SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD", justification = "exposed for Jelly")
public final Class<PromotionProcess> promotionProcessType = PromotionProcess.class;
public FormValidation doCheckName(@QueryParameter String name) {
name = Util.fixEmptyAndTrim(name);
if (name == null) {
return FormValidation.error(Messages.JobPropertyImpl_ValidateRequired());
}
try {
Jenkins.checkGoodName(name);
} catch (Failure f) {
return FormValidation.error(f.getMessage());
}
return FormValidation.ok();
}
}
private static final Logger LOGGER = Logger.getLogger(PromotionProcess.class.getName());
public Future<Promotion> considerPromotion2(AbstractBuild<?, ?> build, ManualApproval approval) throws IOException {
return considerPromotion2(build, approval.badge.getParameterValues());
}
}