/*
* The MIT License
*
* Copyright (c) 2013-2014, 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.workflow;
import com.google.common.base.Function;
import hudson.EnvVars;
import hudson.Functions;
import hudson.model.Computer;
import hudson.model.Descriptor;
import hudson.model.Executor;
import hudson.model.Node;
import hudson.model.Queue;
import hudson.model.Slave;
import hudson.model.TaskListener;
import hudson.model.User;
import hudson.slaves.CommandLauncher;
import hudson.slaves.NodeProperty;
import hudson.slaves.RetentionStrategy;
import hudson.slaves.SlaveComputer;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.model.Jenkins;
import jenkins.security.QueueItemAuthenticatorConfiguration;
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval;
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl;
import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl;
import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl;
import org.jenkinsci.plugins.workflow.steps.AbstractSynchronousNonBlockingStepExecution;
import org.jenkinsci.plugins.workflow.steps.StepContextParameter;
import org.jenkinsci.plugins.workflow.steps.StepExecution;
import org.jenkinsci.plugins.workflow.support.actions.EnvironmentAction;
import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep;
import org.junit.Test;
import org.junit.runners.model.Statement;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.MockQueueItemAuthenticator;
import org.jvnet.hudson.test.TestExtension;
import org.kohsuke.stapler.DataBoundConstructor;
/**
* Tests of workflows that involve restarting Jenkins in the middle.
*/
public class WorkflowTest extends SingleJobTestBase {
/**
* Restart Jenkins while workflow is executing to make sure it suspends all right
*/
@Test public void demo() throws Exception {
story.addStep(new Statement() {
@Override
public void evaluate() throws Throwable {
p = jenkins().createProject(WorkflowJob.class, "demo");
p.setDefinition(new CpsFlowDefinition("semaphore 'wait'"));
startBuilding();
SemaphoreStep.waitForStart("wait/1", b);
assertTrue(b.isBuilding());
liveness();
}
});
story.addStep(new Statement() {
@Override
public void evaluate() throws Throwable {
rebuildContext(story.j);
assertThatWorkflowIsSuspended();
for (int i = 0; i < 600 && !Queue.getInstance().isEmpty(); i++) {
Thread.sleep(100);
}
liveness();
SemaphoreStep.success("wait/1", null);
story.j.assertBuildStatusSuccess(story.j.waitForCompletion(b));
}
});
}
private void liveness() {
assertFalse(jenkins().toComputer().isIdle());
Executor e = b.getOneOffExecutor();
assertNotNull(e);
assertEquals(e, b.getExecutor());
assertTrue(e.isActive());
/* TODO seems flaky:
assertFalse(e.isAlive());
*/
}
/**
* ability to invoke body needs to survive beyond Jenkins restart.
*/
@Test public void invokeBodyLaterAfterRestart() throws Exception {
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
p = jenkins().createProject(WorkflowJob.class, "demo");
p.setDefinition(new CpsFlowDefinition(
"int count=0;\n" +
"retry(3) {\n" +
" semaphore 'wait'\n" +
" if (count++ < 2) {\n" + // forcing retry
" error 'died'\n" +
" }\n" +
"}"));
startBuilding();
SemaphoreStep.waitForStart("wait/1", b);
assertTrue(b.isBuilding());
}
});
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
rebuildContext(story.j);
assertThatWorkflowIsSuspended();
// resume execution and cause the retry to invoke the body again
SemaphoreStep.success("wait/1", null);
SemaphoreStep.success("wait/2", null);
SemaphoreStep.success("wait/3", null);
story.j.assertBuildStatusSuccess(story.j.waitForCompletion(b));
assertTrue(e.programPromise.get().closures.isEmpty());
}
});
}
@Test public void authentication() throws Exception {
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
jenkins().setSecurityRealm(story.j.createDummySecurityRealm());
jenkins().save();
QueueItemAuthenticatorConfiguration.get().getAuthenticators().add(new MockQueueItemAuthenticator(Collections.singletonMap("demo", User.get("someone").impersonate())));
p = jenkins().createProject(WorkflowJob.class, "demo");
p.setDefinition(new CpsFlowDefinition("checkAuth()"));
ScriptApproval.get().preapproveAll();
startBuilding();
waitForWorkflowToSuspend();
assertTrue(b.isBuilding());
story.j.waitForMessage("running as someone", b);
CheckAuth.finish(false);
waitForWorkflowToSuspend();
assertTrue(b.isBuilding());
story.j.waitForMessage("still running as someone", b);
}
});
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
assertEquals(JenkinsRule.DummySecurityRealm.class, jenkins().getSecurityRealm().getClass());
rebuildContext(story.j);
assertThatWorkflowIsSuspended();
story.j.waitForMessage("again running as someone", b);
CheckAuth.finish(true);
story.j.assertLogContains("finally running as someone", story.j.assertBuildStatusSuccess(story.j.waitForCompletion(b)));
}
});
}
public static final class CheckAuth extends AbstractStepImpl {
@DataBoundConstructor public CheckAuth() {}
@TestExtension("authentication") public static final class DescriptorImpl extends AbstractStepDescriptorImpl {
public DescriptorImpl() {
super(Execution.class);
}
@Override public String getFunctionName() {
return "checkAuth";
}
@Override
public String getDisplayName() {
return getFunctionName(); // TODO would be nice for this to be the default, perhaps?
}
}
public static final class Execution extends AbstractStepExecutionImpl {
@StepContextParameter transient TaskListener listener;
@StepContextParameter transient FlowExecution flow;
@Override public boolean start() throws Exception {
listener.getLogger().println("running as " + Jenkins.getAuthentication().getName() + " from " + Thread.currentThread().getName());
return false;
}
@Override public void stop(Throwable cause) throws Exception {}
@Override public void onResume() {
super.onResume();
try {
listener.getLogger().println("again running as " + flow.getAuthentication().getName() + " from " + Thread.currentThread().getName());
} catch (Exception x) {
getContext().onFailure(x);
}
}
}
public static void finish(final boolean terminate) {
StepExecution.applyAll(Execution.class, new Function<Execution,Void>() {
@Override public Void apply(Execution input) {
try {
input.listener.getLogger().println((terminate ? "finally" : "still") + " running as " + input.flow.getAuthentication().getName() + " from " + Thread.currentThread().getName());
if (terminate) {
input.getContext().onSuccess(null);
}
} catch (Exception x) {
input.getContext().onFailure(x);
}
return null;
}
});
}
}
@Issue("JENKINS-30122")
@Test public void authenticationInSynchronousStep() {
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
jenkins().setSecurityRealm(story.j.createDummySecurityRealm());
jenkins().save();
QueueItemAuthenticatorConfiguration.get().getAuthenticators().add(new MockQueueItemAuthenticator(Collections.singletonMap("demo", User.get("someone").impersonate())));
p = jenkins().createProject(WorkflowJob.class, "demo");
p.setDefinition(new CpsFlowDefinition("echo \"ran as ${auth()}\"", true));
b = story.j.assertBuildStatusSuccess(p.scheduleBuild2(0));
story.j.assertLogContains("ran as someone", b);
}
});
}
public static final class CheckAuthSync extends AbstractStepImpl {
@DataBoundConstructor public CheckAuthSync() {}
@TestExtension("authenticationInSynchronousStep") public static final class DescriptorImpl extends AbstractStepDescriptorImpl {
public DescriptorImpl() {
super(Execution.class);
}
@Override public String getFunctionName() {
return "auth";
}
@Override public String getDisplayName() {
return getFunctionName();
}
}
public static final class Execution extends AbstractSynchronousNonBlockingStepExecution<String> {
@Override protected String run() throws Exception {
return Jenkins.getAuthentication().getName();
}
}
}
@Issue("JENKINS-29952")
@Test public void env() {
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
Map<String,String> slaveEnv = new HashMap<String,String>();
slaveEnv.put("BUILD_TAG", null);
slaveEnv.put("PERMACHINE", "set");
createSpecialEnvSlave(story.j, "slave", null, slaveEnv);
p = jenkins().createProject(WorkflowJob.class, "demo");
p.setDefinition(new CpsFlowDefinition("node('slave') {\n"
+ " if (isUnix()) {sh 'echo tag=$BUILD_TAG PERMACHINE=$PERMACHINE'} else {bat 'echo tag=%BUILD_TAG% PERMACHINE=%PERMACHINE%'}\n"
+ " env.BUILD_TAG='custom'\n"
+ " if (isUnix()) {sh 'echo tag2=$BUILD_TAG'} else {bat 'echo tag2=%BUILD_TAG%'}\n"
+ " env.STUFF='more'\n"
+ " semaphore 'env'\n"
+ " env.BUILD_TAG=\"${env.BUILD_TAG}2\"\n"
+ " if (isUnix()) {sh 'echo tag3=$BUILD_TAG stuff=$STUFF'} else {bat 'echo tag3=%BUILD_TAG% stuff=%STUFF%'}\n"
+ " if (isUnix()) {env.PATH=\"/opt/stuff/bin:${env.PATH}\"} else {env.PATH=$/c:\\whatever;${env.PATH}/$}\n"
+ " if (isUnix()) {sh 'echo shell PATH=$PATH'} else {bat 'echo shell PATH=%PATH%'}\n"
+ " echo \"groovy PATH=${env.PATH}\"\n"
+ " echo \"simplified groovy PATH=${PATH}\"\n"
+ "}", true));
startBuilding();
SemaphoreStep.waitForStart("env/1", b);
assertTrue(b.isBuilding());
}
});
story.addStep(new Statement() {
@Override public void evaluate() throws Throwable {
rebuildContext(story.j);
assertThatWorkflowIsSuspended();
SemaphoreStep.success("env/1", null);
story.j.assertBuildStatusSuccess(story.j.waitForCompletion(b));
story.j.assertLogContains("tag=jenkins-demo-1 PERMACHINE=set", b);
story.j.assertLogContains("tag2=custom", b);
story.j.assertLogContains("tag3=custom2 stuff=more", b);
String prefix = Functions.isWindows() ? "c:\\whatever;" : "/opt/stuff/bin:";
story.j.assertLogContains("shell PATH=" + prefix, b);
story.j.assertLogContains("groovy PATH=" + prefix, b);
story.j.assertLogContains("simplified groovy PATH=" + prefix, b);
EnvironmentAction a = b.getAction(EnvironmentAction.class);
assertNotNull(a);
assertEquals("custom2", a.getEnvironment().get("BUILD_TAG"));
assertEquals("more", a.getEnvironment().get("STUFF"));
assertNotNull(a.getEnvironment().get("PATH"));
// Show that EnvActionImpl binding is a fallback only for things which would otherwise have been undefined:
p.setDefinition(new CpsFlowDefinition(
"env.env = 'env.env'\n" +
"env.echo = 'env.echo'\n" +
"env.circle = 'env.circle'\n" +
"env.var = 'env.var'\n" +
"env.global = 'env.global'\n" +
"global = 'global'\n" +
"circle {\n" +
" def var = 'value'\n" +
" echo \"${var} vs. ${echo} vs. ${circle} vs. ${global}\"\n" +
"}", true));
story.j.assertLogContains("value vs. env.echo vs. env.circle vs. global", story.j.buildAndAssertSuccess(p));
}
});
}
// TODO add to jenkins-test-harness
/**
* Akin to {@link JenkinsRule#createSlave(String, String, EnvVars)} but allows {@link Computer#getEnvironment} to be controlled rather than directly modifying launchers.
* @param env variables to override in {@link Computer#getEnvironment}; null values will get unset even if defined in the test environment
* @see <a href="https://github.com/jenkinsci/jenkins/pull/1553/files#r23784822">explanation in core PR 1553</a>
*/
public static Slave createSpecialEnvSlave(JenkinsRule rule, String nodeName, @CheckForNull String labels, Map<String,String> env) throws Exception {
@SuppressWarnings("deprecation") // keep consistency with original signature rather than force the caller to pass in a TemporaryFolder rule
File remoteFS = rule.createTmpDir();
SpecialEnvSlave slave = new SpecialEnvSlave(remoteFS, rule.createComputerLauncher(/* yes null */null), nodeName, labels != null ? labels : "", env);
rule.jenkins.addNode(slave);
return slave;
}
private static class SpecialEnvSlave extends Slave {
private final Map<String,String> env;
SpecialEnvSlave(File remoteFS, CommandLauncher launcher, String nodeName, @Nonnull String labels, Map<String,String> env) throws Descriptor.FormException, IOException {
super(nodeName, nodeName, remoteFS.getAbsolutePath(), 1, Node.Mode.NORMAL, labels, launcher, RetentionStrategy.NOOP, Collections.<NodeProperty<?>>emptyList());
this.env = env;
}
@Override public Computer createComputer() {
return new SpecialEnvComputer(this, env);
}
}
private static class SpecialEnvComputer extends SlaveComputer {
private final Map<String,String> env;
SpecialEnvComputer(SpecialEnvSlave slave, Map<String,String> env) {
super(slave);
this.env = env;
}
@Override public EnvVars getEnvironment() throws IOException, InterruptedException {
EnvVars env2 = super.getEnvironment();
env2.overrideAll(env);
return env2;
}
}
}