Skip to content

Commit

Permalink
[JENKINS-30398] Initial (experimental) support for workflow plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
jcsirot committed Dec 8, 2015
1 parent 3aee5ae commit 67943cf
Show file tree
Hide file tree
Showing 15 changed files with 557 additions and 115 deletions.
9 changes: 8 additions & 1 deletion pom.xml
Expand Up @@ -3,7 +3,7 @@
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>1.565</version>
<version>1.580.1</version>
</parent>

<groupId>org.jenkins-ci.plugins</groupId>
Expand Down Expand Up @@ -61,6 +61,12 @@
<artifactId>ssh-credentials</artifactId>
<version>${ssh-credentials.version}</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-step-api</artifactId>
<version>${workflow-step-api.version}</version>
<optional>true</optional>
</dependency>
<!-- Test -->
<dependency>
<groupId>junit</groupId>
Expand Down Expand Up @@ -118,6 +124,7 @@
<maven-release-plugin.version>2.5.2</maven-release-plugin.version>
<ssh-credentials.version>1.10</ssh-credentials.version>
<credentials.version>1.16.1</credentials.version>
<workflow-step-api.version>1.4.2</workflow-step-api.version>
<junit.version>4.12</junit.version>
<mockito.version>1.10.19</mockito.version>
<assertj.version>1.7.1</assertj.version>
Expand Down
Expand Up @@ -26,6 +26,8 @@
import hudson.FilePath;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.util.ArgumentListBuilder;
import hudson.util.Secret;
import org.apache.commons.lang.StringUtils;
Expand All @@ -36,8 +38,8 @@
abstract class AbstractAnsibleInvocation<T extends AbstractAnsibleInvocation<T>> {

protected final EnvVars envVars;
protected final BuildListener listener;
protected final AbstractBuild<?, ?> build;
protected final TaskListener listener;
protected final Run<?, ?> build;
protected final Map<String, String> environment = new HashMap<String, String>();

protected String exe;
Expand All @@ -49,11 +51,14 @@ abstract class AbstractAnsibleInvocation<T extends AbstractAnsibleInvocation<T>>

private FilePath key = null;
private Inventory inventory;
private boolean copyCredentialsInWorkspace = false;
private final FilePath ws;

protected AbstractAnsibleInvocation(String exe, AbstractBuild<?, ?> build, BuildListener listener)
protected AbstractAnsibleInvocation(String exe, Run<?, ?> build, FilePath ws, TaskListener listener)
throws IOException, InterruptedException, AnsibleInvocationException
{
this.build = build;
this.ws = ws;
this.envVars = build.getEnvironment(listener);
this.listener = listener;
this.exe = exe;
Expand All @@ -76,10 +81,11 @@ protected ArgumentListBuilder appendInventory(ArgumentListBuilder args)
throws IOException, InterruptedException, AnsibleInvocationException
{
if (inventory == null) {
throw new AnsibleInvocationException(
"The inventory of hosts and groups is not defined. Check the job configuration.");
// throw new AnsibleInvocationException(
// "The inventory of hosts and groups is not defined. Check the job configuration.");
return args;
}
inventory.addArgument(args, build.getWorkspace(), envVars, listener);
inventory.addArgument(args, ws, envVars, listener);
return args;
}

Expand Down Expand Up @@ -124,6 +130,11 @@ public T setCredentials(StandardUsernameCredentials credentials) {
return (T) this;
}

public T setCredentials(StandardUsernameCredentials credentials, boolean copyCredentialsInWorkspace) {
this.copyCredentialsInWorkspace = copyCredentialsInWorkspace;
return setCredentials(credentials);
}

protected ArgumentListBuilder prependPasswordCredentials(ArgumentListBuilder args) {
if (credentials instanceof UsernamePasswordCredentials) {
UsernamePasswordCredentials passwordCredentials = (UsernamePasswordCredentials)credentials;
Expand All @@ -137,7 +148,7 @@ protected ArgumentListBuilder appendCredentials(ArgumentListBuilder args)
{
if (credentials instanceof SSHUserPrivateKey) {
SSHUserPrivateKey privateKeyCredentials = (SSHUserPrivateKey)credentials;
key = Utils.createSshKeyFile(key, build.getWorkspace(), privateKeyCredentials);
key = Utils.createSshKeyFile(key, ws, privateKeyCredentials, copyCredentialsInWorkspace);
args.add("--private-key").add(key);
args.add("-u").add(privateKeyCredentials.getUsername());
} else if (credentials instanceof UsernamePasswordCredentials) {
Expand Down Expand Up @@ -175,7 +186,9 @@ public boolean execute(CLIRunner runner) throws IOException, InterruptedExceptio
try {
return runner.execute(buildCommandLine(), environment);
} finally {
inventory.tearDown(listener);
if (inventory != null) {
inventory.tearDown(listener);
}
Utils.deleteTempFile(key, listener);
}
}
Expand Down
Expand Up @@ -15,34 +15,42 @@
*/
package org.jenkinsci.plugins.ansible;

import javax.annotation.Nonnull;
import java.io.IOException;

import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
import hudson.AbortException;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.Computer;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.tasks.Builder;
import hudson.util.FormValidation;
import jenkins.tasks.SimpleBuildStep;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;

/**
* A builder which wraps an Ansible Ad-Hoc command invocation.
*/
public class AnsibleAdHocCommandBuilder extends Builder {
public class AnsibleAdHocCommandBuilder extends Builder implements SimpleBuildStep {

public final String ansibleName;
public String ansibleName;

// SSH settings
/**
* The id of the credentials to use.
*/
public final String credentialsId;
public String credentialsId = null;

public final String hostPattern;

Expand All @@ -55,22 +63,22 @@ public class AnsibleAdHocCommandBuilder extends Builder {

public final String command;

public final boolean sudo;
public boolean sudo = false;

public final String sudoUser;
public String sudoUser = "root";

public final int forks;
public int forks = 5;

public final boolean unbufferedOutput;
public boolean unbufferedOutput = true;

public final boolean colorizedOutput;
public boolean colorizedOutput = false;

public final boolean hostKeyChecking;
public boolean hostKeyChecking = false;

public final String additionalParameters;
public String additionalParameters = null;


@DataBoundConstructor
@Deprecated
public AnsibleAdHocCommandBuilder(String ansibleName, String hostPattern, Inventory inventory, String module,
String command, String credentialsId, boolean sudo, String sudoUser, int forks,
boolean unbufferedOutput, boolean colorizedOutput, boolean hostKeyChecking,
Expand All @@ -91,34 +99,92 @@ public AnsibleAdHocCommandBuilder(String ansibleName, String hostPattern, Invent
this.additionalParameters = additionalParameters;
}

@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
@Deprecated
public AnsibleAdHocCommandBuilder(String hostPattern, Inventory inventory, String module, String command) {
this.hostPattern = hostPattern;
this.inventory = inventory;
this.module = module;
this.command = command;
}

@DataBoundSetter
public void setAnsibleName(String ansibleName) {
this.ansibleName = ansibleName;
}

@DataBoundSetter
public void setCredentialsId(String credentialsId) {
this.credentialsId = credentialsId;
}

@DataBoundSetter
public void setSudo(boolean sudo) {
this.sudo = sudo;
}

@DataBoundSetter
public void setSudoUser(String sudoUser) {
this.sudoUser = sudoUser;
}

@DataBoundSetter
public void setForks(int forks) {
this.forks = forks;
}

@DataBoundSetter
public void setUnbufferedOutput(boolean unbufferedOutput) {
this.unbufferedOutput = unbufferedOutput;
}

@DataBoundSetter
public void setColorizedOutput(boolean colorizedOutput) {
this.colorizedOutput = colorizedOutput;
}

@DataBoundSetter
public void setHostKeyChecking(boolean hostKeyChecking) {
this.hostKeyChecking = hostKeyChecking;
}

@DataBoundSetter
public void setAdditionalParameters(String additionalParameters) {
this.additionalParameters = additionalParameters;
}

@Override
public void perform(@Nonnull Run<?, ?> run, @Nonnull FilePath ws, @Nonnull Launcher launcher, @Nonnull TaskListener listener) throws InterruptedException, IOException {
try {
CLIRunner runner = new CLIRunner(build, launcher, listener);
String exe = AnsibleInstallation.getInstallation(ansibleName).getExecutable(AnsibleCommand.ANSIBLE, launcher);
AnsibleAdHocCommandInvocation invocation = new AnsibleAdHocCommandInvocation(exe , build, listener);
CLIRunner runner = new CLIRunner(run, ws, launcher, listener);
Computer computer = Computer.currentComputer();
if (computer == null) {
throw new AbortException("The ansible playbook build step requires to be launched on a node");
}
String exe = AnsibleInstallation.getExecutable(ansibleName, AnsibleCommand.ANSIBLE, computer.getNode(), listener, run.getEnvironment(listener));
AnsibleAdHocCommandInvocation invocation = new AnsibleAdHocCommandInvocation(exe, run, ws, listener);
invocation.setHostPattern(hostPattern);
invocation.setInventory(inventory);
invocation.setModule(module);
invocation.setModuleCommand(command);
invocation.setSudo(sudo, sudoUser);
invocation.setForks(forks);
invocation.setCredentials(StringUtils.isNotBlank(credentialsId) ?
CredentialsProvider.findCredentialById(credentialsId, StandardUsernameCredentials.class, build) :
null);
CredentialsProvider.findCredentialById(credentialsId, StandardUsernameCredentials.class, run) :
null);
invocation.setAdditionalParameters(additionalParameters);
invocation.setHostKeyCheck(hostKeyChecking);
invocation.setUnbufferedOutput(unbufferedOutput);
invocation.setColorizedOutput(colorizedOutput);
return invocation.execute(runner);
if (!invocation.execute(runner)) {
throw new AbortException("Ansible Ad-Hoc command execution failed");
}
} catch (IOException ioe) {
Util.displayIOException(ioe, listener);
ioe.printStackTrace(listener.fatalError(hudson.tasks.Messages.CommandInterpreter_CommandFailed()));
return false;
throw ioe;
} catch (AnsibleInvocationException aie) {
listener.fatalError(aie.getMessage());
return false;
throw new AbortException(aie.getMessage());
}
}

Expand Down
Expand Up @@ -17,9 +17,12 @@

import java.io.IOException;

import hudson.FilePath;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.util.ArgumentListBuilder;
import org.apache.commons.lang.StringUtils;

Expand All @@ -35,7 +38,13 @@ public class AnsibleAdHocCommandInvocation extends AbstractAnsibleInvocation<Ans
protected AnsibleAdHocCommandInvocation(String exe, AbstractBuild<?, ?> build, BuildListener listener)
throws IOException, InterruptedException, AnsibleInvocationException
{
super(exe, build, listener);
super(exe, build, build.getWorkspace(), listener);
}

public AnsibleAdHocCommandInvocation(String exe, Run<?, ?> build, FilePath ws, TaskListener listener)
throws IOException, InterruptedException, AnsibleInvocationException
{
super(exe, build, ws, listener);
}

public AnsibleAdHocCommandInvocation setHostPattern(String hostPattern) {
Expand Down
Expand Up @@ -22,7 +22,9 @@

import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Util;
import hudson.model.EnvironmentSpecific;
import hudson.model.Node;
import hudson.model.TaskListener;
Expand All @@ -33,6 +35,7 @@
import hudson.tools.ToolProperty;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.jenkinsci.remoting.RoleChecker;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;

Expand All @@ -55,16 +58,33 @@ public AnsibleInstallation forNode(Node node, TaskListener log) throws IOExcepti
return new AnsibleInstallation(getName(), translateFor(node, log), getProperties().toList());
}

public String getExecutable(final AnsibleCommand command, Launcher launcher) throws IOException, InterruptedException {
return launcher.getChannel().call(new Callable<String, IOException>() {
public String call() throws IOException {
File exe = new File(getHome(), command.getName());
if (exe.exists()) {
return exe.getPath();
public static String getExecutable(String name, AnsibleCommand command, Node node, TaskListener listener, EnvVars env) throws IOException, InterruptedException {
if (name != null) {
Jenkins j = Jenkins.getInstance();
if (j != null) {
for (AnsibleInstallation tool : j.getDescriptorByType(DescriptorImpl.class).getInstallations()) {
if (tool.getName().equals(name)) {
if (node != null) {
tool = tool.forNode(node, listener);
}
if (env != null) {
tool = tool.forEnvironment(env);
}
String home = Util.fixEmpty(tool.getHome());
if (home != null) {
if (node != null) {
FilePath homePath = node.createPath(home);
if (homePath != null) {
return homePath.child(command.getName()).getRemote();
}
}
return home + "/" + command.getName();
}
}
}
return null;
}
});
}
return command.getName();
}

public static AnsibleInstallation[] allInstallations() {
Expand All @@ -89,6 +109,14 @@ public static AnsibleInstallation getInstallation(String ansibleInstallation) th
throw new IOException("Ansible not found");
}

@Override
public void buildEnvVars(EnvVars env) {
String home = Util.fixEmpty(getHome());
if (home != null) {
env.put("PATH+ANSIBLE", home);
}
}

@Extension
public static class DescriptorImpl extends ToolDescriptor<AnsibleInstallation> {

Expand Down

0 comments on commit 67943cf

Please sign in to comment.