Skip to content

Commit

Permalink
fix-279: Fix library class equality across template and steps (#282)
Browse files Browse the repository at this point in the history
  • Loading branch information
steven-terrana authored Jun 9, 2022
1 parent 099d831 commit d3e8c0e
Show file tree
Hide file tree
Showing 23 changed files with 526 additions and 288 deletions.
2 changes: 1 addition & 1 deletion config/codenarc/rulesTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ ruleset {
ClassSize
// CrapMetric // Requires the GMetrics jar and a Cobertura coverage file
CyclomaticComplexity // Requires the GMetrics jar
MethodCount
// MethodCount having a lot of tests is a good thing..
MethodSize
NestedBlockDepth
ParameterCount
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package org.boozallen.plugins.jte.init

import hudson.Extension
import org.boozallen.plugins.jte.init.primitives.TemplateBinding
import org.boozallen.plugins.jte.init.primitives.hooks.CleanUp
import org.boozallen.plugins.jte.init.primitives.hooks.HooksWrapper
import org.boozallen.plugins.jte.init.primitives.hooks.Init
Expand All @@ -37,13 +36,11 @@ import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.control.customizers.CompilationCustomizer
import org.jenkinsci.plugins.workflow.cps.CpsFlowExecution
import org.jenkinsci.plugins.workflow.cps.GroovyShellDecorator
import org.jenkinsci.plugins.workflow.flow.FlowDefinition
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner
import org.jenkinsci.plugins.workflow.job.WorkflowJob
import org.jenkinsci.plugins.workflow.job.WorkflowRun

import javax.annotation.CheckForNull
import java.lang.reflect.Field
import java.util.logging.Level
import java.util.logging.Logger

/**
* Decorates the pipeline template during compilation
Expand All @@ -55,36 +52,6 @@ import java.util.logging.Logger
@Extension(ordinal=1.0D) // set ordinal > 0 so JTE comes before Declarative
class PipelineTemplateCompiler extends GroovyShellDecorator {

private static final Logger LOGGER = Logger.getLogger(PipelineTemplateCompiler.name);

@Override
void configureShell(@CheckForNull CpsFlowExecution context, GroovyShell shell) {
if (!isFromJTE(context)) {
return
}

// use our custom binding that checks for collisions
Field shellBinding = GroovyShell.getDeclaredField("context")
shellBinding.setAccessible(true)
shellBinding.set(shell, new TemplateBinding())

// add loaded libraries `src` directories to the classloader
File jte = context.getOwner().getRootDir()
File srcDir = new File(jte, "jte/src")
if (srcDir.exists()){
if(srcDir.isDirectory()) {
shell.getClassLoader().addURL(srcDir.toURI().toURL())
} else {
LOGGER.log(Level.WARNING, "${srcDir.getPath()} is not a directory.")
}
}
}

@Override
GroovyShellDecorator forTrusted() {
return this
}

/**
* For the JTE pipeline template, customizes the compiler so that the template is wrapped
* in a try-finally block so that the @Validation and @Init Lifecycle Hooks can be invoked
Expand All @@ -94,141 +61,136 @@ class PipelineTemplateCompiler extends GroovyShellDecorator {
@Override
@SuppressWarnings("MethodSize")
void configureCompiler(@CheckForNull final CpsFlowExecution execution, CompilerConfiguration cc) {
if (isFromJTE(execution)) {
cc.addCompilationCustomizers(new CompilationCustomizer(CompilePhase.CONVERSION) {

@Override
void call(SourceUnit source, GeneratorContext context, ClassNode classNode) throws CompilationFailedException {
if (!isTemplate(classNode)) {
return
}
// represents the AST of the user-provided template
ModuleNode template = source.getAST()
List<Statement> statements = template.getStatementBlock().getStatements()

/* We want to seamlessly wrap the template in code for hooks like:
*
* _JTE_hookContext_exceptionThrown = false
* try{
* Hooks.invoke(Validation)
* Hooks.invoke(Init)
* --> Insert Template Code Here <--
* } catch(Exception e){
* _JTE_hookContext_exceptionThrown = true
* throw e
* } finally {
* Hooks.invoke(CleanUp, _JTE_hookContext_exceptionThrown)
* Hooks.invoke(Notify, _JTE_hookContext_exceptionThrown)
* }
*/
if(execution == null){
return
}
// ensure this is a JTE pipeline
FlowExecutionOwner flowOwner = execution.getOwner()
WorkflowRun run = flowOwner.run()
WorkflowJob job = run.getParent()
if(!(job.getDefinition() in TemplateFlowDefinition)){
return
}
cc.addCompilationCustomizers(new CompilationCustomizer(CompilePhase.CONVERSION) {

// 1. get the AST for the code in the comment above without the template
BlockStatement wrapper = getTemplateWrapperAST()
// 2. inject the user's pipeline template statements
wrapper.getStatements()[1].getTryStatement().addStatements(statements)
// 3. replace the compiled template with our new AST
statements.clear()
statements.add(0, wrapper)
@Override
void call(SourceUnit source, GeneratorContext context, ClassNode classNode) throws CompilationFailedException {
if (!isTemplate(classNode)) {
return
}
// represents the AST of the user-provided template
ModuleNode template = source.getAST()
List<Statement> statements = template.getStatementBlock().getStatements()

/* We want to seamlessly wrap the template in code for hooks like:
*
* _JTE_hookContext_exceptionThrown = false
* try{
* Hooks.invoke(Validation)
* Hooks.invoke(Init)
* --> Insert Template Code Here <--
* } catch(Exception e){
* _JTE_hookContext_exceptionThrown = true
* throw e
* } finally {
* Hooks.invoke(CleanUp, _JTE_hookContext_exceptionThrown)
* Hooks.invoke(Notify, _JTE_hookContext_exceptionThrown)
* }
*/

BlockStatement getTemplateWrapperAST() {
List<ASTNode> statements = new AstBuilder().buildFromSpec {
block {
expression{
declaration{
variable "_JTE_hookContext_exceptionThrown"
token "="
constant false
// 1. get the AST for the code in the comment above without the template
BlockStatement wrapper = getTemplateWrapperAST()
// 2. inject the user's pipeline template statements
wrapper.getStatements()[1].getTryStatement().addStatements(statements)
// 3. replace the compiled template with our new AST
statements.clear()
statements.add(0, wrapper)
}

BlockStatement getTemplateWrapperAST() {
List<ASTNode> statements = new AstBuilder().buildFromSpec {
block {
expression{
declaration{
variable "_JTE_hookContext_exceptionThrown"
token "="
constant false
}
}
tryCatch {
block {
expression {
staticMethodCall(HooksWrapper, 'invoke') {
argumentList {
classExpression Validate
}
}
}
expression {
staticMethodCall(HooksWrapper, 'invoke') {
argumentList {
classExpression Init
}
}
}
}
tryCatch {
block {
block {
expression {
staticMethodCall(HooksWrapper, 'invoke') {
argumentList {
classExpression Validate
classExpression CleanUp
variable "_JTE_hookContext_exceptionThrown"
}
}
}
expression {
staticMethodCall(HooksWrapper, 'invoke') {
argumentList {
classExpression Init
classExpression Notify
variable "_JTE_hookContext_exceptionThrown"
}
}
}
}
block {
block {
expression {
staticMethodCall(HooksWrapper, 'invoke') {
argumentList {
classExpression CleanUp
variable "_JTE_hookContext_exceptionThrown"
}
}
}
expression {
staticMethodCall(HooksWrapper, 'invoke') {
argumentList {
classExpression Notify
variable "_JTE_hookContext_exceptionThrown"
}
}
}
catchStatement {
parameter 'e': Exception
block{
expression{
binary{
variable "_JTE_hookContext_exceptionThrown"
token "="
constant true
}
}
}
catchStatement {
parameter 'e': Exception
block{
expression{
binary{
variable "_JTE_hookContext_exceptionThrown"
token "="
constant true
}
}
throwStatement {
variable 'e'
}
throwStatement {
variable 'e'
}
}
}
}
}
return statements.first()
}
return statements.first()
}

/*
need to ensure we're only modifying AST for the template.
an example of additional scripts getting compiled would be from a
template or library step that does something like:
evaluate("x = 1")
/*
need to ensure we're only modifying AST for the template.
an example of additional scripts getting compiled would be from a
template or library step that does something like:
evaluate("x = 1")
to understand why specifically checking for "WorkflowScript" see:
https://github.com/jenkinsci/workflow-cps-plugin/blob/workflow-cps-2.80/src/main/java/org/jenkinsci/plugins/workflow/cps/CpsFlowExecution.java#L561
to understand why specifically checking for "WorkflowScript" see:
https://github.com/jenkinsci/workflow-cps-plugin/blob/workflow-cps-2.80/src/main/java/org/jenkinsci/plugins/workflow/cps/CpsFlowExecution.java#L561
to understand where "evaluate" method comes from see:
https://github.com/jenkinsci/workflow-cps-plugin/blob/workflow-cps-2.80/src/main/java/org/jenkinsci/plugins/workflow/cps/CpsScript.java#L178-L182
*/

boolean isTemplate(ClassNode classNode) {
return classNode.getName() == "WorkflowScript"
}
})
}
}

/**
* determines if the current pipeline is using JTE
*/
boolean isFromJTE(CpsFlowExecution execution){
if(!execution){
return false // no execution defined yet, still initializing
}
WorkflowJob job = execution.getOwner().run().getParent()
FlowDefinition definition = job.getDefinition()
return (definition in TemplateFlowDefinition)
to understand where "evaluate" method comes from see:
https://github.com/jenkinsci/workflow-cps-plugin/blob/workflow-cps-2.80/src/main/java/org/jenkinsci/plugins/workflow/cps/CpsScript.java#L178-L182
*/
boolean isTemplate(ClassNode classNode) {
return classNode.getName() == "WorkflowScript"
}
})
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,26 @@ package org.boozallen.plugins.jte.init.primitives

import org.boozallen.plugins.jte.util.JTEException
import org.boozallen.plugins.jte.util.TemplateLogger
import org.jenkinsci.plugins.workflow.cps.CpsFlowExecution
import org.jenkinsci.plugins.workflow.cps.DSL
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner

/**
* A custom binding that prevents users from inadvertently
* overriding TemplatePrimitives or ReservedVariables
*/
class TemplateBinding extends Binding{

static TemplateBinding create(CpsFlowExecution exec){
return create(exec.getOwner())
}

static TemplateBinding create(FlowExecutionOwner flowOwner){
TemplateBinding binding = new TemplateBinding()
binding.setVariable("steps", new DSL(flowOwner))
return binding
}

@Override
void setVariable(String name, Object value){
checkPrimitiveCollision(name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ import javax.annotation.Nonnull
*/
class TemplatePrimitiveCollector extends InvisibleAction{

/**
* The common classloader that's used across Pipeline Template
* and individual StepWrapper compilation. It gets added by
* {@see StepWrapperFactory}. While not serializable, as an
* action, this class is persisted to the run's build.xml file
*/
@SuppressWarnings('UnnecessaryTransientModifier')
transient ClassLoader loader
List<TemplatePrimitiveNamespace> namespaces = []

static List<GlobalVariable> getGlobalVariablesByName(String name, Run run){
Expand Down
Loading

0 comments on commit d3e8c0e

Please sign in to comment.