/* * 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.dsl; import static java.util.concurrent.TimeUnit.SECONDS; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.util.List; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import org.kohsuke.stapler.framework.io.LargeText; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.io.ByteStreams.copy; import com.google.common.base.Charsets; import com.google.common.collect.Lists; import com.google.common.hash.Hashing; import com.google.common.primitives.UnsignedLongs; import com.google.common.util.concurrent.Uninterruptibles; import com.google.jenkins.plugins.delegate.DelegateSCM; import com.google.jenkins.plugins.dsl.util.Binder; import hudson.FilePath; import hudson.console.ModelHyperlinkNote; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Action; import hudson.model.BuildListener; import hudson.model.Cause; import hudson.model.CauseAction; import hudson.model.Item; import hudson.model.ItemGroup; import hudson.model.Node; import hudson.model.ParameterDefinition; import hudson.model.ParameterValue; import hudson.model.ParametersAction; import hudson.model.ParametersDefinitionProperty; import hudson.model.Queue; import hudson.model.Result; import hudson.model.TopLevelItem; import hudson.model.listeners.ItemListener; import hudson.scm.NullSCM; import hudson.slaves.WorkspaceList; import jenkins.model.Jenkins; import jenkins.scm.api.SCMRevisionAction; import net.sf.json.JSONObject; import net.sf.json.JSONSerializer; /** * This records the execution of a {@link YamlProject}. * @param <T> The type of element contained * @see YamlExecution */ public class YamlBuild<T extends AbstractProject & TopLevelItem> extends AbstractBuild<YamlProject<T>, YamlBuild<T>> { /** Instantiate the build */ public YamlBuild(YamlProject<T> parent) throws IOException { super(parent); } /** Used to load the build from disk */ public YamlBuild(YamlProject<T> parent, File file) throws IOException { super(parent, file); } /** {@inheritDoc} */ @Override public void run() { execute(new YamlExecution()); } /** {@inheritDoc} */ @Override public List<Action> getActions() { YamlHistoryAction action = YamlHistoryAction.of(this); if (action == null) { return getRawActions(); } List<Action> actions = Lists.newArrayList(); final AbstractBuild build = action.getBuild(getParent()); if (build == null) { return getRawActions(); } // Delegate to the nested build. for (Action a : build.getActions()) { if (preservedAction(a)) { // Exclude actions from the child that will be preserved // from the parent. continue; } actions.add(a); } // Add in our preserved actions. for (Action a : getRawActions()) { if (preservedAction(a)) { actions.add(a); } } return actions; } /** * @return whether the given action type should be preserved from our build, * vs. extracted from our delegate build. */ private static boolean preservedAction(Action a) { return SCMRevisionAction.class.isInstance(a) || CauseAction.class.isInstance(a); } /** Get the actual actions of this build, without delegation */ public List<Action> getRawActions() { return super.getActions(); } /** * This performs the actual execution of a {@link YamlProject}. * After the workspace has been set up, this loads the DSL file, * and instantiates a project from it. It then schedules that * project and follows its execution as-if it were our own. */ protected class YamlExecution extends AbstractBuild.AbstractRunner { /** {@inheritDoc} */ @Override protected Result doRun(BuildListener listener) throws IOException, InterruptedException { final FilePath ws = checkNotNull(getWorkspace()); final YamlProject<T> parent = YamlBuild.this.getParent(); // TODO(mattmoor): Resolve variables in the yaml path? final FilePath yamlFile = ws.child(parent.getYamlPath()); if (!yamlFile.exists()) { listener.error( Messages.YamlBuild_MissingFile(parent.getYamlPath())); return Result.FAILURE; } // Write the Yaml file to the log for now. copy(yamlFile.read(), maybeLog(listener, Messages.YamlBuild_LoadedYaml())); maybeLog(listener, "\n\n"); // TODO(mattmoor): Catch pertinent exceptions to report malformed // Yaml. final JSONObject json = readToJSON(yamlFile); maybeLog(listener, Messages.YamlBuild_LoadedJson()); maybeLog(listener, json.toString()); final AbstractProject project = getOrCreateProject(json); maybeLog(listener, Messages.YamlBuild_CreatedJob( ModelHyperlinkNote.encodeTo(project, project.getName()))); ParametersAction parameters = YamlBuild.this.getAction(ParametersAction.class); if (parameters == null) { parameters = getDefaultParametersValues(project); } final CauseAction cause = new CauseAction( new Cause.UpstreamCause(YamlBuild.this)); final Queue.Item item = Queue.getInstance().schedule( project, 0, cause, parameters); if (item == null) { throw new IllegalStateException("Project not scheduled"); } listener.getLogger().println( Messages.YamlBuild_StartDelimiter(parent.getYamlPath())); try { AbstractBuild newBuild = null; do { try { // This future waits for completion, we only need it to have // started. This future also doesn't seem to properly report // cancellation. newBuild = (AbstractBuild) item.getFuture().get(1, SECONDS); } catch (CancellationException e) { return Result.ABORTED; } catch (TimeoutException e) { // Check intermittently for cancellation final Queue.Item currentItem = Queue.getInstance().getItem( item.getId()); if (currentItem instanceof Queue.LeftItem) { final Queue.LeftItem leftItem = (Queue.LeftItem) currentItem; if (leftItem.isCancelled()) { return Result.ABORTED; } final Queue.Executable executable = leftItem.getExecutable(); if (executable instanceof AbstractBuild) { newBuild = (AbstractBuild) executable; } } } } while (newBuild == null); // Attach an action so that we know what sub-project and sub-build // were executed as part of this build. YamlBuild.this.addAction(new YamlHistoryAction( project.getName(), newBuild.getNumber())); writeWholeLogTo(newBuild, listener.getLogger()); listener.getLogger().println( Messages.YamlBuild_EndDelimiter(parent.getYamlPath())); return newBuild.getResult(); } catch (ExecutionException e) { e.printStackTrace(listener.error( Messages.YamlBuild_InnerException())); return Result.FAILURE; } } /** {@inheritDoc} */ @Override protected WorkspaceList.Lease decideWorkspace(Node n, WorkspaceList wsl) throws InterruptedException, IOException { final YamlProject project = YamlBuild.this.getParent(); final WorkspaceList.Lease lease = project.shareWorkspace(n); return (lease != null) ? lease : super.decideWorkspace(n, wsl); } /** Get the default parameter values for the given delegate */ private ParametersAction getDefaultParametersValues( AbstractProject delegate) { final ParametersDefinitionProperty property = (ParametersDefinitionProperty) delegate.getProperty( ParametersDefinitionProperty.class); if (property == null) { return null; } final List<ParameterValue> result = Lists.newArrayList(); for (ParameterDefinition def : property.getParameterDefinitions()) { ParameterValue value = def.getDefaultParameterValue(); if (value != null) { result.add(value); } } return new ParametersAction(result); } /** * Pipe the log of the delegated execution through to our log. * * Modeled after Jenkins' Run's writeWholeLogTo, which is not giving us * annotations back. From the javadoc: * "If someone is still writing to the log, this method will not * return until the whole log file gets written out." */ private void writeWholeLogTo(AbstractBuild build, OutputStream out) throws IOException { long pos = 0; while (!build.getLogFile().exists() || build.getLogFile().isDirectory()) { Uninterruptibles.sleepUninterruptibly(1, SECONDS); } do { LargeText logText = new LargeText(build.getLogFile(), build.getCharset(), !build.isLogUpdated()); pos = logText.writeLogTo(pos, out); if (logText.isComplete()) { break; } Uninterruptibles.sleepUninterruptibly(1, SECONDS); } while (true); } /** * This method is used to log messages that should only show up in * verbose logs. It handles check the global flag and logging the * message if it should be logged. It also returns a stream through * which continued verbose logging may be written, which similarly will * only show up if verbose logging is enabled. */ private PrintStream maybeLog(BuildListener listener, String message) { if (YamlBuild.this.getParent().getDescriptor().isVerbose()) { listener.getLogger().println(message); return listener.getLogger(); } else { return BuildListener.NULL.getLogger(); } } /** * Determine whether a project exists for the json loaded from the DSL file. */ private AbstractProject getOrCreateProject(JSONObject json) throws IOException { final String jsonText = json.toString(); final String hash = UnsignedLongs.toString( Hashing.md5().hashString(jsonText, Charsets.UTF_8).asLong(), 16); final YamlProject<T> parent = YamlBuild.this.getParent(); final YamlHistoryAction action = YamlHistoryAction.of(YamlBuild.this.getPreviousBuild()); // First build, there is no project to re-use if (action == null) { return newProject(json, hash); } final AbstractProject lastProject = action.getProject(parent); // If the last project had the same hash, then simply re-use it if (lastProject.getName().endsWith(hash)) { return lastProject; } // If we aren't using the lastProject then we need to blow away its // workspace and that of any of its descendants. deleteWorkspaceRecursive(lastProject); // Otherwise create a new one. return newProject(json, hash); } /** Instantiate a new project from the json loaded from the DSL file */ private AbstractProject newProject(JSONObject json, String hash) throws IOException { final YamlProject<T> parent = YamlBuild.this.getParent(); final String displayName = String.format("v%04d", parent.getItems().size()); final String jobName = String.format("%s-%s", displayName, hash); final Binder binder = parent.getModule().getBinder(parent); final T project = (T) binder.bindJob(parent, jobName, json); project.setDisplayName(displayName); // Validate that the embedded project doesn't specify source control, // and instate our own DelegateSCM to inject our SCM into it. checkState(project.getScm() instanceof NullSCM, Messages.YamlBuild_DSLWithSCMError()); project.setScm(new DelegateSCM(YamlProject.class)); project.onCreatedFromScratch(); parent.addItem(project); project.save(); ItemListener.fireOnCreated(project); checkNotNull(Jenkins.getInstance()).rebuildDependencyGraph(); return project; } private void deleteWorkspaceRecursive(AbstractProject project) throws IOException { final AbstractBuild build = project.getLastBuild(); if (build != null) { final FilePath workspace = build.getWorkspace(); if (workspace != null) { try { workspace.deleteRecursive(); } catch (InterruptedException e) { throw new IOException(e); } } } if (project instanceof ItemGroup) { deleteWorkspacesRecursive((ItemGroup) project); } } private void deleteWorkspacesRecursive(ItemGroup<? extends Item> itemGroup) throws IOException { for (Item item : itemGroup.getItems()) { if (item instanceof AbstractProject) { deleteWorkspaceRecursive((AbstractProject) item); } else if (item instanceof ItemGroup) { deleteWorkspacesRecursive((ItemGroup) item); } } } /** Read the DSL file into a {@link JSONObject}. */ private JSONObject readToJSON(FilePath yamlFile) throws IOException, InterruptedException { final String freshJson = getParent().getModule().getYamlToJson().toJson(yamlFile.read()); return (JSONObject) JSONSerializer.toJSON(freshJson); } /** {@inheritDoc} */ @Override public void post2(BuildListener listener) throws IOException, InterruptedException { // See: http://javadoc.jenkins-ci.org/hudson/model/ \ // AbstractBuild.AbstractBuildExecution.html performAllBuildSteps(listener, getParent().getPublishersList(), true /* post-build processing */); // TODO(mattmoor): Why not: super.post2(listener)? } /** {@inheritDoc} */ @Override public void cleanUp(BuildListener listener) throws Exception { // See: http://javadoc.jenkins-ci.org/hudson/model/ \ // AbstractBuild.AbstractBuildExecution.html performAllBuildSteps(listener, getParent().getPublishersList(), false /* run after finalized processing */); super.cleanUp(listener); } } }