/* * The MIT License * * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Tom Huybrechts * * 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.matrix; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.EnvVars; import hudson.Util; import hudson.model.Action; import hudson.model.Executor; import hudson.model.InvisibleAction; import hudson.model.Node; import hudson.model.Queue.QueueAction; import hudson.model.TaskListener; import hudson.util.AlternativeUiTextProvider; import hudson.util.DescribableList; import hudson.model.Cause; import hudson.model.CauseAction; import hudson.model.DependencyGraph; import hudson.model.Descriptor; import jenkins.model.BuildDiscarder; import jenkins.model.Jenkins; import hudson.model.Item; import hudson.model.ItemGroup; import hudson.model.JDK; import hudson.model.Label; import hudson.model.ParametersAction; import hudson.model.Project; import hudson.model.SCMedItem; import hudson.model.Queue.NonBlockingTask; import hudson.model.Cause.LegacyCodeCause; import hudson.model.Run; import hudson.scm.SCM; import jenkins.scm.SCMCheckoutStrategy; import hudson.tasks.BuildWrapper; import hudson.tasks.Builder; import hudson.tasks.LogRotator; import hudson.tasks.Publisher; import hudson.util.HttpResponses; import java.io.IOException; import java.util.Collections; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.StaplerResponse; /** * One configuration of {@link MatrixProject}. * * @author Kohsuke Kawaguchi */ public class MatrixConfiguration extends Project<MatrixConfiguration,MatrixRun> implements SCMedItem, NonBlockingTask { private static final Logger LOGGER = Logger.getLogger(MatrixConfiguration.class.getName()); /** * The actual value combination. */ private transient /*final*/ Combination combination; /** * Hash value of {@link #combination}. Cached for efficiency. */ private transient String digestName; /** * Cached label expression. * * null in case it needs to be computed, empty for no restriction. */ private transient @CheckForNull String label; public MatrixConfiguration(MatrixProject parent, Combination c) { super(parent,c.toString()); setCombination(c); } @Override public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException { // directory name is not a name for us --- it's taken from the combination name super.onLoad(parent, combination.toString()); } @Override public EnvVars getEnvironment(Node node, TaskListener listener) throws IOException, InterruptedException { EnvVars env = super.getEnvironment(node, listener); AxisList axes = getParent().getAxes(); for (Map.Entry<String,String> e : getCombination().entrySet()) { Axis a = axes.find(e.getKey()); if (a!=null) a.addBuildVariable(e.getValue(),env); // TODO: hijacking addBuildVariable but perhaps we need addEnvVar? else env.put(e.getKey(), e.getValue()); } return env; } @Override public final boolean isDisabled() { // Matrix configurations cannot be disabled independently from the master return getParent().isDisabled(); } @Override public final void makeDisabled(boolean b) throws IOException { super.makeDisabled(getParent().isDisabled()); } @Override public final boolean supportsMakeDisabled() { return false; } @Override public final HttpResponse doDisable() throws IOException, ServletException { return HttpResponses.errorWithoutStack(405, Messages.MatrixConfiguration_DisableNotAllowed()); } @Override protected void updateTransientActions(){ // This method is exactly the same as in {@link #AbstractProject}. // Enabling to call this method from MatrixProject is the only reason for overriding. super.updateTransientActions(); } @Override public boolean isConcurrentBuild() { return getParent().isConcurrentBuild(); } @Override public void setConcurrentBuild(boolean b) throws IOException { throw new UnsupportedOperationException("The setting can be only changed at MatrixProject"); } @Override public void delete() throws IOException, InterruptedException{ //do not delete active configuration if(getParent().getActiveConfigurations().contains(this)) return; super.delete(); } /** * Used during loading to set the combination back. */ /*package*/ void setCombination(Combination c) { this.combination = c; this.digestName = c.digest().substring(0,8); this.label = null; } /** * Build numbers are always synchronized with the parent. * * <p> * Computing this is bit tricky. Several considerations: * * <ol> * <li>A new configuration build #N is started while the parent build #N is building, * and when that happens we want to return N. * <li>But the configuration build #N is done before the parent build #N finishes, * and when that happens we want to return N+1 because that's going to be the next one. * <li>Configuration builds might skip some numbers if the parent build is aborted * before this configuration is built. * <li>If nothing is building right now and the last build of the parent is #N, * then we want to return N+1. * </ol> */ @Override public int getNextBuildNumber() { MatrixBuild lcb = getParent().getLastCompletedBuild(); if (lcb == null) { return 0; } int n = lcb.getNumber() + 1; MatrixRun lb = getLastBuild(); if (lb != null) { n = Math.max(n, lb.getNumber() + 1); } return n; } @Override public int assignBuildNumber() throws IOException { int nb = getNextBuildNumber(); MatrixRun r = getLastBuild(); if(r!=null && r.getNumber()>=nb) // make sure we don't schedule the same build twice throw new IllegalStateException("Build #"+nb+" is already completed"); return nb; } @Override public String getDisplayName() { return combination.toCompactString(getParent().getAxes()); } @Override public MatrixProject getParent() { return (MatrixProject)super.getParent(); } /** * Get the actual combination of the axes values for this {@link MatrixConfiguration} */ public Combination getCombination() { return combination; } /** * Since {@link MatrixConfiguration} is always invoked from {@link MatrixRun} * once and just once, there's no point in having a quiet period. */ @Override public int getQuietPeriod() { return 0; } /** * Inherit the value from the parent. */ @Override public int getScmCheckoutRetryCount() { return getParent().getScmCheckoutRetryCount(); } /** * Inherit the value from the parent. */ @Override public SCMCheckoutStrategy getScmCheckoutStrategy() { return getParent().getScmCheckoutStrategy(); } @Override public boolean isConfigurable() { return false; } @Override protected Class<MatrixRun> getBuildClass() { return MatrixRun.class; } @Override protected MatrixRun newBuild() throws IOException { final Executor executor = Executor.currentExecutor(); if (executor == null) { LOGGER.log(Level.WARNING, "New build of {0} has been started outside the executor", this); return null; } List<Action> actions = executor.getCurrentWorkUnit().context.actions; ParentBuildAction a = null; for (Action _a : actions) { if (_a instanceof ParentBuildAction) { a = (ParentBuildAction) _a; break; } } if (a == null) { LOGGER.log(Level.WARNING, "JENKINS-26582: ignoring apparent attempt to trigger {0} without its parent", getFullName()); return null; } MatrixBuild lb = a.getMatrixBuild(); if (lb == null) { // Could happen if the parent started but Jenkins was restarted while the children were still in the queue. // In this case we simply guess that the last build of the parent is what triggered this configuration. // If MatrixProject.concurrentBuild, that is not necessarily correct. lb = getParent().getLastBuild(); if (lb == null) { LOGGER.log(Level.WARNING, "cannot start a build of {0} since its parent has no builds at all", getFullName()); return null; } LOGGER.log(Level.WARNING, "guessing that the correct build of {0} is #{1}", new Object[] {getFullName(), lb.getNumber()}); } // for every MatrixRun there should be a parent MatrixBuild MatrixRun lastBuild = new MatrixRun(this, lb.getTimestamp()); lastBuild.number = lb.getNumber(); _getRuns().put(lastBuild); return lastBuild; } @Override protected void buildDependencyGraph(DependencyGraph graph) { } @Override public MatrixConfiguration asProject() { return this; } @Override public Label getAssignedLabel() { if (label == null) { label = computeAssignedLabel(); } final Jenkins jenkins = Jenkins.getInstance(); return jenkins != null ? jenkins.getLabel(Util.fixEmpty(label)) : null; } private @Nonnull String computeAssignedLabel() { // combine all the label axes by &&. StringBuilder sb = new StringBuilder(); boolean written = false; for (Axis axis : getParent().getAxes()) { if (axis instanceof LabelAxis || axis instanceof LabelExpAxis) { if (written) { sb.append("&&"); } sb.append(combination.get(axis)); written = true; } } return sb.toString(); } @Override public String getPronoun() { return AlternativeUiTextProvider.get(PRONOUN, this, Messages.MatrixConfiguration_Pronoun()); } @Override public JDK getJDK() { final Jenkins jenkins = Jenkins.getInstance(); return jenkins != null ? jenkins.getJDK(combination.get("jdk")) : null; } // // inherit build setting from the parent project // @Override public List<Builder> getBuilders() { return getParent().getBuilders(); } @Override public Map<Descriptor<Publisher>, Publisher> getPublishers() { return getParent().getPublishers(); } @Override public DescribableList<Builder, Descriptor<Builder>> getBuildersList() { return getParent().getBuildersList(); } @Override public DescribableList<Publisher, Descriptor<Publisher>> getPublishersList() { return getParent().getPublishersList(); } @Override public Map<Descriptor<BuildWrapper>, BuildWrapper> getBuildWrappers() { return getParent().getBuildWrappers(); } @Override public DescribableList<BuildWrapper, Descriptor<BuildWrapper>> getBuildWrappersList() { return getParent().getBuildWrappersList(); } @Override public Publisher getPublisher(Descriptor<Publisher> descriptor) { return getParent().getPublisher(descriptor); } @Override public BuildDiscarder getBuildDiscarder() { // TODO: LinkedLogRotator doesn't work very well in the face of pluggable BuildDiscarder but I don't know what to do BuildDiscarder bd = getParent().getBuildDiscarder(); if (bd instanceof LogRotator) { LogRotator lr = (LogRotator) bd; return new LinkedLogRotator(lr.getArtifactDaysToKeep(),lr.getArtifactNumToKeep()); } return new LinkedLogRotator(); } @Override public SCM getScm() { return getParent().getScm(); } /*package*/ String getDigestName() { return digestName; } /** * JDK cannot be set on {@link MatrixConfiguration} because * it's controlled by {@link MatrixProject}. * @deprecated * Not supported. */ @Override public void setJDK(JDK jdk) throws IOException { throw new UnsupportedOperationException(); } /** * @deprecated * Value is controlled by {@link MatrixProject}. */ @Override public void setBuildDiscarder(BuildDiscarder logRotator) { throw new UnsupportedOperationException(); } /** * Returns true if this configuration is a configuration * currently in use today (as opposed to the ones that are * there only to keep the past record.) * * @see MatrixProject#getActiveConfigurations() */ public boolean isActiveConfiguration() { return getParent().getActiveConfigurations().contains(this); } /** * On Cygwin, path names cannot be longer than 256 chars. * See http://cygwin.com/ml/cygwin/2005-04/msg00395.html and * http://www.nabble.com/Windows-Filename-too-long-errors-t3161089.html for * the background of this issue. Setting this flag to true would * cause Jenkins to use cryptic but short path name, giving more room for * jobs to use longer path names. */ @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Can be changed in the runtime by plugins and Groovy scripts") public static boolean useShortWorkspaceName = Boolean.getBoolean(MatrixConfiguration.class.getName()+".useShortWorkspaceName"); /** * @deprecated * Use {@link #scheduleBuild(ParametersAction, Cause)}. Since 1.283 */ public boolean scheduleBuild(ParametersAction parameters) { return scheduleBuild(parameters, new LegacyCodeCause()); } /** Starts the build with the ParametersAction that are passed in. * * @param parameters * Can be null. * @deprecated * Use {@link #scheduleBuild(List, Cause)}. Since 1.480 */ public boolean scheduleBuild(ParametersAction parameters, Cause c) { return scheduleBuild(Collections.singletonList(parameters), c); } /** * Starts the build with the actions that are passed in. * * @param actions Can be null. * @param c Reason for starting the build * @return true if the build has been scheduled */ public boolean scheduleBuild(List<? extends Action> actions, Cause c) { final Jenkins jenkins = Jenkins.getInstance(); if (jenkins == null) { LOGGER.log(Level.WARNING, "Cannot schedule the build {0}. Jenkins is not ready", this); return false; } List<Action> allActions = new ArrayList<Action>(); if(actions != null) { for (Action a : actions) { // SECURITY-170 if (a instanceof ParametersAction) { allActions.add(MatrixChildParametersAction.create((ParametersAction) a)); } else { allActions.add(a); } } } allActions.add(new ParentBuildAction()); allActions.add(new CauseAction(c)); return jenkins.getQueue().schedule2(this, getQuietPeriod(), allActions ).isAccepted(); } /** * */ public static class ParentBuildAction extends InvisibleAction implements QueueAction { /** * @deprecated use {@link #getMatrixBuild()} instead. */ @Deprecated public transient MatrixBuild parent; private String parentId; public ParentBuildAction() { final Executor currentExecutor = Executor.currentExecutor(); this.parent = currentExecutor != null ? (MatrixBuild)currentExecutor.getCurrentExecutable() : null; parentId = parent != null ? parent.getExternalizableId() : null; } public boolean shouldSchedule(List<Action> actions) { return true; } public MatrixBuild getMatrixBuild() { if (parent == null && parentId != null) { try { parent = (MatrixBuild)Run.fromExternalizableId(parentId); } catch (Exception e) { LOGGER.log(Level.WARNING, "Unable to retrieve parent reference", e); parent = null; } } return parent; } } // Hide /configure view inherited from Job @Restricted(DoNotUse.class) public void doConfigure(StaplerResponse rsp) throws IOException { rsp.sendError(HttpServletResponse.SC_NOT_FOUND); } }