Skip to content

Commit

Permalink
Merge pull request #193 from abayer/jenkins-46547
Browse files Browse the repository at this point in the history
[JENKINS-46547] Allow loading pipeline {} blocks from shared libraries
  • Loading branch information
abayer committed Sep 15, 2017
2 parents e9d0fee + 6904fc1 commit 6dba039
Show file tree
Hide file tree
Showing 21 changed files with 636 additions and 32 deletions.
Expand Up @@ -2,6 +2,8 @@

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import net.sf.json.JSONArray;
import org.jenkinsci.plugins.pipeline.modeldefinition.validator.ModelValidator;

Expand All @@ -12,9 +14,11 @@
*/
public final class ModelASTStages extends ModelASTElement {
private List<ModelASTStage> stages = new ArrayList<ModelASTStage>();
private final UUID uuid;

public ModelASTStages(Object sourceLocation) {
super(sourceLocation);
this.uuid = UUID.randomUUID();
}

@Override
Expand Down Expand Up @@ -55,6 +59,10 @@ public void removeSourceLocation() {
}
}

public UUID getUuid() {
return uuid;
}

public List<ModelASTStage> getStages() {
return stages;
}
Expand Down
Expand Up @@ -47,6 +47,7 @@ import org.jenkinsci.plugins.pipeline.StageStatus
import org.jenkinsci.plugins.pipeline.StageTagsMetadata
import org.jenkinsci.plugins.pipeline.SyntheticStage
import org.jenkinsci.plugins.pipeline.modeldefinition.actions.DeclarativeJobPropertyTrackerAction
import org.jenkinsci.plugins.pipeline.modeldefinition.actions.ExecutionModelAction
import org.jenkinsci.plugins.pipeline.modeldefinition.model.Environment
import org.jenkinsci.plugins.pipeline.modeldefinition.model.StepsBlock
import org.jenkinsci.plugins.pipeline.modeldefinition.steps.CredentialWrapper
Expand Down Expand Up @@ -263,6 +264,18 @@ public class Utils {
return nodes
}

static void markExecutedStagesOnAction(CpsScript script, String astUUID) throws Exception {
WorkflowRun r = script.$build()
ExecutionModelAction action = r.getAction(ExecutionModelAction.class)
if (action != null) {
if (action.stagesUUID != null) {
throw new IllegalStateException("Only one pipeline { ... } block can be executed in a single run.")
}
action.setStagesUUID(astUUID)
r.save()
}
}

private static void markStageWithTag(String stageName, String tagName, String tagValue) {
List<FlowNode> matched = findStageFlowNodes(stageName)

Expand Down
Expand Up @@ -57,9 +57,11 @@ public class Root implements Serializable {

Libraries libraries

final String astUUID

@Whitelisted
Root(Agent agent, Stages stages, PostBuild post, Environment environment, Tools tools, Options options,
Triggers triggers, Parameters parameters, Libraries libraries) {
Triggers triggers, Parameters parameters, Libraries libraries, String astUUID) {
this.agent = agent
this.stages = stages
this.post = post
Expand All @@ -69,6 +71,7 @@ public class Root implements Serializable {
this.triggers = triggers
this.parameters = parameters
this.libraries = libraries
this.astUUID = astUUID
}

/**
Expand Down
Expand Up @@ -36,6 +36,7 @@ import org.codehaus.groovy.ast.ModuleNode
import org.codehaus.groovy.ast.expr.*
import org.codehaus.groovy.ast.stmt.*
import org.jenkinsci.plugins.pipeline.modeldefinition.DescriptorLookupCache
import org.jenkinsci.plugins.pipeline.modeldefinition.ModelStepLoader
import org.jenkinsci.plugins.pipeline.modeldefinition.Utils
import org.jenkinsci.plugins.pipeline.modeldefinition.ast.*
import org.jenkinsci.plugins.pipeline.modeldefinition.when.DeclarativeStageConditional
Expand Down Expand Up @@ -443,4 +444,37 @@ class ASTParserUtils {
static boolean isGroovyAST(ModelASTElement original) {
return original != null && original.sourceLocation != null && original.sourceLocation instanceof ASTNode
}

static boolean blockHasMethod(BlockStatement block, String methodName) {
if (block != null) {
return block.statements.any {
MethodCallExpression expr = matchMethodCall(it)
return expr != null && matchMethodName(expr) == methodName
}
} else {
return false
}
}

static boolean isDeclarativePipelineStep(Statement stmt, boolean topLevel = true) {
def b = matchBlockStatement(stmt)

if (b != null &&
b.methodName == ModelStepLoader.STEP_NAME &&
b.arguments.expressions.size() == 1) {
BlockStatement block = asBlock(b.body.code)
if (topLevel) {
// If we're in a Jenkinsfile, we want to find any pipeline block at the top-level
return block != null
} else {
// If we're in a shared library, filter out anything that doesn't have agent and stages method calls
def hasAgent = blockHasMethod(block, "agent")
def hasStages = blockHasMethod(block, "stages")
return hasAgent && hasStages
}
}

return false
}

}
Expand Up @@ -31,6 +31,9 @@ import hudson.model.Run
import jenkins.model.Jenkins
import jenkins.util.SystemProperties
import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.GroovyCodeVisitor
import org.codehaus.groovy.ast.MethodNode
import org.codehaus.groovy.ast.ModuleNode
import org.codehaus.groovy.ast.expr.*
import org.codehaus.groovy.ast.stmt.BlockStatement
Expand Down Expand Up @@ -139,16 +142,38 @@ class ModelParser implements Parser {
public @CheckForNull ModelASTPipelineDef parse(ModuleNode src, boolean secondaryRun = false) {
// first, quickly ascertain if this module should be parsed at all
def pst = src.statementBlock.statements.find {
MethodCallExpression m = matchMethodCall(it)
return m != null && matchMethodName(m) == ModelStepLoader.STEP_NAME
return isDeclarativePipelineStep(it)
}

if (pst==null) {
// Check if there's a 'pipeline' step somewhere nested within the other statements and error out if that's the case.
src.statementBlock.statements.each { checkForNestedPipelineStep(it) }
return null; // no 'pipeline', so this doesn't apply
if (pst != null) {
return parsePipelineStep(pst, secondaryRun)
} else {
// Look for the pipeline step inside methods named call.
MethodNode callMethod = src.methods.find { it.name == "call" }
if (callMethod != null) {
PipelineStepFinder finder = new PipelineStepFinder()
callMethod.code.visit(finder)
List<Statement> pipelineSteps = finder.pipelineSteps

if (!pipelineSteps.isEmpty()) {
List<ModelASTPipelineDef> pipelineDefs = pipelineSteps.collect { p ->
return parsePipelineStep(p, secondaryRun)
}
// Even if there are multiple pipeline blocks, just return the first one - this return value is only
// used in a few places: tests, where there will only ever be one, and linting/converting, which also
// are guaranteed to only have one, since we don't intend to support linting of pipelines defined in
// shared libraries.
return pipelineDefs.get(0)
}
}
}

// Check if there's a 'pipeline' step somewhere nested within the other statements and error out if that's the case.
src.statementBlock.statements.each { checkForNestedPipelineStep(it) }
return null; // no 'pipeline', so this doesn't apply
}

private @CheckForNull ModelASTPipelineDef parsePipelineStep(Statement pst, boolean secondaryRun = false) {
ModelASTPipelineDef r = new ModelASTPipelineDef(pst);

def pipelineBlock = matchBlockStatement(pst);
Expand Down Expand Up @@ -232,7 +257,7 @@ class ModelParser implements Parser {
// Only transform the pipeline {} to pipeline({ return root }) if this is being called in the compiler and there
// are no errors.
if (!secondaryRun && errorCollector.errorCount == 0) {
pipelineBlock.whole.arguments = new RuntimeASTTransformer(r).transform(build)
pipelineBlock.whole.arguments = new RuntimeASTTransformer().transform(r, build)
// Lazily evaluate prettyPrint(...) - i.e., only if AST_DEBUG_LOGGING is true.
astDebugLog {
"Transformed runtime AST: ${ -> prettyPrint(pipelineBlock.whole.arguments)}"
Expand Down
@@ -0,0 +1,43 @@
/*
* The MIT License
*
* Copyright (c) 2017, 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.parser

import org.codehaus.groovy.ast.CodeVisitorSupport
import org.codehaus.groovy.ast.stmt.ExpressionStatement
import org.codehaus.groovy.ast.stmt.Statement


class PipelineStepFinder extends CodeVisitorSupport {
List<Statement> pipelineSteps = []

@Override
void visitExpressionStatement(ExpressionStatement exprStmt) {
if (ASTParserUtils.isDeclarativePipelineStep(exprStmt, false)) {
pipelineSteps.add(exprStmt)
} else {
super.visitExpressionStatement(exprStmt)
}
}
}
Expand Up @@ -62,23 +62,27 @@ import static org.jenkinsci.plugins.pipeline.modeldefinition.parser.ASTParserUti
*/
@SuppressFBWarnings(value="SE_NO_SERIALVERSIONID")
class RuntimeASTTransformer {
private final ModelASTPipelineDef pipelineDef

RuntimeASTTransformer(@Nonnull ModelASTPipelineDef pipelineDef) {
this.pipelineDef = pipelineDef
RuntimeASTTransformer() {
}

/**
* Given a run, transform {@link #pipelineDef}, attach the {@link ModelASTStages} for {@link #pipelineDef} to the
* Given a run, transform a {@link ModelASTPipelineDef}, attach the {@link ModelASTStages} for that {@link ModelASTPipelineDef} to the
* run, and return an {@link ArgumentListExpression} containing a closure that returns the {@Root} we just created.
*/
ArgumentListExpression transform(@CheckForNull Run<?,?> run) {
ArgumentListExpression transform(@Nonnull ModelASTPipelineDef pipelineDef, @CheckForNull Run<?,?> run) {
Expression root = transformRoot(pipelineDef)
if (run != null && run.getAction(ExecutionModelAction.class) == null) {
if (run != null) {
ModelASTStages stages = pipelineDef.stages
stages.removeSourceLocation()
run.addAction(new ExecutionModelAction(stages))
ExecutionModelAction action = run.getAction(ExecutionModelAction.class)
if (action == null) {
run.addAction(new ExecutionModelAction(stages))
} else {
action.addStages(stages)
run.save()
}
}

return args(
closureX(
block(
Expand Down Expand Up @@ -606,7 +610,8 @@ class RuntimeASTTransformer {
transformOptions(original.options),
transformTriggers(original.triggers),
transformParameters(original.parameters),
transformLibraries(original.libraries)))
transformLibraries(original.libraries),
constX(original.stages.getUuid().toString())))
}

return constX(null)
Expand Down
Expand Up @@ -27,14 +27,56 @@
import hudson.model.InvisibleAction;
import org.jenkinsci.plugins.pipeline.modeldefinition.ast.ModelASTStages;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ExecutionModelAction extends InvisibleAction {
private final ModelASTStages stages;
private ModelASTStages stages;
private String stagesUUID;
private final List<ModelASTStages> stagesList = new ArrayList<>();

public ExecutionModelAction(ModelASTStages s) {
this.stages = s;
this.stagesList.add(s);
this.stages = null;
}

public ExecutionModelAction(List<ModelASTStages> s) {
this.stagesList.addAll(s);
this.stages = null;
}

protected Object readResolve() throws IOException {
if (this.stages != null) {
this.stagesList.add(stages);
this.stages = null;
}
return this;
}

public ModelASTStages getStages() {
return stages;
for (ModelASTStages s : stagesList) {
if (s.getUuid().toString().equals(stagesUUID)) {
return s;
}
}
return null;
}

public String getStagesUUID() {
return stagesUUID;
}

public void setStagesUUID(String s) {
this.stagesUUID = s;
}

public List<ModelASTStages> getStagesList() {
return Collections.unmodifiableList(stagesList);
}

public void addStages(ModelASTStages s) {
this.stagesList.add(s);
}
}

0 comments on commit 6dba039

Please sign in to comment.