/* * 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 java.util.Map; import javax.annotation.Nullable; import org.kohsuke.stapler.DataBoundConstructor; import org.xml.sax.SAXException; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Cause; import hudson.model.CauseAction; import hudson.model.ItemGroup; import hudson.model.TaskListener; import hudson.scm.ChangeLogParser; import hudson.scm.ChangeLogSet; import hudson.scm.PollingResult; import hudson.scm.SCM; import hudson.scm.SCMDescriptor; import hudson.scm.SCMRevisionState; import jenkins.model.Jenkins; import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMRevision; import jenkins.scm.api.SCMRevisionAction; import jenkins.scm.api.SCMSource; /** * This {@link SCM} is intended to be used by items contained within and * launched by an {@link AbstractBranchAwareProject}. This {@link SCM} * delegates to the {@link SCM} of the containing * {@link AbstractBranchAwareProject}, acting at the revision of its upstream * build that caused this build to execute. * * @param <T> The type of container project from which we are inheriting our SCM */ public class DelegateSCM<T extends AbstractBranchAwareProject & ItemGroup> extends SCM { public DelegateSCM(Class<T> clazz) { this.clazz = checkNotNull(clazz); } /** * The concrete type of the project that will be delegating to us. * * NOTE: Solely used for dynamic validation, only exceptional flow * is gated on the value of this field. */ private final Class<T> clazz; @DataBoundConstructor public DelegateSCM(String clazz) throws ClassNotFoundException { this((Class<T>) checkNotNull(Jenkins.getInstance()) .getPluginManager() .uberClassLoader .loadClass(clazz)); } /** {@inheritDoc} */ @Override public void buildEnvVars(AbstractBuild<?, ?> build, Map<String, String> env) { parentSCMFromBuild(build, false /* attach action */) .buildEnvVars(build, env); } /** {@inheritDoc} */ @Override protected PollingResult compareRemoteRevisionWith(AbstractProject project, Launcher launcher, FilePath workspace, TaskListener listener, SCMRevisionState baseline) { // NOTE: As we delegate the public API surface to another SCM, we don't // expect this protected API to be reachable. throw new UnsupportedOperationException(); } /** {@inheritDoc} */ @Override public SCMRevisionState calcRevisionsFromBuild(AbstractBuild build, Launcher launcher, TaskListener listener) throws IOException, InterruptedException { return parentSCMFromBuild(build, false /* attach action */) .calcRevisionsFromBuild(build, launcher, listener); } /** {@inheritDoc} */ @Override public boolean checkout(AbstractBuild build, Launcher launcher, FilePath remoteDir, BuildListener listener, File changeLogFile) throws IOException, InterruptedException { return parentSCMFromBuild(build, true /* attach action */) .checkout(build, launcher, remoteDir, listener, changeLogFile); } /** {@inheritDoc} */ @Override public ChangeLogParser createChangeLogParser() { return new ChangeLogParser() { @Override public ChangeLogSet<? extends ChangeLogSet.Entry> parse( AbstractBuild build, File changelogFile) throws IOException, SAXException { return parentSCMFromBuild(build, false /* attach action */) .createChangeLogParser().parse(build, changelogFile); } }; } /** * Walk the ancestors of the current project to identify from which * project we inherit our {@link SCM}. * * @param project The project that is consuming this {@link DelegateSCM} * @return The project from which to inherit our actual {@link SCM} */ private T getParentProject(AbstractProject project) { // We expect this SCM to be shared by 1 or more layers beneath a project // matching our clazz, from which we inherit our source context. // NOTE: multiple layers are possible with a matrix job, for example. checkArgument(this == project.getScm()); // Some configuration, e.g. MatrixProject/MatrixConfiguration // have several layers of project that we need to walk through // get to the real container. Walk through all of the projects // that share this SCM to find the AbstractBranchAwareProject // that contains this and assigned this sub-project the DelegateSCM. AbstractProject cursor = project; do { ItemGroup parent = cursor.getParent(); // We are searching for a project, so at any point in time our // container must remain a project. Validate the cast. checkState(parent instanceof AbstractProject); cursor = (AbstractProject) parent; } while (this == cursor.getScm()); // Validate that the container we ultimately find matches our // expected container type before casting and returning it. checkState(clazz.isInstance(cursor)); return clazz.cast(cursor); } /** * Potentially recursively walk the upstream builds until we find one that * originates from {@code parentProject}. * * @param parentProject The containing project from which we inherit our * actual {@link SCM} * @param build The build we are tracing back to its possible origin at * {@code parentProject}. * @return The build of {@code parentProject} that (possibly transititvely) * caused {@code build}. */ private AbstractBuild getParentBuild(T parentProject, AbstractBuild build) { for (final CauseAction action : build.getActions(CauseAction.class)) { for (final Cause cause : action.getCauses()) { if (!(cause instanceof Cause.UpstreamCause)) { continue; } final Cause.UpstreamCause upstreamCause = (Cause.UpstreamCause) cause; // If our parent project caused this build, then return its build that // triggered this. final String upstreamProjectName = parentProject.getFullName(); final AbstractProject causeProject = ((AbstractProject) checkNotNull(Jenkins.getInstance()) .getItemByFullName(upstreamCause.getUpstreamProject())); if (causeProject == null) { throw new IllegalStateException( "Unable to lookup upstream cause project"); } AbstractBuild causeBuild = causeProject.getBuildByNumber( upstreamCause.getUpstreamBuild()); if (upstreamCause.getUpstreamProject().equals(upstreamProjectName)) { return causeBuild; } // Otherwise, see if the build that triggered our build was triggered by // our parent project (transitively) causeBuild = getParentBuild(parentProject, causeBuild); if (causeBuild != null) { return causeBuild; } } } throw new IllegalStateException(Messages.DelegateSCM_NoParentBuild()); } /** * Fetch a changeset-bound SCM for actions like checking out code. * <p> * NOTE: Modeled after Literate Build plugin's "checkout" methods * * @param build The active build for which we need an actual {@link SCM} * @param attachAction Whether to attach the SCMRevisionAction from the * parent build to this build. * @return An {@link SCM} derived from our parent {@code T} project, but * additionally bound to the changeset at which our originating build ran. */ SCM parentSCMFromBuild(AbstractBuild build, boolean attachAction) { T parentProject = getParentProject(build.getProject()); // Using actions is unreliable if there are nested projects through // which we must see (e.g. Matrix), or if we want am abstraction // where children are free to trigger other children, all tracked by a // single parent build (no way to easily attach "parent" actions). final AbstractBuild parentBuild = getParentBuild(parentProject, build); // The parent project must attach this action with its revision state // prior to delegation for this to know what changeset at which to build. final SCMRevisionAction hashAction = parentBuild.getAction(SCMRevisionAction.class); checkState(hashAction != null, Messages.DelegateSCM_NoRevision()); if (attachAction) { build.addAction(hashAction); } // SCMSource is a sort of SCM-factory. Get it for our // AbstractBranchAwareProject and use it to construct an SCM at the // appropriate revision. final SCMSource source = parentProject.getSource(); final SCMRevision revisionHash = hashAction.getRevision(); final SCMHead head = revisionHash.getHead(); return source.build(head, revisionHash); } /** Boilerplate extension code */ @Extension public static class DescriptorImpl extends SCMDescriptor { public DescriptorImpl() { super(null /* browser */); } /** {@inheritDoc} */ @Override public boolean isApplicable(AbstractProject project) { return getContainer(project) != null; } /** * Surface the parent class with which this plugin should be instantiated. */ @Nullable public String getClassName(AbstractProject project) { checkNotNull(project); final AbstractBranchAwareProject parent = getContainer(project); return (parent != null) ? parent.getClass().getName() : null; } /** * Find the nearest {@link AbstractBranchAwareProject} containing the * project to which the candidate project is descended, or null. */ @Nullable private AbstractBranchAwareProject getContainer( AbstractProject project) { // Determine whether we are a child of an AbstractBranchAwareProject AbstractProject cursor = project; do { ItemGroup parent = cursor.getParent(); if (!(parent instanceof AbstractProject)) { return null; } cursor = (AbstractProject) parent; } while (!(cursor instanceof AbstractBranchAwareProject)); return (AbstractBranchAwareProject) cursor; } /** {@inheritDoc} */ @Override public String getDisplayName() { return Messages.DelegateSCM_DisplayName(); } } }