Skip to content

Commit

Permalink
Merged with #15.
Browse files Browse the repository at this point in the history
  • Loading branch information
jglick committed Jun 10, 2016
2 parents 959aa19 + 0043928 commit 2f70a8d
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 30 deletions.
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Pipeline Groovy Plugin

[Wiki page](https://wiki.jenkins-ci.org/display/JENKINS/Pipeline+Groovy+Plugin)

## Introduction

A key component of the Pipeline plugin suite, this provides the standard execution engine for Pipeline steps, based on a custom [Groovy](http://www.groovy-lang.org/) interpreter that runs inside the Jenkins master process.

(In principle other execution engines could be supported, with `FlowDefinition` being the API entry point, but none has been prototyped and it would likely be a very substantial effort to write one.)

Pipeline Groovy script code such as

```groovy
retry(3) {
for (int i = 0; i < 10; i++) {
branches["branch${i}"] = {
node {
retry(3) {
checkout scm
}
sh 'make world'
}
}
}
parallel branches
```

gets run as a Groovy program, with certain special function calls called *steps* performing Jenkins-specific operations.
In this example the step `parallel` is defined in this plugin, while `node`, `retry`, `checkout`, and `sh` are defined in other plugins in the Pipeline suite.
The `scm` global variable is defined in the Pipeline Multibranch plugin.

Unlike a regular Groovy program run from a command line, the complete state of a Pipeline build’s program is saved to disk every time an *asynchronous* operation is performed, which includes most Pipeline steps.
Jenkins may be restarted while a build is running, and will resume running the program where it left off.
This is not intended to be efficient, and so should be limited to high-level “glue” code directly related to Jenkins features;
your project’s own build logic should be run from external programs on a build node, in a `sh` or `bat` step.

## Known limitations

The [Pipeline Groovy epic](https://issues.jenkins-ci.org/browse/JENKINS-35390) in JIRA covers some known limitations in the Groovy interpreter.
Most notable is that not all `for`-loops work, due to non-`Serializable` intermediate values; and some Groovy idioms involving closures such as `list.each {it -> …}` do not work.
These issues stem from the fact that Pipeline cannot run Groovy directly, but must intercept each operation to save the program state.

The [Pipeline Sandbox epic](https://issues.jenkins-ci.org/browse/JENKINS-35391) covers issues with the *Groovy sandbox* used to prevent malicious Pipeline scripts from taking control of Jenkins.
Scripts run with the sandbox disabled can make direct calls to Jenkins internal APIs, which can be a useful workaround for missing step functionality, but for security reasons only administrators can approve such scripts.

The [Pipeline Snippet Generator epic](https://issues.jenkins-ci.org/browse/JENKINS-35393) covers issues with the tool used to provide samples of step syntax based on live configuration forms.

## Technical design

The plugin uses the [Groovy CPS library](https://github.com/cloudbees/groovy-cps/) to implement a [contination-passing style transformation](https://en.wikipedia.org/wiki/Continuation-passing_style) on the program as it is compiled.
The standard Groovy compiler is used to create the AST, but generation of bytecode is intercepted by a `CompilationCustomizer` which replaces most operations with variants that throw a special “error”, `CpsCallableInvocation`.
This is then caught by the engine, which uses information from it (such as arguments about to be passed to a method call) to pass control on to the next continuation.

Pipeline scripts may mark designated methods with the annotation `@NonCPS`.
These are then compiled normally (except for sandbox security checks), and so behave much like “binary” methods from the Java Platform, Groovy runtime, or Jenkins core or plugin code.
`@NonCPS` methods may safely use non-`Serializable` objects as local variables, though they should not accept nonserializable parameters or return or store nonserializable values.
You may not call regular (CPS-transformed) methods, or Pipeline steps, from a `@NonCPS` method, so they are best used for performing some calculations before passing a summary back to the main script.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
</parent>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-cps</artifactId>
<version>2.5-SNAPSHOT</version>
<version>2.6-SNAPSHOT</version>
<packaging>hpi</packaging>
<name>Pipeline: Groovy</name>
<url>https://wiki.jenkins-ci.org/display/JENKINS/Pipeline+Groovy+Plugin</url>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@
import java.beans.Introspector;
import java.util.LinkedHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.annotation.CheckForNull;
import javax.annotation.concurrent.GuardedBy;
import jenkins.security.NotReallyRoleSensitiveCallable;

import org.acegisecurity.Authentication;
import org.acegisecurity.userdetails.UsernameNotFoundException;
Expand Down Expand Up @@ -885,19 +885,24 @@ public String getNextScriptName(String path) {
}

@Restricted(DoNotUse.class)
@Terminator public static void suspendAll() throws InterruptedException, ExecutionException, TimeoutException {
LOGGER.fine("starting to suspend all executions");
for (FlowExecution execution : FlowExecutionList.get()) {
if (execution instanceof CpsFlowExecution) {
LOGGER.log(Level.FINE, "waiting to suspend {0}", execution);
CpsFlowExecution exec = (CpsFlowExecution) execution;
// Like waitForSuspension but with a timeout:
if (exec.programPromise != null) {
exec.programPromise.get(1, TimeUnit.MINUTES).scheduleRun().get(1, TimeUnit.MINUTES);
@Terminator public static void suspendAll() throws Exception {
ACL.impersonate(ACL.SYSTEM, new NotReallyRoleSensitiveCallable<Void,Exception>() { // TODO Jenkins 2.1+ remove JENKINS-34281 workaround
@Override public Void call() throws Exception {
LOGGER.fine("starting to suspend all executions");
for (FlowExecution execution : FlowExecutionList.get()) {
if (execution instanceof CpsFlowExecution) {
LOGGER.log(Level.FINE, "waiting to suspend {0}", execution);
CpsFlowExecution exec = (CpsFlowExecution) execution;
// Like waitForSuspension but with a timeout:
if (exec.programPromise != null) {
exec.programPromise.get(1, TimeUnit.MINUTES).scheduleRun().get(1, TimeUnit.MINUTES);
}
}
}
LOGGER.fine("finished suspending all executions");
return null;
}
}
LOGGER.fine("finished suspending all executions");
});
}

// TODO: write a custom XStream Converter so that while we are writing CpsFlowExecution, it holds that lock
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
package org.jenkinsci.plugins.workflow.cps;

import com.cloudbees.groovy.cps.impl.CpsClosure;
import groovy.lang.Closure;
import groovy.lang.GroovyClassLoader;
import org.jenkinsci.plugins.scriptsecurity.sandbox.Whitelist;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException;
import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.EnumeratingWhitelist;
import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.ProxyWhitelist;

/**
* Allow any calls into the scripted compiled in the sandbox.
*
* IOW, allow the user to call his own methods.
*
* @author Kohsuke Kawaguchi
*/
class GroovyClassLoaderWhitelist extends Whitelist {

private final ClassLoader scriptLoader;

public GroovyClassLoaderWhitelist(GroovyClassLoader scriptLoader) {
/**
* {@link ProxyWhitelist} has an optimization which bypasses {@code permits*} calls
* when it detects {@link EnumeratingWhitelist} delegates,
* so we must do delegation manually.
* For the same reason, doing this check inside {@link CpsWhitelist} is tricky.
*/
private final Whitelist delegate;

GroovyClassLoaderWhitelist(GroovyClassLoader scriptLoader, Whitelist delegate) {
this.scriptLoader = scriptLoader;
this.delegate = delegate;
}

private boolean permits(Class<?> declaringClass) {
Expand All @@ -30,30 +45,48 @@ private boolean permits(Class<?> declaringClass) {
}

@Override public boolean permitsMethod(Method method, Object receiver, Object[] args) {
return permits(method.getDeclaringClass());
return checkJenkins26481(args, method, method.getParameterTypes()) || delegate.permitsMethod(method, receiver, args);
}

@Override public boolean permitsConstructor(Constructor<?> constructor, Object[] args) {
return permits(constructor.getDeclaringClass());
return checkJenkins26481(args, constructor, constructor.getParameterTypes()) || delegate.permitsConstructor(constructor, args);
}

@Override public boolean permitsStaticMethod(Method method, Object[] args) {
return permits(method.getDeclaringClass());
return checkJenkins26481(args, method, method.getParameterTypes()) || delegate.permitsStaticMethod(method, args);
}

@Override public boolean permitsFieldGet(Field field, Object receiver) {
return permits(field.getDeclaringClass());
return permits(field.getDeclaringClass()) || delegate.permitsFieldGet(field, receiver);
}

@Override public boolean permitsFieldSet(Field field, Object receiver, Object value) {
return permits(field.getDeclaringClass());
return permits(field.getDeclaringClass()) || delegate.permitsFieldSet(field, receiver, value);
}

@Override public boolean permitsStaticFieldGet(Field field) {
return permits(field.getDeclaringClass());
return permits(field.getDeclaringClass()) || delegate.permitsStaticFieldGet(field);
}

@Override public boolean permitsStaticFieldSet(Field field, Object value) {
return permits(field.getDeclaringClass());
return permits(field.getDeclaringClass()) || delegate.permitsStaticFieldSet(field, value);
}

/**
* Blocks accesses to some {@link DefaultGroovyMethods}, or any other binary method taking a closure, until JENKINS-26481 is fixed.
* Only improves diagnostics for scripts using the sandbox.
* Note that we do not simply return false for these cases, as that would throw {@link RejectedAccessException} without a meaningful explanation.
*/
private boolean checkJenkins26481(Object[] args, /* TODO Java 8: just take Executable */ Member method, Class<?>[] parameterTypes) throws UnsupportedOperationException {
if (permits(method.getDeclaringClass())) { // fine for source-defined methods to take closures
return true;
}
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof CpsClosure && parameterTypes.length > i && parameterTypes[i] == Closure.class) {
throw new UnsupportedOperationException("Calling " + method + " on a CPS-transformed closure is not yet supported (JENKINS-26481); encapsulate in a @NonCPS method, or use Java-style loops");
}
}
return false;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import com.cloudbees.groovy.cps.Outcome;
import org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException;
import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.GroovySandbox;
import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.ProxyWhitelist;
import org.jenkinsci.plugins.scriptsecurity.scripts.ApprovalContext;
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval;

Expand Down Expand Up @@ -37,7 +36,7 @@ public Outcome call() {
}
return outcome;
}
}, new ProxyWhitelist(new GroovyClassLoaderWhitelist(thread.group.getExecution().getShell().getClassLoader()),CpsWhitelist.get()));
}, new GroovyClassLoaderWhitelist(thread.group.getExecution().getShell().getClassLoader(), CpsWhitelist.get()));
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.jenkinsci.plugins.workflow.steps.StepExecution;
import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep;
import static org.junit.Assert.*;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runners.model.Statement;
Expand All @@ -55,8 +56,9 @@ public class CpsFlowExecutionTest {

private static WeakReference<ClassLoader> LOADER;
public static void register(Object o) {
LOADER = new WeakReference<ClassLoader>(o.getClass().getClassLoader());
LOADER = new WeakReference<>(o.getClass().getClassLoader());
}
@Ignore("TODO fails in Jenkins 2 for reasons TBD (no root references detected)")
@Test public void loaderReleased() {
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.jenkinsci.plugins.workflow;

import groovy.lang.Closure;
import hudson.model.Result;
import hudson.slaves.DumbSlave;
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval;
Expand Down Expand Up @@ -157,7 +158,7 @@ public void stop(Throwable cause) throws Exception {
"for (def elt : arr) {echo \"running new-style loop on ${elt}\"; semaphore \"new-${elt}\"}"
, true));
ScriptApproval.get().approveSignature("staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods plus java.util.Collection java.lang.Object"); // TODO ought to be in generic-whitelist
ScriptApproval.get().approveSignature("staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods plus java.util.List java.lang.Object"); // Groovy 2.x adds this version. above line is for 1.x
ScriptApproval.get().approveSignature("staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods plus java.util.List java.lang.Object"); // TODO ought to be in generic-whitelist-groovy2
startBuilding();
SemaphoreStep.waitForStart("C-one/1", b);
story.j.waitForMessage("running C-style loop on one", b);
Expand Down Expand Up @@ -233,6 +234,43 @@ public void stop(Throwable cause) throws Exception {
});
}

/**
* Verifies that we can use closures in ways that were not affected by JENKINS-26481.
* In particular:
* <ul>
* <li>on non-CPS-transformed {@link Closure}s
* <li>on closures passed to methods defined in Pipeline script
* <li>on closures passed to methods which did not declare {@link Closure} as a parameter type and so presumably are not going to try to call them
* </ul>
*/
@Issue("JENKINS-26481")
@Test public void eachClosureNonCps() {
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
ScriptApproval.get().approveSignature("staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods plus java.util.Collection java.lang.Object");
p = jenkins().createProject(WorkflowJob.class, "demo");
p.setDefinition(new CpsFlowDefinition(
"@NonCPS def fine() {\n" +
" def text = ''\n" +
" ['a', 'b', 'c'].each {it -> text += it}\n" +
" text\n" +
"}\n" +
"def takesMyOwnClosure(body) {\n" +
" node {\n" +
" def list = []\n" +
" list += body\n" +
" echo list[0]()\n" +
" }\n" +
"}\n" +
"takesMyOwnClosure {\n" +
" fine()\n" +
"}\n", true));
b = story.j.assertBuildStatusSuccess(p.scheduleBuild2(0));
story.j.assertLogContains("abc", b);
}
});
}

@Ignore("TODO JENKINS-31314: calls writeFile just once, echoes null (i.e., return value of writeFile), then succeeds")
@Test public void nonCpsContinuable() {
story.addStep(new Statement() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ private void liveness() {
assertNotNull(e);
assertEquals(e, b.getExecutor());
assertTrue(e.isActive());
/* TODO seems flaky:
assertFalse(e.isAlive());
*/
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep;
import static org.junit.Assert.*;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runners.model.Statement;
import org.jvnet.hudson.test.BuildWatcher;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.RestartableJenkinsRule;

Expand All @@ -19,6 +21,8 @@
* @author Kohsuke Kawaguchi
*/
public class RestartingLoadStepTest {

@ClassRule public static BuildWatcher buildWatcher = new BuildWatcher();
@Rule public RestartableJenkinsRule story = new RestartableJenkinsRule();

@Inject
Expand All @@ -34,8 +38,8 @@ public void persistenceOfLoadedScripts() throws Exception {
WorkflowJob p = jenkins.createProject(WorkflowJob.class, "p");
jenkins.getWorkspaceFor(p).child("test.groovy").write(
"def answer(i) { return i*2; }\n" +
"def foo() {\n" +
" def i=21;\n" +
"def foo(body) {\n" +
" def i = body()\n" +
" semaphore 'watchA'\n" +
" return answer(i);\n" +
"}\n" +
Expand All @@ -44,7 +48,7 @@ public void persistenceOfLoadedScripts() throws Exception {
"node {\n" +
" println 'started'\n" +
" def o = load 'test.groovy'\n" +
" println 'o=' + o.foo();\n" +
" println 'o=' + o.foo({21})\n" +
"}"));

// get the build going
Expand Down

0 comments on commit 2f70a8d

Please sign in to comment.