Skip to content

Commit

Permalink
[FIXED JENKINS-38153] Added synthetic stages using TagsAction.
Browse files Browse the repository at this point in the history
This is downstream of jenkinsci/workflow-api-plugin#24
  • Loading branch information
abayer committed Nov 4, 2016
1 parent eb197a9 commit f88d2cc
Show file tree
Hide file tree
Showing 11 changed files with 320 additions and 40 deletions.
2 changes: 1 addition & 1 deletion pipeline-model-api/pom.xml
Expand Up @@ -27,7 +27,7 @@
<parent>
<groupId>org.jenkinsci.plugins</groupId>
<artifactId>pipeline-model-parent</artifactId>
<version>0.6-SNAPSHOT</version>
<version>0.6-synthetic-stage-SNAPSHOT</version> <!-- Revert back to 0.6-SNAPSHOT after merge -->
</parent>

<groupId>org.jenkinsci.plugins</groupId>
Expand Down
2 changes: 1 addition & 1 deletion pipeline-model-declarative-agent/pom.xml
Expand Up @@ -27,7 +27,7 @@
<parent>
<groupId>org.jenkinsci.plugins</groupId>
<artifactId>pipeline-model-parent</artifactId>
<version>0.6-SNAPSHOT</version>
<version>0.6-synthetic-stage-SNAPSHOT</version> <!-- Revert back to 0.6-SNAPSHOT after merge -->
</parent>

<groupId>org.jenkinsci.plugins</groupId>
Expand Down
7 changes: 6 additions & 1 deletion pipeline-model-definition/pom.xml
Expand Up @@ -27,7 +27,7 @@
<parent>
<groupId>org.jenkinsci.plugins</groupId>
<artifactId>pipeline-model-parent</artifactId>
<version>0.6-SNAPSHOT</version>
<version>0.6-synthetic-stage-SNAPSHOT</version> <!-- Revert back to 0.6-SNAPSHOT after merge -->
</parent>

<groupId>org.jenkinsci.plugins</groupId>
Expand Down Expand Up @@ -66,6 +66,11 @@
<artifactId>workflow-cps</artifactId>
<version>2.18</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-api</artifactId>
<version>2.6-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-job</artifactId>
Expand Down
@@ -0,0 +1,51 @@
/*
* The MIT License
*
* Copyright (c) 2016, CloudBees, Inc.
*
* 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 org.jenkinsci.plugins.pipeline.modeldefinition


public class SyntheticStage {
public static final String SYNTHETIC_STAGE_TAG = "SYNTHETIC_STAGE"

public static final String SYNTHETIC_PRE = "PRE"

public static final String SYNTHETIC_POST = "POST"

public static String checkout() {
return "Checkout SCM"
}

public static String agentSetup() {
return "Agent Setup"
}

public static String toolInstall() {
return "Tool Install"
}

public static String postBuild() {
return "Post Build Actions"
}
}
Expand Up @@ -24,6 +24,7 @@

package org.jenkinsci.plugins.pipeline.modeldefinition

import com.google.common.base.Predicate
import com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader
import com.google.common.cache.LoadingCache;
Expand All @@ -40,7 +41,13 @@ import org.jenkinsci.plugins.pipeline.modeldefinition.parser.Converter
import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted
import org.jenkinsci.plugins.structs.SymbolLookup
import org.jenkinsci.plugins.structs.describable.UninstantiatedDescribable
import org.jenkinsci.plugins.workflow.actions.StageAction
import org.jenkinsci.plugins.workflow.actions.TagsAction
import org.jenkinsci.plugins.workflow.cps.CpsFlowExecution
import org.jenkinsci.plugins.workflow.cps.CpsScript
import org.jenkinsci.plugins.workflow.cps.CpsThread
import org.jenkinsci.plugins.workflow.graph.FlowNode
import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner
import org.jenkinsci.plugins.workflow.job.WorkflowRun

import java.lang.reflect.ParameterizedType
Expand Down Expand Up @@ -198,6 +205,42 @@ public class Utils {
r.addAction(new ExecutionModelAction(stages))
}

static boolean withinAStage() {
CpsThread thread = CpsThread.current()
CpsFlowExecution execution = thread.execution

DepthFirstScanner scanner = new DepthFirstScanner();

FlowNode stageNode = scanner.findFirstMatch(execution, new Predicate<FlowNode>() {
@Override
public boolean apply(FlowNode input) {
return input.getAction(StageAction.class) != null
}
})

return stageNode == null
}

/**
* Marks the containing stage with this name as a synthetic stage, with the appropriate context.
*
* @param stageName
* @param context
*/
static void markSyntheticStage(String stageName, String context) {
CpsThread thread = CpsThread.current()
CpsFlowExecution execution = thread.execution

FlowNode currentNode = execution.currentHeads.find { n ->
n?.displayName?.equals(stageName)
}

if (currentNode.getAction(TagsAction.class) == null) {
currentNode.actions.add(new TagsAction())
}
currentNode.getAction(TagsAction.class).addTag(SyntheticStage.SYNTHETIC_STAGE_TAG, context)
}

/**
* Creates and sets the loading for a cache of {@link Describable}s descending from the given descriptor type.
*
Expand Down
Expand Up @@ -82,37 +82,40 @@ public class ModelInterpreter implements Serializable {
// Entire build, including notifications, runs in the withEnv.
withEnvBlock(root.getEnvVars()) {
inWrappers(root.wrappers) {
// Stage execution and post-build actions run in try/catch blocks, so we still run post-build actions
// even if the build fails, and we still send notifications if the build and/or post-build actions fail.
// We save the caught error, if any, for throwing at the end of the build.
inDeclarativeAgent(root.agent) {
toolsBlock(root.agent, root.tools) {
// If we have an agent and script.scm isn't null, run checkout scm
if (root.agent.hasAgent() && Utils.hasScmContext(script)) {
script.checkout script.scm
// Stage execution and post-build actions run in try/catch blocks, so we still run post-build actions
// even if the build fails, and we still send notifications if the build and/or post-build actions fail.
// We save the caught error, if any, for throwing at the end of the build.
inDeclarativeAgent(root.agent) {
toolsBlock(root.agent, root.tools) {
// If we have an agent and script.scm isn't null, run checkout scm
if (root.agent.hasAgent() && Utils.hasScmContext(script)) {
script.stage(SyntheticStage.checkout()) {
Utils.markSyntheticStage(SyntheticStage.checkout(), SyntheticStage.SYNTHETIC_PRE)
script.checkout script.scm
}
}

for (int i = 0; i < root.stages.getStages().size(); i++) {
Stage thisStage = root.stages.getStages().get(i)

script.stage(thisStage.name) {
withEnvBlock(thisStage.getEnvVars()) {
if (firstError == null) {
inDeclarativeAgent(thisStage.agent) {
toolsBlock(thisStage.agent ?: root.agent, thisStage.tools) {
try {
catchRequiredContextForNode(root.agent) {
setUpDelegate(thisStage.steps.closure).call()
}.call()
} catch (Exception e) {
script.echo "Error in stages execution: ${e.getMessage()}"
script.getProperty("currentBuild").result = Result.FAILURE
if (firstError == null) {
firstError = e
}
} finally {
// And finally, run the post stage steps.
List<Closure> postClosures = thisStage.satisfiedPostStageConditions(root, script.getProperty("currentBuild"))
script.stage(thisStage.name) {
withEnvBlock(thisStage.getEnvVars()) {
if (firstError == null) {
inDeclarativeAgent(thisStage.agent) {
toolsBlock(thisStage.agent ?: root.agent, thisStage.tools) {
try {
catchRequiredContextForNode(root.agent) {
setUpDelegate(thisStage.steps.closure).call()
}.call()
} catch (Exception e) {
script.echo "Error in stages execution: ${e.getMessage()}"
script.getProperty("currentBuild").result = Result.FAILURE
if (firstError == null) {
firstError = e
}
} finally {
// And finally, run the post stage steps.
List<Closure> postClosures = thisStage.satisfiedPostStageConditions(root, script.getProperty("currentBuild"))
catchRequiredContextForNode(thisStage.agent != null ? thisStage.agent : root.agent, false) {
if (postClosures.size() > 0) {
script.echo("Post stage")
Expand Down Expand Up @@ -142,7 +145,8 @@ public class ModelInterpreter implements Serializable {
catchRequiredContextForNode(root.agent) {
List<Closure> postBuildClosures = root.satisfiedPostBuilds(script.getProperty("currentBuild"))
if (postBuildClosures.size() > 0) {
script.stage("Post Build Actions") {
script.stage(SyntheticStage.postBuild()) {
Utils.markSyntheticStage(SyntheticStage.postBuild(), SyntheticStage.SYNTHETIC_POST)
for (int i = 0; i < postBuildClosures.size(); i++) {
setUpDelegate(postBuildClosures.get(i)).call()
}
Expand Down Expand Up @@ -235,16 +239,14 @@ public class ModelInterpreter implements Serializable {
if (agent.hasAgent() && tools != null) {
def toolEnv = []
def toolsList = tools.getToolEntries()
for (int i = 0; i < toolsList.size(); i++) {
def entry = toolsList.get(i)
String k = entry.get(0)
String v= entry.get(1)

String toolPath = script.tool(name:v, type:Tools.typeForKey(k))

toolEnv.addAll(script.envVarsForTool(toolId: Tools.typeForKey(k), toolVersion: v))
if (!Utils.withinAStage()) {
script.stage(SyntheticStage.toolInstall()) {
Utils.markSyntheticStage(SyntheticStage.toolInstall(), SyntheticStage.SYNTHETIC_PRE)
toolEnv = actualToolsInstall(toolsList)
}
} else {
toolEnv = actualToolsInstall(toolsList)
}

return {
script.withEnv(toolEnv) {
body.call()
Expand All @@ -257,6 +259,22 @@ public class ModelInterpreter implements Serializable {
}
}


def actualToolsInstall(List<List<Object>> toolsList) {
def toolEnv = []
for (int i = 0; i < toolsList.size(); i++) {
def entry = toolsList.get(i)
String k = entry.get(0)
String v = entry.get(1)

String toolPath = script.tool(name: v, type: Tools.typeForKey(k))

toolEnv.addAll(script.envVarsForTool(toolId: Tools.typeForKey(k), toolVersion: v))
}

return toolEnv
}

def inDeclarativeAgent(Agent agent, Closure body) {
if (agent == null) {
return {
Expand Down
Expand Up @@ -25,6 +25,8 @@

package org.jenkinsci.plugins.pipeline.modeldefinition.agent.impl

import org.jenkinsci.plugins.pipeline.modeldefinition.SyntheticStage
import org.jenkinsci.plugins.pipeline.modeldefinition.Utils
import org.jenkinsci.plugins.pipeline.modeldefinition.agent.DeclarativeAgent
import org.jenkinsci.plugins.pipeline.modeldefinition.agent.DeclarativeAgentScript
import org.jenkinsci.plugins.workflow.cps.CpsScript
Expand All @@ -43,6 +45,12 @@ public class DockerPipelineScript extends DeclarativeAgentScript {
}
LabelScript labelScript = (LabelScript) Label.DescriptorImpl.instanceForName("label", [label: targetLabel]).getScript(script)
return labelScript.run {
if (!Utils.withinAStage()) {
script.stage(SyntheticStage.agentSetup()) {
Utils.markSyntheticStage(SyntheticStage.toolInstall(), SyntheticStage.SYNTHETIC_PRE)
script.getProperty("docker").image(declarativeAgent.docker).pull()
}
}
script.getProperty("docker").image(declarativeAgent.docker).inside(declarativeAgent.dockerArgs, {
body.call()
})
Expand Down
Expand Up @@ -23,6 +23,7 @@
*/
package org.jenkinsci.plugins.pipeline.modeldefinition;

import com.google.common.base.Predicate;
import hudson.model.Result;
import hudson.slaves.DumbSlave;
import hudson.slaves.EnvironmentVariablesNodeProperty;
Expand All @@ -36,11 +37,16 @@
import org.jenkinsci.plugins.pipeline.modeldefinition.ast.ModelASTStages;
import org.jenkinsci.plugins.pipeline.modeldefinition.ast.ModelASTStep;
import org.jenkinsci.plugins.pipeline.modeldefinition.ast.ModelASTTreeStep;
import org.jenkinsci.plugins.workflow.actions.TagsAction;
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.graphanalysis.DepthFirstScanner;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.junit.BeforeClass;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;

import java.util.Collection;
import java.util.List;

import static org.junit.Assert.assertEquals;
Expand Down Expand Up @@ -295,4 +301,69 @@ public void globalLibrarySuccess() throws Exception {
j.assertLogContains("running inside closure2", b);

}

@Test
public void syntheticStages() throws Exception {
prepRepoWithJenkinsfile("syntheticStages");

WorkflowRun b = getAndStartBuild();
j.assertBuildStatusSuccess(j.waitForCompletion(b));

j.assertLogContains("[Pipeline] { (Tool Install)", b);
j.assertLogContains("[Pipeline] { (Checkout SCM)", b);
j.assertLogContains("[Pipeline] { (foo)", b);
j.assertLogContains("hello", b);
j.assertLogContains("[Pipeline] { (Post Build Actions)", b);
j.assertLogContains("I AM A POST-BUILD", b);

FlowExecution execution = b.getExecution();

Collection<FlowNode> heads = execution.getCurrentHeads();

DepthFirstScanner scanner = new DepthFirstScanner();

assertNotNull(scanner.findFirstMatch(heads, null, syntheticStagePredicate(SyntheticStage.toolInstall(), SyntheticStage.SYNTHETIC_PRE)));
assertNotNull(scanner.findFirstMatch(heads, null, syntheticStagePredicate(SyntheticStage.checkout(), SyntheticStage.SYNTHETIC_PRE)));
assertNotNull(scanner.findFirstMatch(heads, null, syntheticStagePredicate(SyntheticStage.postBuild(), SyntheticStage.SYNTHETIC_POST)));
assertNull(scanner.findFirstMatch(heads, null, syntheticStagePredicate(SyntheticStage.agentSetup(), SyntheticStage.SYNTHETIC_PRE)));
}

@Test
public void noToolSyntheticStage() throws Exception {
prepRepoWithJenkinsfile("noToolSyntheticStage");

WorkflowRun b = getAndStartBuild();
j.assertBuildStatusSuccess(j.waitForCompletion(b));

j.assertLogContains("[Pipeline] { (Checkout SCM)", b);
j.assertLogContains("[Pipeline] { (foo)", b);
j.assertLogContains("hello", b);
j.assertLogContains("[Pipeline] { (Post Build Actions)", b);
j.assertLogContains("I AM A POST-BUILD", b);

FlowExecution execution = b.getExecution();

Collection<FlowNode> heads = execution.getCurrentHeads();

DepthFirstScanner scanner = new DepthFirstScanner();

assertNull(scanner.findFirstMatch(heads, null, syntheticStagePredicate(SyntheticStage.toolInstall(), SyntheticStage.SYNTHETIC_PRE)));
assertNotNull(scanner.findFirstMatch(heads, null, syntheticStagePredicate(SyntheticStage.checkout(), SyntheticStage.SYNTHETIC_PRE)));
assertNotNull(scanner.findFirstMatch(heads, null, syntheticStagePredicate(SyntheticStage.postBuild(), SyntheticStage.SYNTHETIC_POST)));
assertNull(scanner.findFirstMatch(heads, null, syntheticStagePredicate(SyntheticStage.agentSetup(), SyntheticStage.SYNTHETIC_PRE)));
}

private Predicate<FlowNode> syntheticStagePredicate(final String stageName,
final String context) {
return new Predicate<FlowNode>() {
@Override
public boolean apply(FlowNode input) {
return input.getDisplayName().equals(stageName) &&
input.getAction(TagsAction.class) != null &&
input.getAction(TagsAction.class).getTagValue(SyntheticStage.SYNTHETIC_STAGE_TAG) != null &&
input.getAction(TagsAction.class).getTagValue(SyntheticStage.SYNTHETIC_STAGE_TAG).equals(context);
}
};
}

}

0 comments on commit f88d2cc

Please sign in to comment.