package hudson.plugins.promoted_builds.conditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import hudson.CopyOnWrite;
import hudson.EnvVars;
import hudson.Extension;
import hudson.Util;
import hudson.console.HyperlinkNote;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.AutoCompletionCandidates;
import hudson.model.Cause;
import hudson.model.Cause.UpstreamCause;
import hudson.model.Fingerprint;
import hudson.model.Fingerprint.BuildPtr;
import hudson.model.InvisibleAction;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.listeners.RunListener;
import hudson.plugins.promoted_builds.JobPropertyImpl;
import hudson.plugins.promoted_builds.PromotionBadge;
import hudson.plugins.promoted_builds.PromotionCondition;
import hudson.plugins.promoted_builds.PromotionConditionDescriptor;
import hudson.plugins.promoted_builds.PromotionProcess;
import hudson.plugins.promoted_builds.util.JenkinsHelper;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import javax.annotation.CheckForNull;
import org.kohsuke.stapler.export.Exported;
/**
* {@link PromotionCondition} that tests if certain downstream projects have passed.
*
* @author Kohsuke Kawaguchi
*/
public class DownstreamPassCondition extends PromotionCondition {
/**
* List of downstream jobs that are used as the promotion criteria.
*
* Every job has to have at least one successful build for us to promote a build.
*/
private final String jobs;
private final boolean evenIfUnstable;
public DownstreamPassCondition(String jobs) {
this(jobs, false);
}
public DownstreamPassCondition(String jobs, boolean evenIfUnstable) {
this.jobs = jobs;
this.evenIfUnstable = evenIfUnstable;
}
public String getJobs() {
return jobs;
}
public boolean isEvenIfUnstable() {
return evenIfUnstable;
}
@Override
public PromotionBadge isMet(PromotionProcess promotionProcess, AbstractBuild<?,?> build) {
Badge badge = new Badge();
PseudoDownstreamBuilds pdb = build.getAction(PseudoDownstreamBuilds.class);
EnvVars buildEnvironment = new EnvVars(build.getBuildVariables());
OUTER:
for (AbstractProject<?,?> j : getJobList(build.getProject().getParent(), buildEnvironment)) {
for( AbstractBuild<?,?> b : build.getDownstreamBuilds(j) ) {
if (!b.isBuilding()) {
Result r = b.getResult();
if ((r == Result.SUCCESS) || (evenIfUnstable && r == Result.UNSTABLE)) {
badge.add(b);
continue OUTER;
}
}
}
if (pdb!=null) {// if fingerprint doesn't have any, try the pseudo-downstream
for (AbstractBuild<?,?> b : pdb.listBuilds(j)) {
if (!b.isBuilding()) {
Result r = b.getResult();
if ((r == Result.SUCCESS) || (evenIfUnstable && r == Result.UNSTABLE)) {
badge.add(b);
continue OUTER;
}
}
}
}
// none of the builds of this job passed.
return null;
}
return badge;
}
/**
* @deprecated use {@link #getJobList(hudson.model.ItemGroup, hudson.EnvVars)}
* List of downstream jobs that we need to monitor.
*
* @return never null.
*/
@Deprecated
public List<AbstractProject<?,?>> getJobList(ItemGroup context){
return getJobList(context, null);
}
/**
*
* List of downstream jobs that we need to monitor resolving variables.
* @since 2.33
* @return never null.
*/
public List<AbstractProject<?,?>> getJobList(ItemGroup context, EnvVars buildEnvironment) {
List<AbstractProject<?,?>> r = new ArrayList<AbstractProject<?,?>>();
String expandedJobs = getExpandedJobs(jobs, buildEnvironment);
if (expandedJobs == null) return r;
for (String name : Util.tokenize(expandedJobs,",")) {
AbstractProject job = JenkinsHelper.getInstance().getItem(name.trim(), context, AbstractProject.class);
if(job!=null) r.add(job);
}
return r;
}
private static String getExpandedJobs(@CheckForNull String jobs, @CheckForNull EnvVars environment){
if (environment == null) {
return jobs;
}
if (jobs == null){
return null;
}
return environment.expand(jobs);
}
/**
* @deprecated use {@link #contains(hudson.model.ItemGroup, hudson.model.AbstractProject, hudson.EnvVars)}
*/
public boolean contains(ItemGroup ctx, AbstractProject<?,?> job){
return contains(ctx, job, null);
}
/**
* Checks if the configured jobs property contains job. Resolves jobs property first.
* @since 2.33
*/
public boolean contains(ItemGroup ctx, AbstractProject<?,?> job, EnvVars environment) {
String expandedJobs = getExpandedJobs(jobs, environment);
if (expandedJobs == null) return false;
// quick rejection test
if(!expandedJobs.contains(job.getName())) return false;
String name = job.getFullName();
for (AbstractProject<?, ?> project : getJobList(ctx)) {
if (project.getFullName().equals(name)) return true;
}
return false;
}
/**
* Short-cut for {@code getJobList().contains(job)}.
* @deprecated use {@link #contains(hudson.model.ItemGroup, hudson.model.AbstractProject)}
*/
public boolean contains(AbstractProject<?,?> job) {
return contains(Jenkins.getInstance(), job);
}
public static final class Badge extends PromotionBadge {
/**
* Downstream builds that certified this build. Should be considered read-only.
*/
@Exported
public final List<Fingerprint.BuildPtr> builds = new ArrayList<Fingerprint.BuildPtr>();
void add(AbstractBuild<?,?> b) {
builds.add(new Fingerprint.BuildPtr(b));
}
}
@Extension
public static final class DescriptorImpl extends PromotionConditionDescriptor {
public boolean isApplicable(AbstractProject<?,?> item) {
return true;
}
public String getDisplayName() {
return Messages.DownstreamPassCondition_DisplayName();
}
public PromotionCondition newInstance(StaplerRequest req, JSONObject formData) throws FormException {
return new DownstreamPassCondition(
formData.getString("jobs"), formData.getBoolean("evenIfUnstable"));
}
public AutoCompletionCandidates doAutoCompleteJobs(@QueryParameter String value, @AncestorInPath AbstractProject project) {
List<AbstractProject> downstreams = project.getDownstreamProjects();
List<Item> all = JenkinsHelper.getInstance().getItems(Item.class);
List<String> candidatesDownstreams = Lists.newArrayList();
List<String> candidatesOthers = Lists.newArrayList();
for (Item i : all) {
if(! i.hasPermission(Item.READ)) continue;
Set<String> names = Sets.newLinkedHashSet();
names.add(i.getRelativeNameFrom(project));
names.add(i.getFullName());
for(String name : names) {
if(name.startsWith(value)) {
if(downstreams.contains(i)) {
candidatesDownstreams.add(name);
}else{
candidatesOthers.add(name);
}
}
}
}
AutoCompletionCandidates candidates = new AutoCompletionCandidates();
candidates.add(candidatesDownstreams.toArray(new String[candidatesDownstreams.size()]));
if(candidatesDownstreams.size() > 0 && candidatesOthers.size() > 0) {
candidates.add("- - -");
}
// Downstream jobs might not be set when user wants to set DownstreamPassCondition.
// Better to show non-downstream candidates even if they are not downstreams at the moment.
candidates.add(candidatesOthers.toArray(new String[candidatesOthers.size()]));
return candidates;
}
}
/**
* {@link RunListener} to pick up completions of downstream builds.
*
* <p>
* This is a single instance that receives all the events everywhere in the system.
* @author Kohsuke Kawaguchi
*/
@Extension
public static final class RunListenerImpl extends RunListener<AbstractBuild<?,?>> {
public RunListenerImpl() {
super((Class)AbstractBuild.class);
}
@Override
public void onCompleted(AbstractBuild<?,?> build, TaskListener listener) {
// this is not terribly efficient,
EnvVars buildEnvironment = new EnvVars(build.getBuildVariables());
for(AbstractProject<?,?> j : JenkinsHelper.getInstance().getAllItems(AbstractProject.class)) {
boolean warned = false; // used to avoid warning for the same project more than once.
JobPropertyImpl jp = j.getProperty(JobPropertyImpl.class);
if(jp!=null) {
for (PromotionProcess p : jp.getItems()) {
boolean considerPromotion = false;
for (PromotionCondition cond : p.conditions) {
if (cond instanceof DownstreamPassCondition) {
DownstreamPassCondition dpcond = (DownstreamPassCondition) cond;
if(dpcond.contains(j.getParent(), build.getParent(), buildEnvironment)) {
considerPromotion = true;
break;
}
}
}
if(considerPromotion) {
try {
AbstractBuild<?,?> u = build.getUpstreamRelationshipBuild(j);
if (u==null) {
// if the fingerprint doesn't tell us, perhaps the cause would tell us?
final Stack<List<Cause>> stack = new Stack<List<Cause>>();
stack.push(build.getCauses());
while(!stack.isEmpty()) {
for (UpstreamCause uc : Util.filter(stack.pop(), UpstreamCause.class)) {
if (uc.getUpstreamProject().equals(j.getFullName())) {
u = j.getBuildByNumber(uc.getUpstreamBuild());
if (u!=null) {
// remember that this build is a pseudo-downstream of the discovered build.
PseudoDownstreamBuilds pdb = u.getAction(PseudoDownstreamBuilds.class);
if (pdb==null)
u.addAction(pdb=new PseudoDownstreamBuilds());
pdb.add(build);
u.save();
break;
}
}
stack.push(uc.getUpstreamCauses());
}
}
}
if (u==null) {
// no upstream build. perhaps a configuration problem?
if(build.getResult()==Result.SUCCESS && !warned) {
listener.getLogger().println("WARNING: "+j.getFullDisplayName()+" appears to use this job as a promotion criteria, " +
"but no fingerprint is recorded. Fingerprint needs to be enabled on both this job and "+j.getFullDisplayName()+". " +
"See https://wiki.jenkins-ci.org/display/JENKINS/Fingerprint for more details");
warned = true;
}
}
if(u!=null && p.considerPromotion2(u)!=null)
listener.getLogger().println("Promoted "+HyperlinkNote.encodeTo('/'+u.getUrl(),u.getFullDisplayName()));
} catch (IOException e) {
e.printStackTrace(listener.error("Failed to promote a build"));
}
}
}
}
}
}
/**
* Called whenever some {@link JobPropertyImpl} changes to update downstream jobs.
* @deprecated Caches are not being used anymore
*/
@Deprecated
public static void rebuildCache() {
// Do nothing
}
}
/**
* Remembers those downstream jobs that are not related by fingerprint but by the triggering relationship.
* This is a weaker form of the relationship and less reliable, but often people don't understand
* the notion of fingerprints, in which case this works.
*/
public static class PseudoDownstreamBuilds extends InvisibleAction {
final List<BuildPtr> builds = new ArrayList<BuildPtr>();
public void add(AbstractBuild<?,?> run) {
builds.add(new BuildPtr(run));
}
public List<AbstractBuild<?,?>> listBuilds(AbstractProject<?, ?> job) {
List<AbstractBuild<?,?>> list = new ArrayList<AbstractBuild<?,?>>();
for (BuildPtr b : builds) {
if (b.is(job)) {
Run r = b.getRun();
if (r instanceof AbstractBuild)
// mainly null check, plus a defensive measure caused by a possible rename.
list.add((AbstractBuild)r);
}
}
return list;
}
}
}