Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Improve support for default ssh docker container #763

Closed
wants to merge 6 commits into from
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 @@ -39,13 +39,15 @@
import hudson.util.ListBoxModel;
import io.jenkins.docker.DockerTransientNode;
import io.jenkins.docker.client.DockerAPI;
import io.jenkins.docker.client.DockerEnvUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -250,24 +252,8 @@ public String toString() {
@Override
public void beforeContainerCreated(DockerAPI api, String workdir, CreateContainerCmd cmd)
throws IOException, InterruptedException {
sshKeyStrategy.beforeContainerCreated(this, api, workdir, cmd);
// TODO define a strategy for SSHD process configuration so we support more than openssh's sshd
final String[] cmdArray = cmd.getCmd();
if (cmdArray == null || cmdArray.length == 0) {
if (sshKeyStrategy.getInjectedKey() != null) {
cmd.withCmd(
"/usr/sbin/sshd",
"-D",
"-p",
String.valueOf(port),
// override sshd_config to force retrieval of InstanceIdentity public for as authentication
"-o",
"AuthorizedKeysCommand=/root/authorized_key",
"-o",
"AuthorizedKeysCommandUser=root");
} else {
cmd.withCmd("/usr/sbin/sshd", "-D", "-p", String.valueOf(port));
}
}
cmd.withPortSpecs(port + "/tcp");
final PortBinding sshPortBinding = PortBinding.parse(":" + port);
HostConfig hostConfig = cmd.getHostConfig();
Expand All @@ -288,32 +274,7 @@ public void beforeContainerCreated(DockerAPI api, String workdir, CreateContaine
@Override
public void beforeContainerStarted(DockerAPI api, String workdir, DockerTransientNode node)
throws IOException, InterruptedException {
final String key = sshKeyStrategy.getInjectedKey();
if (key != null) {
final String containerId = node.getContainerId();
final String authorizedKeysCommand = "#!/bin/sh\n"
+ "[ \"$1\" = \"" + sshKeyStrategy.getUser() + "\" ] "
+ "&& echo '" + key + "'"
+ "|| :";
final byte[] authorizedKeysCommandAsBytes = authorizedKeysCommand.getBytes(StandardCharsets.UTF_8);
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
TarArchiveOutputStream tar = new TarArchiveOutputStream(bos)) {
TarArchiveEntry entry = new TarArchiveEntry("authorized_key");
entry.setSize(authorizedKeysCommandAsBytes.length);
entry.setMode(0700);
tar.putArchiveEntry(entry);
tar.write(authorizedKeysCommandAsBytes);
tar.closeArchiveEntry();
tar.close();
try (InputStream is = new ByteArrayInputStream(bos.toByteArray());
DockerClient client = api.getClient()) {
client.copyArchiveToContainerCmd(containerId)
.withTarInputStream(is)
.withRemotePath("/root")
.exec();
}
}
}
sshKeyStrategy.beforeContainerStarted(this, api, workdir, node);
}

@Override
Expand Down Expand Up @@ -425,7 +386,13 @@ public List getSSHKeyStrategyDescriptors() {
}

public abstract static class SSHKeyStrategy extends AbstractDescribableImpl<SSHKeyStrategy> {
public abstract String getInjectedKey() throws IOException;
public abstract void beforeContainerCreated(
DockerComputerSSHConnector connector, DockerAPI api, String workdir, CreateContainerCmd cmd)
throws IOException, InterruptedException;

public abstract void beforeContainerStarted(
DockerComputerSSHConnector connector, DockerAPI api, String workdir, DockerTransientNode node)
throws IOException, InterruptedException;

public abstract String getUser();

Expand All @@ -442,11 +409,10 @@ public abstract ComputerLauncher getSSHLauncher(
public abstract String toString(); // force subclasses to implement this
}

public static class InjectSSHKey extends SSHKeyStrategy {
private final String user;
public abstract static class KeyInjectionStrategy extends SSHKeyStrategy {
protected final String user;

@DataBoundConstructor
public InjectSSHKey(String user) {
protected KeyInjectionStrategy(String user) {
this.user = user;
}

Expand Down Expand Up @@ -503,19 +469,126 @@ public ComputerLauncher getSSHLauncher(InetSocketAddress address, DockerComputer
new NonVerifyingKeyVerificationStrategy());
}

@Override
public String getInjectedKey() throws IOException {
protected String getInjectedKey() throws IOException {
InstanceIdentity id = InstanceIdentity.get();
return "ssh-rsa "
+ Base64.getEncoder().encodeToString(new RSAKeyAlgorithm().encodePublicKey(id.getPublic()));
}
}

public static class InjectSSHKey extends KeyInjectionStrategy {
@DataBoundConstructor
public InjectSSHKey(String user) {
super(user);
}

@Override
public void beforeContainerCreated(
DockerComputerSSHConnector connector, DockerAPI api, String workdir, CreateContainerCmd cmd)
throws IOException, InterruptedException {
final int port = connector.getPort();
final boolean portIsNotDefault = port != 22;
final String injectedKey = getInjectedKey();
final String[] cmdArray = cmd.getCmd();
if (cmdArray == null || cmdArray.length == 0) {
if (portIsNotDefault) {
cmd.withCmd(
"/usr/sbin/sshd",
"-D",
"-p",
String.valueOf(port),
// override sshd_config to force retrieval of InstanceIdentity public for as authentication
"-o",
"AuthorizedKeysCommand=/root/authorized_key",
"-o",
"AuthorizedKeysCommandUser=root");
} else {
cmd.withCmd(
"/usr/sbin/sshd",
"-D",
// override sshd_config to force retrieval of InstanceIdentity public for as authentication
"-o",
"AuthorizedKeysCommand=/root/authorized_key",
"-o",
"AuthorizedKeysCommandUser=root");
}
}
if (injectedKey != null) {
DockerEnvUtils.addEnvToCmd("JENKINS_AGENT_SSH_PUBKEY", injectedKey, cmd);
}
}

@Override
public void beforeContainerStarted(
DockerComputerSSHConnector connector, DockerAPI api, String workdir, DockerTransientNode node)
throws IOException, InterruptedException {
final String key = getInjectedKey();
final String containerId = node.getContainerId();
final String authorizedKeysCommand =
"#!/bin/sh\n" + "[ \"$1\" = \"" + getUser() + "\" ] " + "&& echo '" + key + "'" + "|| :";
final byte[] authorizedKeysCommandAsBytes = authorizedKeysCommand.getBytes(StandardCharsets.UTF_8);
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
TarArchiveOutputStream tar = new TarArchiveOutputStream(bos)) {
TarArchiveEntry entry = new TarArchiveEntry("authorized_key");
entry.setSize(authorizedKeysCommandAsBytes.length);
entry.setMode(0700);
tar.putArchiveEntry(entry);
tar.write(authorizedKeysCommandAsBytes);
tar.closeArchiveEntry();
tar.close();
try (InputStream is = new ByteArrayInputStream(bos.toByteArray());
DockerClient client = api.getClient()) {
client.copyArchiveToContainerCmd(containerId)
.withTarInputStream(is)
.withRemotePath("/root")
.exec();
}
}
}

@Extension
public static final class DescriptorImpl extends Descriptor<SSHKeyStrategy> {
@NonNull
@Override
public String getDisplayName() {
return "Inject SSH key";
return "Inject SSH key using SSH AuthorizedKeysCommand option";
}
}
}

public static class InjectSSHKeyAsContainerArgument extends KeyInjectionStrategy {
@DataBoundConstructor
public InjectSSHKeyAsContainerArgument(String user) {
super(user);
}

@Override
public void beforeContainerCreated(
DockerComputerSSHConnector connector, DockerAPI api, String workdir, CreateContainerCmd cmd)
throws IOException, InterruptedException {
final String injectedKey = getInjectedKey();
final String[] cmdArray = cmd.getCmd();
if (cmdArray == null || cmdArray.length == 0) {
cmd.withCmd(injectedKey);
} else {
List<String> l = new ArrayList<>(cmdArray.length + 1);
l.add(injectedKey);
l.addAll(List.of(cmdArray));
cmd.withCmd(l.toArray(new String[l.size()]));
}
}

@Override
public void beforeContainerStarted(
DockerComputerSSHConnector connector, DockerAPI api, String workdir, DockerTransientNode node)
throws IOException, InterruptedException {}

@Extension
public static final class DescriptorImpl extends Descriptor<SSHKeyStrategy> {
@NonNull
@Override
public String getDisplayName() {
return "Inject SSH key as 1st container argument";
}
}
}
Expand Down Expand Up @@ -592,8 +665,17 @@ public ComputerLauncher getSSHLauncher(InetSocketAddress address, DockerComputer
}

@Override
public String getInjectedKey() throws IOException {
return null;
public void beforeContainerCreated(
DockerComputerSSHConnector connector, DockerAPI api, String workdir, CreateContainerCmd cmd)
throws IOException, InterruptedException {
// TODO Auto-generated method stub
}

@Override
public void beforeContainerStarted(
DockerComputerSSHConnector connector, DockerAPI api, String workdir, DockerTransientNode node)
throws IOException, InterruptedException {
// TODO Auto-generated method stub
}

@Extension
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:f="/lib/form" >

<f:block>
A dedicated SSH (public) key (based on Jenkins master's unique Identity)
will be passed to the container as the first argument, and the matching
private key given to the Jenkins SSH launcher code to use to log in with.
<br/>
<u>Note:</u> This is what the official Jenkins docker-ssh-slave image expects.
<br/>
<u>Note:</u> If you are using the Windows version of the docker-ssh-slave,
you may find that you also need to set
(in the "Advanced" SSH connection properties below)
the "Prefix Start Slave Command" to
<code>powershell -Command "cd C:\Users\jenkins ; java -jar remoting.jar -workDir C:\Users\jenkins -jar-cache C:\Users\jenkins/remoting/jarCache" ; exit 0 ; rem '</code>
and "Suffix Start Slave Command" to <code>'</code>
</f:block>

<f:entry title="${%User}" field="user">
<f:textbox default="jenkins" />
</f:entry>

</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div>
The user that Jenkins should tell the SSH Launcher to log in as.
<br/>
<u>Note:</u> The user must pre-exist in container image and
changes here may also require changes to other fields that
specify container filesystem locations.
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,22 @@ public void connectAgentViaSSHUsingInjectSshKey() throws Exception {
should_connect_agent(template);
}

@Test
public void connectAgentViaSSHUsingInjectSshKeyAsContainerArgument() throws Exception {
final DockerComputerSSHConnector.SSHKeyStrategy sshKeyStrategy =
new DockerComputerSSHConnector.InjectSSHKeyAsContainerArgument(COMMON_IMAGE_USERNAME);
final DockerComputerSSHConnector connector = new DockerComputerSSHConnector(sshKeyStrategy);
connector.setJavaPath(SSH_AGENT_IMAGE_JAVAPATH);
final DockerTemplate template = new DockerTemplate(
new DockerTemplateBase(SSH_AGENT_IMAGE_IMAGENAME),
connector,
getLabelForTemplate(),
COMMON_IMAGE_HOMEDIR,
INSTANCE_CAP);
template.setName("connectAgentViaSSHUsingInjectSshKeyAsContainerArgument");
should_connect_agent(template);
}

@Test
public void connectAgentViaSSHUsingCredentialsKey() throws Exception {
final InstanceIdentity id = InstanceIdentity.get();
Expand Down