Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<String> credentialIds, boolean ignoreMissing, String socketPath) {
this.credentialIds = new ArrayList<String>(new LinkedHashSet<String>(credentialIds));
this.ignoreMissing = ignoreMissing;
this.socketPath = socketPath;
}

/**
* Constructs a new instance.
*
Expand All @@ -128,6 +152,7 @@ public SSHAgentBuildWrapper(CredentialHolder[] credentialHolders, boolean ignore
public SSHAgentBuildWrapper(List<String> credentialIds, boolean ignoreMissing) {
this.credentialIds = new ArrayList<String>(new LinkedHashSet<String>(credentialIds));
this.ignoreMissing = ignoreMissing;
this.socketPath = null;
}

/**
Expand All @@ -137,7 +162,7 @@ public SSHAgentBuildWrapper(List<String> 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;
}
Expand Down Expand Up @@ -172,7 +197,16 @@ public List<String> 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;
}

/**
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -405,7 +439,7 @@ public void add(SSHUserPrivateKey key) throws IOException, InterruptedException
agent.addIdentity(privateKey, effectivePassphrase, description(key), listener);
}
}

/**
* {@inheritDoc}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -46,6 +51,7 @@ public class SSHAgentStep extends AbstractStepImpl implements Serializable {
public SSHAgentStep(final List<String> credentials) {
this.credentials = credentials;
this.ignoreMissing = false;
this.socketPath = null;
}

@Extension
Expand Down Expand Up @@ -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<String> getCredentials() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<String> sshAgentCmds = new ArrayList<String>();
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,7 @@
<f:entry field="ignoreMissing">
<f:checkbox title="${%Ignore missing credentials}" default="false"/>
</f:entry>
<f:entry field="socketPath">
<f:textbox title="${%SSH_AUTH_SOCK path}" default="" />
</f:entry>
</j:jelly>
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,8 @@
<f:entry field="ignoreMissing">
<f:checkbox title="${%Ignore missing credentials}" default="false" />
</f:entry>
<f:entry field="socketPath">
<f:textbox title="${%SSH_AUTH_SOCK path}" default="" />
</f:entry>

</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div>
When set to a value different than empty, SSH_AUTH_SOCK path will be set to it. Otherwise, ssh-agent defaults will
be used.
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,35 @@ public void sshAgentWithSpacesInWorkspacePath() throws Exception {

@Test
public void sshAgentWithTrickyPassphrase() throws Exception {
startMockSSHServer();

List<String> credentialIds = new ArrayList<String>();
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<String> credentialIds = new ArrayList<String>();
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> credentialIds = new ArrayList<String>();
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();
}
});
}
}