/* * Copyright 2013 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.jenkins.plugins.delegate; import java.io.File; import java.io.IOException; import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import com.google.common.collect.ImmutableList; import hudson.FilePath; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Computer; import hudson.model.ItemGroup; import hudson.model.Node; import hudson.scm.NullSCM; import hudson.scm.SCM; import hudson.slaves.WorkspaceList; import jenkins.branch.Branch; import jenkins.branch.BranchProperty; import jenkins.branch.MultiBranchProject; import jenkins.model.Jenkins; import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMRevision; import jenkins.scm.api.SCMRevisionAction; import jenkins.scm.api.SCMSource; import jenkins.scm.impl.SingleSCMSource; /** * This new project type is intended to act as a bridge between new * {@code MultiBranchProject} types and classic {@link AbstractProject} types. * <p> * By implementing this, a "branch aware" project is enabled to act on both * {@link Branch} and {@link SCM} based workspaces. Furthermore, when this is * a container project (e.g. Literate, Matrix, DSL) the child projects can * simply use {@link DelegateSCM} to get the same source snapshot as this * container used when it triggered it. * * @param <P> The project type that derives from this type. * @param <B> The build type associated with the project type {@code P}. */ public abstract class AbstractBranchAwareProject <P extends AbstractBranchAwareProject<P, B>, B extends AbstractBuild<P, B>> extends AbstractProject<P, B> { public AbstractBranchAwareProject(ItemGroup parent, String name) throws IOException { super(parent, name); setScm(new NullSCM()); } /** {@inheritDoc} */ @Override public boolean checkout(AbstractBuild build, Launcher launcher, BuildListener listener, File changelogFile) throws IOException, InterruptedException { final FilePath workspace = _shareWorkspace(build.getBuiltOn()); if (workspace != null) { listener.getLogger().println( Messages.AbstractBranchAwareProject_SharingWorkspace()); checkState(isDelegate()); // Instead of performing a full 'checkout', just have the // DelegateSCM attach the appropriate SCMRevisionAction. ((DelegateSCM) getScm()).parentSCMFromBuild( build, true /* attach SCMRevisionAction */); return true; } boolean result = super.checkout(build, launcher, listener, changelogFile); // Attach an SCMRevisionAction to this build, so that if/when the nested // build checks out with the DelegateSCM it knows what revision to // checkout. // NOTE: If we are a delegate then DelegateSCM adds the appropriate // SCMRevisionAction for us during the above checkout call. if (build.getAction(SCMRevisionAction.class) == null) { final SCMHead head = getBranch().getHead(); final SCMSource source = getSource(); final SCMRevision revision = source.fetch(head, listener); if (revision == null) { throw new IllegalStateException("Revision action without revision"); } build.addAction(new SCMRevisionAction(revision)); } return result; } /** Sets the branch of the project, returning it for chained-setter usage. */ public P setBranch(Branch branch) throws IOException { this.branch = checkNotNull(branch); checkState(!isDelegate()); save(); // persist the state change to disk return (P) this; } /** Fetch the branch of the project. */ public Branch getBranch() { if (isDelegate()) { final ItemGroup parent = getParent(); // If we are a delegate, defer to our project context for our Branch // since it provides the complete version. // NOTE: A delegate SCM is only an option for projects contained // within an AbstractBranchAwareProject, so validate this invariant. checkState(parent instanceof AbstractBranchAwareProject); final AbstractBranchAwareProject parentProject = (AbstractBranchAwareProject) parent; return parentProject.getBranch(); } else { // If we are not a delegate, than the branch we store is accurate. return checkNotNull(branch); } } /** @see #getBranch() */ private Branch branch; /** Retrieve the {@link SCMSource} for the {@link Branch} of our project */ public SCMSource getSource() { // TODO(mattmoor): Consider using getParent(Class<T>) to fetch the // appropriately typed parent, and allow it to see through multiple layers // of container ItemGroup. final ItemGroup parent = getParent(); if (!isDelegate()) { // If we are NOT a delegate project, then if we have an SCMSource id. final String sourceId = getBranch().getSourceId(); if (sourceId != null) { // We fetch the identified SCMSource from the // containing MultiBranchProject. checkState(parent instanceof MultiBranchProject); final MultiBranchProject parentProject = (MultiBranchProject) parent; final SCMSource source = parentProject.getSCMSource(sourceId); checkState(source != null, Messages.AbstractBranchAwareProject_NoSCMSource()); return source; } else { // Otherwise, we construct a standalone SCMSource from our SCM. return new SingleSCMSource(null /* sourceId */, NAME, getScm()); } } else { // If we are a delegate project, then we inherit our SCMSource from // our context. checkState(parent instanceof AbstractBranchAwareProject); final AbstractBranchAwareProject parentProject = (AbstractBranchAwareProject) parent; return parentProject.getSource(); } } /** * @return whether this project is a delegate of some parent project * that dictates information about the {@link Branch} or {@link SCM}. */ private boolean isDelegate() { return getScm() instanceof DelegateSCM; } /** * Projects that delegate often have a largely cosmetic workspace, * based on which logic/control decisions are made, e.g. how to build * up a DSL project from its checked in code. To minimize the impact * of these read-only workspaces in the presence of composition, attempt * to share our workspace with a parent job, if it has a workspace on * the node on which we have been scheduled. This should be called from * {@code decideWorkspace}. * <p> * NOTE: public so that it is accessible to builds. * * @param onNode The Node we've been scheduled on. * @return A lease on the workspace to share, or null if no such workspace * exists on this node. */ @Nullable public WorkspaceList.Lease shareWorkspace(Node onNode) { final FilePath workspace = _shareWorkspace(onNode); return workspace == null ? null : WorkspaceList.Lease.createDummyLease(workspace); } /** @see #shareWorkspace */ @Nullable private FilePath _shareWorkspace(Node onNode) { final AbstractBranchAwareProject container = getSameNodeConstraint(); if (container == this) { return null; } final FilePath workspace = container.getSomeWorkspace(); if (workspace == null) { // We are inside of an active parent build, there must be a workspace return null; } final Node node = workspaceToNode(workspace); if (node == null) { // We are inside of an active parent build, there must be a workspace return null; } if (node != onNode) { // We were scheduled on a different node, tough luck. return null; } // If we are running on the same node as our parent, then share // its workspace. return workspace; } /** {@inheritDoce} */ @Override public AbstractBranchAwareProject getSameNodeConstraint() { if (!(this instanceof ReadOnlyWorkspaceTask)) { // We can't squat in a parent project's workspace if // we plan to mutate things return this; } if (!isDelegate()) { // We aren't necessarily executing within the same source context as a // parent project. return this; } checkState(getParent() instanceof AbstractBranchAwareProject); final AbstractBranchAwareProject container = (AbstractBranchAwareProject) getParent(); if (!(container instanceof ReadOnlyWorkspaceTask)) { // The container mutates its workspace. return this; } checkState(container != this, "Avoiding infinite recursion"); // Try to get ourselves scheduled onto the same node as our // parent, so we can share its workspace return container.getSameNodeConstraint(); } /** From GitSCM */ private static Node workspaceToNode(FilePath workspace) { Jenkins j = Jenkins.getInstance(); if (workspace.isRemote()) { for (Computer c : j.getComputers()) { if (c.getChannel() == workspace.getChannel()) { Node n = c.getNode(); if (n != null) { return n; } } } } return j; } /** {@inheritDoc} */ @Override public void setScm(SCM scm) throws IOException { // Marshall the scm into a branch. // NOTE: by passing us a DelegateSCM here, clients and turn us into an // "isDelegate()", which we disallow in "setBranch()". This is why once // we have turned the SCM into a Branch, we don't simply call "setBranch()" this.branch = new Branch(null, new SCMHead(NAME), checkNotNull(scm), ImmutableList.<BranchProperty>of()); save(); // persist the state change to disk } /** {@inheritDoc} */ @Override public SCM getScm() { // Access and return our actual SCM. As this is used for UI round-tripping, // if we were to return another SCM here, the user would see a snapshot of // the parent's SCM instead of this. The point of DelegateSCM is that we // can return it here, even if "isDelegate()" because it will do the right // thing. return branch.getScm(); } /** The name to give degenerate {@link SCMHead}s */ private static final String NAME = "name"; }