diff --git a/src/main/java/com/cloudbees/jenkins/plugins/sshagent/RemoteAgentFactory.java b/src/main/java/com/cloudbees/jenkins/plugins/sshagent/RemoteAgentFactory.java index 26b1d02..09e6418 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/sshagent/RemoteAgentFactory.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/sshagent/RemoteAgentFactory.java @@ -56,9 +56,10 @@ public abstract class RemoteAgentFactory implements ExtensionPoint { * @param launcherProvider provides launchers on which to start a ssh-agent. * @param listener a listener for any diagnostics. * @param temp a temporary directory to use; null if unspecified + * @param socketPath the optional SSH_AUTH_SOCK socket path * @return the agent. * @throws Throwable if the agent cannot be started. */ public abstract RemoteAgent start(LauncherProvider launcherProvider, TaskListener listener, - @CheckForNull FilePath temp) throws Throwable; + @CheckForNull FilePath temp, String socketPath) throws Throwable; } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentBuildWrapper.java b/src/main/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentBuildWrapper.java index 597d16a..ac235ee 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentBuildWrapper.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentBuildWrapper.java @@ -90,6 +90,13 @@ public class SSHAgentBuildWrapper extends BuildWrapper { * @since 1.5 */ private final boolean ignoreMissing; + + /** + * When not blank, the specified path wil be used as SSH_AUTH_SOCK. + * + * @since 1.24 + */ + private final String socketPath; /** * Constructs a new instance. @@ -108,14 +115,31 @@ public SSHAgentBuildWrapper(String user) { * * @param credentialHolders the {@link com.cloudbees.jenkins.plugins.sshagent.SSHAgentBuildWrapper.CredentialHolder}s of the credentials to use. * @param ignoreMissing {@code true} missing credentials will not cause a build failure. - * @since 1.5 + * @param socketPath blank path will default to ssh-agent defaults. + * @since 1.24 */ @DataBoundConstructor @SuppressWarnings("unused") // used via stapler - public SSHAgentBuildWrapper(CredentialHolder[] credentialHolders, boolean ignoreMissing) { - this(CredentialHolder.toIdList(credentialHolders), ignoreMissing); + public SSHAgentBuildWrapper(CredentialHolder[] credentialHolders, boolean ignoreMissing, String socketPath) { + this(CredentialHolder.toIdList(credentialHolders), ignoreMissing, socketPath); } - + + /** + * Constructs a new instance. + * + * @param credentialIds the {@link com.cloudbees.plugins.credentials.common.StandardUsernameCredentials#getId()}s + * of the credentials to use. + * @param ignoreMissing {@code true} missing credentials will not cause a build failure. + * @param socketPath blank path will default to ssh-agent defaults. + * @since 1.24 + */ + @SuppressWarnings("unused") // used via stapler + public SSHAgentBuildWrapper(List credentialIds, boolean ignoreMissing, String socketPath) { + this.credentialIds = new ArrayList(new LinkedHashSet(credentialIds)); + this.ignoreMissing = ignoreMissing; + this.socketPath = socketPath; + } + /** * Constructs a new instance. * @@ -128,6 +152,7 @@ public SSHAgentBuildWrapper(CredentialHolder[] credentialHolders, boolean ignore public SSHAgentBuildWrapper(List credentialIds, boolean ignoreMissing) { this.credentialIds = new ArrayList(new LinkedHashSet(credentialIds)); this.ignoreMissing = ignoreMissing; + this.socketPath = null; } /** @@ -137,7 +162,7 @@ public SSHAgentBuildWrapper(List credentialIds, boolean ignoreMissing) { */ private Object readResolve() throws ObjectStreamException { if (user != null) { - return new SSHAgentBuildWrapper(Collections.singletonList(user),false); + return new SSHAgentBuildWrapper(Collections.singletonList(user),false,null); } return this; } @@ -172,7 +197,16 @@ public List getCredentialIds() { */ @SuppressWarnings("unused") // used via stapler public boolean isIgnoreMissing() { - return ignoreMissing; + return ignoreMissing; + } + + /** + * When null or blank ssh-agent will use its defaults. + * @return SSH auth socket path. When null or blank ssh-agent will use its defaults + */ + @SuppressWarnings("unused") // used via stapler + public String getSocketPath() { + return socketPath; } /** @@ -367,7 +401,7 @@ public SSHAgentEnvironment(Launcher launcher, BuildListener listener, @CheckForN try { listener.getLogger().println("[ssh-agent] " + factory.getDisplayName()); agent = factory.start(new SingletonLauncherProvider(launcher), listener, - workspace != null ? SSHAgentStepExecution.tempDir(workspace) : null); + workspace != null ? SSHAgentStepExecution.tempDir(workspace) : null, socketPath); break; } catch (Throwable t) { faults.put(factory.getDisplayName(), t); @@ -405,7 +439,7 @@ public void add(SSHUserPrivateKey key) throws IOException, InterruptedException agent.addIdentity(privateKey, effectivePassphrase, description(key), listener); } } - + /** * {@inheritDoc} */ diff --git a/src/main/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStep.java b/src/main/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStep.java index aa3fa99..7bc6f98 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStep.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStep.java @@ -36,6 +36,11 @@ public class SSHAgentStep extends AbstractStepImpl implements Serializable { * By the fault is false. Initialized in the constructor. */ private boolean ignoreMissing; + + /** + * Path to use for SSH_AUTH_SOCK. If null or blank, ssh-agent default will be used. + */ + private String socketPath; /** * Default parameterized constructor. @@ -46,6 +51,7 @@ public class SSHAgentStep extends AbstractStepImpl implements Serializable { public SSHAgentStep(final List credentials) { this.credentials = credentials; this.ignoreMissing = false; + this.socketPath = null; } @Extension @@ -96,7 +102,16 @@ public void setIgnoreMissing(final boolean ignoreMissing) { } public boolean isIgnoreMissing() { - return ignoreMissing; + return ignoreMissing; + } + + @DataBoundSetter + public void setSocketPath(final String socketPath) { + this.socketPath = socketPath; + } + + public String getSocketPath() { + return socketPath; } public List getCredentials() { diff --git a/src/main/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStepExecution.java b/src/main/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStepExecution.java index d36e18d..2a60243 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStepExecution.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStepExecution.java @@ -154,7 +154,7 @@ private void initRemoteAgent() throws IOException, InterruptedException { if (factory.isSupported(launcher, listener)) { try { listener.getLogger().println("[ssh-agent] " + factory.getDisplayName()); - agent = factory.start(this, listener, tempDir(workspace)); + agent = factory.start(this, listener, tempDir(workspace), step.getSocketPath()); break; } catch (Throwable t) { faults.put(factory.getDisplayName(), t); diff --git a/src/main/java/com/cloudbees/jenkins/plugins/sshagent/exec/ExecRemoteAgent.java b/src/main/java/com/cloudbees/jenkins/plugins/sshagent/exec/ExecRemoteAgent.java index 8cf8e5a..f30deff 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/sshagent/exec/ExecRemoteAgent.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/sshagent/exec/ExecRemoteAgent.java @@ -32,7 +32,9 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -56,12 +58,18 @@ public class ExecRemoteAgent implements RemoteAgent { private final LauncherProvider launcherProvider; - ExecRemoteAgent(LauncherProvider launcherProvider, TaskListener listener, FilePath temp) throws Exception { + ExecRemoteAgent(LauncherProvider launcherProvider, TaskListener listener, FilePath temp, String socketPath) throws Exception { this.temp = temp; this.launcherProvider = launcherProvider; ByteArrayOutputStream baos = new ByteArrayOutputStream(); - if (launcherProvider.getLauncher().launch().cmds("ssh-agent").stdout(baos).start() + List sshAgentCmds = new ArrayList(); + sshAgentCmds.add("ssh-agent"); + if ( socketPath != null && !socketPath.isEmpty()) { + sshAgentCmds.add("-a"); + sshAgentCmds.add(socketPath); + } + if (launcherProvider.getLauncher().launch().cmds(sshAgentCmds).stdout(baos).start() .joinWithTimeout(1, TimeUnit.MINUTES, listener) != 0) { String reason = new String(baos.toByteArray(), StandardCharsets.US_ASCII); throw new AbortException("Failed to run ssh-agent: " + reason); diff --git a/src/main/java/com/cloudbees/jenkins/plugins/sshagent/exec/ExecRemoteAgentFactory.java b/src/main/java/com/cloudbees/jenkins/plugins/sshagent/exec/ExecRemoteAgentFactory.java index c787f12..8e4bf8f 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/sshagent/exec/ExecRemoteAgentFactory.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/sshagent/exec/ExecRemoteAgentFactory.java @@ -79,8 +79,8 @@ public boolean isSupported(Launcher launcher, final TaskListener listener) { * {@inheritDoc} */ @Override - public RemoteAgent start(LauncherProvider launcherProvider, final TaskListener listener, FilePath temp) + public RemoteAgent start(LauncherProvider launcherProvider, final TaskListener listener, FilePath temp, String socketPath) throws Throwable { - return new ExecRemoteAgent(launcherProvider, listener, temp); + return new ExecRemoteAgent(launcherProvider, listener, temp, socketPath); } } diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/sshagent/SSHAgentBuildWrapper/config.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/sshagent/SSHAgentBuildWrapper/config.jelly index 7b840f6..9f4d832 100644 --- a/src/main/resources/com/cloudbees/jenkins/plugins/sshagent/SSHAgentBuildWrapper/config.jelly +++ b/src/main/resources/com/cloudbees/jenkins/plugins/sshagent/SSHAgentBuildWrapper/config.jelly @@ -31,4 +31,7 @@ + + + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStep/config.jelly b/src/main/resources/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStep/config.jelly index 81bb240..43cb744 100644 --- a/src/main/resources/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStep/config.jelly +++ b/src/main/resources/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStep/config.jelly @@ -32,5 +32,8 @@ + + + diff --git a/src/main/resources/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStep/help-socketPath.html b/src/main/resources/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStep/help-socketPath.html new file mode 100644 index 0000000..8c0d90f --- /dev/null +++ b/src/main/resources/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStep/help-socketPath.html @@ -0,0 +1,4 @@ +
+ When set to a value different than empty, SSH_AUTH_SOCK path will be set to it. Otherwise, ssh-agent defaults will + be used. +
diff --git a/src/test/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentBuildWrapperTest.java b/src/test/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentBuildWrapperTest.java index 414f518..63fcd0d 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentBuildWrapperTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentBuildWrapperTest.java @@ -224,6 +224,35 @@ public void sshAgentWithSpacesInWorkspacePath() throws Exception { @Test public void sshAgentWithTrickyPassphrase() throws Exception { + startMockSSHServer(); + + List credentialIds = new ArrayList(); + credentialIds.add(CREDENTIAL_ID); + + SSHUserPrivateKey key = new BasicSSHUserPrivateKey(CredentialsScope.GLOBAL, credentialIds.get(0), "cloudbees", + new BasicSSHUserPrivateKey.DirectEntryPrivateKeySource(getPrivateKey2()), "* .*", "test"); + SystemCredentialsProvider.getInstance().getCredentials().add(key); + SystemCredentialsProvider.getInstance().save(); + + FreeStyleProject job = r.createFreeStyleProject(); + job.setAssignedNode(r.createSlave()); + + SSHAgentBuildWrapper sshAgent = new SSHAgentBuildWrapper(credentialIds, false); + job.getBuildWrappersList().add(sshAgent); + + Shell shell = new Shell("set | grep SSH_AUTH_SOCK " + + "&& ssh-add -l " + + "&& ssh -o NoHostAuthenticationForLocalhost=yes -o StrictHostKeyChecking=no -p " + getAssignedPort() + + " -v -l cloudbees " + SSH_SERVER_HOST); + job.getBuildersList().add(shell); + + r.assertBuildStatusSuccess(job.scheduleBuild2(0)); + + stopMockSSHServer(); + } + + @Test + public void sshAgentWithCustomSocketPath() throws Exception { startMockSSHServer(); List credentialIds = new ArrayList(); @@ -237,10 +266,10 @@ public void sshAgentWithTrickyPassphrase() throws Exception { FreeStyleProject job = r.createFreeStyleProject(); job.setAssignedNode(r.createSlave()); - SSHAgentBuildWrapper sshAgent = new SSHAgentBuildWrapper(credentialIds, false); + SSHAgentBuildWrapper sshAgent = new SSHAgentBuildWrapper(credentialIds, false, "/tmp/custom.sock"); job.getBuildWrappersList().add(sshAgent); - Shell shell = new Shell("set | grep SSH_AUTH_SOCK " + Shell shell = new Shell("set | grep /tmp/custom.sock " + "&& ssh-add -l " + "&& ssh -o NoHostAuthenticationForLocalhost=yes -o StrictHostKeyChecking=no -p " + getAssignedPort() + " -v -l cloudbees " + SSH_SERVER_HOST); diff --git a/src/test/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStepWorkflowTest.java b/src/test/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStepWorkflowTest.java index e8e7b67..b5eef10 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStepWorkflowTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStepWorkflowTest.java @@ -270,5 +270,34 @@ public void sshAgentDocker() throws Exception { r.assertLogNotContains("cloudbees", b); }); } + + @Test + public void sshAgentAvailableWithCustomSockPath() throws Exception { + story.addStep(new Statement() { + @Override + public void evaluate() throws Throwable { + startMockSSHServer(); + + List credentialIds = new ArrayList(); + credentialIds.add(CREDENTIAL_ID); + + SSHUserPrivateKey key = new BasicSSHUserPrivateKey(CredentialsScope.GLOBAL, credentialIds.get(0), "cloudbees", + new BasicSSHUserPrivateKey.DirectEntryPrivateKeySource(getPrivateKey()), "cloudbees", "test"); + SystemCredentialsProvider.getInstance().getCredentials().add(key); + SystemCredentialsProvider.getInstance().save(); + WorkflowJob job = story.j.jenkins.createProject(WorkflowJob.class, "sshAgentAvailable"); + job.setDefinition(new CpsFlowDefinition("" + + "node('" + story.j.createSlave().getNodeName() + "') {\n" + + " sshagent (credentials: ['" + CREDENTIAL_ID + "'], socketPath: '/tmp/custom.sock') {\n" + + " sh 'set | grep /tmp/custom.sock && ls -l $SSH_AUTH_SOCK && ssh -o StrictHostKeyChecking=no -p " + getAssignedPort() + " -v -l cloudbees " + SSH_SERVER_HOST + "'\n" + + " }\n" + + "}\n", true) + ); + story.j.assertBuildStatusSuccess(job.scheduleBuild2(0)); + + stopMockSSHServer(); + } + }); + } }