package com.cloudbees.jenkins.plugins.sshagent;
import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.google.inject.Inject;
import hudson.EnvVars;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.slaves.WorkspaceList;
import hudson.util.Secret;
import jenkins.model.Jenkins;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.plugins.workflow.steps.*;
import javax.annotation.CheckReturnValue;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.*;
public class SSHAgentStepExecution extends AbstractStepExecutionImpl {
private static final long serialVersionUID = 1L;
@StepContextParameter
private transient TaskListener listener;
@StepContextParameter
private transient Run<?, ?> build;
@StepContextParameter
private transient Launcher launcher;
@StepContextParameter
private transient FilePath workspace;
@Inject(optional = true)
private SSHAgentStep step;
/**
* Value for SSH_AUTH_SOCK environment variable.
*/
private String socket;
/**
* Listing of socket files created. Will be used by {@link #purgeSockets()} and {@link #initRemoteAgent()}
*/
private List<String> sockets;
/**
* The proxy for the real remote agent that is on the other side of the channel (as the agent needs to
* run on a remote machine)
*/
private transient RemoteAgent agent = null;
@Override
public boolean start() throws Exception {
StepContext context = getContext();
sockets = new ArrayList<String>();
initRemoteAgent();
context.newBodyInvoker().
withContext(EnvironmentExpander.merge(getContext().get(EnvironmentExpander.class), new ExpanderImpl(this))).
withCallback(new Callback(this)).withDisplayName(null).start();
return false;
}
@Override
public void stop(Throwable cause) throws Exception {
if (agent != null) {
agent.stop();
listener.getLogger().println(Messages.SSHAgentBuildWrapper_Stopped());
}
purgeSockets();
}
@Override
public void onResume() {
super.onResume();
try {
purgeSockets();
initRemoteAgent();
} catch (IOException | InterruptedException x) {
listener.getLogger().println(Messages.SSHAgentBuildWrapper_CouldNotStartAgent());
x.printStackTrace(listener.getLogger());
}
}
// TODO use 1.652 use WorkspaceList.tempDir
static FilePath tempDir(FilePath ws) {
return ws.sibling(ws.getName() + System.getProperty(WorkspaceList.class.getName(), "@") + "tmp");
}
private static class Callback extends BodyExecutionCallback.TailCall {
private static final long serialVersionUID = 1L;
private final SSHAgentStepExecution execution;
Callback (SSHAgentStepExecution execution) {
this.execution = execution;
}
@Override
protected void finished(StepContext context) throws Exception {
execution.cleanUp();
}
}
private static final class ExpanderImpl extends EnvironmentExpander {
private static final long serialVersionUID = 1L;
private final SSHAgentStepExecution execution;
ExpanderImpl(SSHAgentStepExecution execution) {
this.execution = execution;
}
@Override
public void expand(EnvVars env) throws IOException, InterruptedException {
env.override("SSH_AUTH_SOCK", execution.getSocket());
}
}
/**
* Initializes a SSH Agent.
*
* @throws IOException
*/
private void initRemoteAgent() throws IOException, InterruptedException {
List<SSHUserPrivateKey> userPrivateKeys = new ArrayList<SSHUserPrivateKey>();
for (String id : new LinkedHashSet<String>(step.getCredentials())) {
final SSHUserPrivateKey c = CredentialsProvider.findCredentialById(id, SSHUserPrivateKey.class, build);
CredentialsProvider.track(build, c);
if (c == null && !step.isIgnoreMissing()) {
listener.fatalError(Messages.SSHAgentBuildWrapper_CredentialsNotFound());
}
if (c != null && !userPrivateKeys.contains(c)) {
userPrivateKeys.add(c);
}
}
for (SSHUserPrivateKey userPrivateKey : userPrivateKeys) {
listener.getLogger().println(Messages.SSHAgentBuildWrapper_UsingCredentials(SSHAgentBuildWrapper.description(userPrivateKey)));
}
listener.getLogger().println("[ssh-agent] Looking for ssh-agent implementation...");
Map<String, Throwable> faults = new LinkedHashMap<String, Throwable>();
for (RemoteAgentFactory factory : Jenkins.getActiveInstance().getExtensionList(RemoteAgentFactory.class)) {
if (factory.isSupported(launcher, listener)) {
try {
listener.getLogger().println("[ssh-agent] " + factory.getDisplayName());
agent = factory.start(launcher, listener, tempDir(workspace));
break;
} catch (Throwable t) {
faults.put(factory.getDisplayName(), t);
}
}
}
if (agent == null) {
listener.getLogger().println("[ssh-agent] FATAL: Could not find a suitable ssh-agent provider");
listener.getLogger().println("[ssh-agent] Diagnostic report");
for (Map.Entry<String, Throwable> fault : faults.entrySet()) {
listener.getLogger().println("[ssh-agent] * " + fault.getKey());
StringWriter sw = new StringWriter();
fault.getValue().printStackTrace(new PrintWriter(sw));
for (String line : StringUtils.split(sw.toString(), "\n")) {
listener.getLogger().println("[ssh-agent] " + line);
}
}
throw new RuntimeException("[ssh-agent] Could not find a suitable ssh-agent provider.");
}
for (SSHUserPrivateKey userPrivateKey : userPrivateKeys) {
final Secret passphrase = userPrivateKey.getPassphrase();
final String effectivePassphrase = passphrase == null ? null : passphrase.getPlainText();
for (String privateKey : userPrivateKey.getPrivateKeys()) {
agent.addIdentity(privateKey, effectivePassphrase, SSHAgentBuildWrapper.description(userPrivateKey));
}
}
listener.getLogger().println(Messages.SSHAgentBuildWrapper_Started());
socket = agent.getSocket();
sockets.add(socket);
}
/**
* Shuts down the current SSH Agent and purges socket files.
*/
private void cleanUp() throws Exception {
try {
TaskListener listener = getContext().get(TaskListener.class);
if (agent != null) {
agent.stop();
listener.getLogger().println(Messages.SSHAgentBuildWrapper_Stopped());
}
} finally {
purgeSockets();
}
}
/**
* Purges all socket files created previously.
* Especially useful when Jenkins is restarted during the execution of this step.
*/
private void purgeSockets() {
Iterator<String> it = sockets.iterator();
while (it.hasNext()) {
File socket = new File(it.next());
if (socket.exists()) {
if (!socket.delete()) {
listener.getLogger().format("It was a problem removing this socket file %s", socket.getAbsolutePath());
}
}
it.remove();
}
}
/**
* Returns the socket.
*
* @return The value that SSH_AUTH_SOCK should be set to.
*/
@CheckReturnValue private String getSocket() {
return socket;
}
}