Skip to content

Commit

Permalink
[JENKINS-46547] Allow loading pipeline {} blocks from shared libraries
Browse files Browse the repository at this point in the history
This is a work-in-progress - more is needed to be sure that we don't
parse/validate/transform things we shouldn't, etc
  • Loading branch information
abayer committed Sep 12, 2017
1 parent 0596156 commit cbdb67a
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 9 deletions.
Expand Up @@ -31,6 +31,8 @@ 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.MethodNode
import org.codehaus.groovy.ast.ModuleNode
import org.codehaus.groovy.ast.expr.*
import org.codehaus.groovy.ast.stmt.BlockStatement
Expand Down Expand Up @@ -119,6 +121,33 @@ class ModelParser implements Parser {
}
}

private 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
}
}

private boolean isDeclarativePipelineStep(Statement stmt) {
def b = matchBlockStatement(stmt)
if (b != null &&
b.methodName == ModelStepLoader.STEP_NAME &&
b.arguments.expressions.size() == 1) {
BlockStatement block = asBlock(b.body.code)

// 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
}

public void checkForNestedPipelineStep(Statement statement) {
def b = matchBlockStatement(statement)
if (b != null) {
Expand All @@ -144,6 +173,16 @@ class ModelParser implements Parser {
}

if (pst==null) {
// Look for the pipeline step inside methods named call.
MethodNode callMethod = src.methods.find { it.name == "call" }
if (callMethod != null) {
pst = asBlock(callMethod.code).statements.find { s ->
return isDeclarativePipelineStep(s)
}
}
}

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
Expand Down
Expand Up @@ -22,6 +22,11 @@
*/
@Extension
public class GroovyShellDecoratorImpl extends GroovyShellDecorator {
@Override
public GroovyShellDecorator forTrusted() {
return this;
}

@Override
public void configureCompiler(@CheckForNull final CpsFlowExecution execution, CompilerConfiguration cc) {
ImportCustomizer ic = new ImportCustomizer();
Expand All @@ -33,15 +38,9 @@ public void configureCompiler(@CheckForNull final CpsFlowExecution execution, Co
cc.addCompilationCustomizers(new CompilationCustomizer(CompilePhase.SEMANTIC_ANALYSIS) {
@Override
public void call(SourceUnit source, GeneratorContext context, ClassNode classNode) throws CompilationFailedException {
// TODO: workflow-cps-plugin CpsFlowExecution.parseScript() should be passing in CodeSource
// to help us determine that that is a user-written script
// Commenting out for findbugs
// CodeSource cs = classNode.getCompileUnit().getCodeSource();
// if (<< cs comes from Jenkinsfile>>) {

// but until that happens,
// Using getNameWithoutPackage to make sure we don't end up executing without parsing when an inadvertent package name is put in the Jenkinsfile.
if (classNode.getNameWithoutPackage().equals(Converter.PIPELINE_SCRIPT_NAME)) {
// Only look for pipeline blocks in classes without package names - i.e., in vars and Jenkinsfiles. It's
// theoretically possible to do src/Foo.groovy as well, but we'll deal with that later.
if (classNode.getPackageName() == null) {
new ModelParser(source, execution).parse();
}
}
Expand Down
Expand Up @@ -1042,4 +1042,41 @@ public void scmEnvVars() throws Exception {
.logContains("GIT_COMMIT is null")
.go();
}

@Issue("JENKINS-46547")
@Test
public void pipelineDefinedInLibrary() throws Exception {
otherRepo.init();
otherRepo.write("vars/fromLib.groovy", pipelineSourceFromResources("libForPipelineDefinedInLibrary"));
otherRepo.git("add", "vars");
otherRepo.git("commit", "--message=init");
LibraryConfiguration firstLib = new LibraryConfiguration("from-lib",
new SCMSourceRetriever(new GitSCMSource(null, otherRepo.toString(), "", "*", "", true)));

GlobalLibraries.get().setLibraries(Arrays.asList(firstLib));

expect("pipelineDefinedInLibrary")
.logContains("[Pipeline] { (One)", "[Pipeline] { (Two)")
.logNotContains("World")
.go();
}

@Issue("JENKINS-46547")
@Test
public void pipelineDefinedInLibraryInFolder() throws Exception {
otherRepo.init();
otherRepo.write("vars/fromLib.groovy", pipelineSourceFromResources("libForPipelineDefinedInLibrary"));
otherRepo.git("add", "vars");
otherRepo.git("commit", "--message=init");
LibraryConfiguration firstLib = new LibraryConfiguration("from-lib",
new SCMSourceRetriever(new GitSCMSource(null, otherRepo.toString(), "", "*", "", true)));
Folder folder = j.jenkins.createProject(Folder.class, "testFolder");
folder.getProperties().add(new FolderLibraries(Collections.singletonList(firstLib)));

expect("pipelineDefinedInLibrary")
.inFolder(folder)
.logContains("[Pipeline] { (One)", "[Pipeline] { (Two)")
.logNotContains("World")
.go();
}
}
@@ -0,0 +1,56 @@
/*
* 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.
*/

def call() {
pipeline {
agent any
environment {
BRANCH_NAME = "master"
}
stages {
stage("One") {
steps {
echo "Hello"
}
}
stage("Two") {
when {
allOf {
branch "master"
expression {
"foo" == "bar"
}
}
}
steps {
script {
echo "World"
echo "Heal it"
}

}
}
}
}
}
@@ -0,0 +1,27 @@
/*
* 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.
*/

@Library("from-lib@master") _

fromLib()

0 comments on commit cbdb67a

Please sign in to comment.