Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ The complex example uses several devcontainer features, so the first `up` or `re
- When Docker Desktop host services are available, `devbox` can share the SSH agent without relying on a host-shell `SSH_AUTH_SOCK`.
- On Docker Desktop, `devbox` prefers the Docker-provided SSH agent socket over the host `SSH_AUTH_SOCK`, which avoids macOS launchd socket mount issues.
- `--allow-missing-ssh` starts the workspace without mounting an SSH agent and prints a warning instead of failing.
- `--no-ssh` skips installation and startup of the bundled SSH server but still shares the SSH agent and configures the other host integrations.
- `--no-ssh` skips starting the bundled SSH server but still installs the common container tools, shares the SSH agent, and configures the other host integrations.
- `--ssh` explicitly enables the bundled SSH server for a workspace whose saved state has SSH disabled.
- `devbox` stages a snapshot of the host `~/.ssh/known_hosts` before startup and skips injection with a warning when that file is missing, unreadable, empty, symlinked, or not a regular file.
- `devbox` tries to install the host public key from `~/.ssh/id_rsa.pub` for SSH key-based login inside the container; if that default file is missing, it simply skips that step.
Expand Down
44 changes: 25 additions & 19 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ async function handleUpLike(
console.log(
sshEnabled
? "Configuring SSH access inside the devcontainer..."
: "Configuring devcontainer access without installing the bundled SSH server...",
: "Configuring devcontainer access without starting the bundled SSH server...",
);
if (requiresSshAuthSockPermissionFix(environment.sshAuthSock)) {
console.log("Making the forwarded SSH agent socket accessible to the container user...");
Expand All @@ -344,15 +344,32 @@ async function handleUpLike(
console.log("Syncing Git author identity from the host into the devcontainer...");
await configureGitIdentity(upResult.containerId, environment.gitUserName, environment.gitUserEmail);
}
if (!sshEnabled && existingInspects.length > 0) {
const previousPorts = new Set<number>([
...getWorkspacePorts(state),
...(getManagedPortFromContainerName(existingInspects[0]?.Name) !== undefined
? [getManagedPortFromContainerName(existingInspects[0]?.Name)!]
: []),
]);
for (const previousPort of previousPorts) {
await stopManagedSshd(upResult.containerId, previousPort);
}
Comment thread
PabloZaiden marked this conversation as resolved.
}
if (sshEnabled) {
await stopManagedSshd(upResult.containerId, ports[0]);
await restoreRunnerHostKeys(upResult.containerId, remoteWorkspaceFolder);
const runnerCredentials = await runStepWithHeartbeat({
startMessage: "Installing and starting the SSH server inside the container (first run can take a bit)...",
heartbeatMessage: "Still installing and starting the SSH server",
successMessage: "SSH server is ready",
action: () => startRunner(upResult.containerId, ports[0], remoteWorkspaceFolder),
});
}
const runnerCredentials = await runStepWithHeartbeat({
startMessage: sshEnabled
? "Installing and starting the SSH server inside the container (first run can take a bit)..."
: "Installing bundled development tools inside the container (first run can take a bit)...",
heartbeatMessage: sshEnabled
? "Still installing and starting the SSH server"
: "Still installing bundled development tools",
successMessage: sshEnabled ? "SSH server is ready" : "Bundled development tools are ready",
action: () => startRunner(upResult.containerId, ports[0], remoteWorkspaceFolder, sshEnabled),
});
if (sshEnabled) {
if (resolvedSshPublicKey.publicKey) {
const sshUser = runnerCredentials.user ?? upResult.remoteUser;
if (!sshUser) {
Expand Down Expand Up @@ -381,18 +398,7 @@ async function handleUpLike(
console.log("Saving SSH server state for future runs...");
await persistRunnerHostKeys(upResult.containerId, remoteWorkspaceFolder);
} else {
if (existingInspects.length > 0) {
const previousPorts = new Set<number>([
...getWorkspacePorts(state),
...(getManagedPortFromContainerName(existingInspects[0]?.Name) !== undefined
? [getManagedPortFromContainerName(existingInspects[0]?.Name)!]
: []),
]);
for (const previousPort of previousPorts) {
await stopManagedSshd(upResult.containerId, previousPort);
}
}
console.log("Bundled SSH server installation skipped; published ports are ready for the devcontainer service.");
console.log("Bundled SSH server remains disabled; common container tools were installed.");
}

const workspaceState = createWorkspaceState({
Expand Down
2 changes: 1 addition & 1 deletion src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export function helpText(): string {
" -p, --port <port> Publish the same port on host and container.",
" --ports <count> Publish this many ports, auto-selecting later ports when needed.",
" --allow-missing-ssh Continue without SSH agent sharing when unavailable.",
" --no-ssh Do not install or start devbox's bundled SSH server.",
" --no-ssh Do not start devbox's bundled SSH server; install the common container tools.",
" --ssh Install and start devbox's bundled SSH server.",
" --startup-command <command> Run and persist a command after the container starts.",
" --no-startup-command Clear the persisted post-start command.",
Expand Down
33 changes: 26 additions & 7 deletions src/runner/ssh-server.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail

START_SSH_SERVER="${START_SSH_SERVER:-1}"
if [[ "$START_SSH_SERVER" != "0" && "$START_SSH_SERVER" != "1" ]]; then
echo "ERROR: START_SSH_SERVER must be 0 or 1" >&2
exit 1
fi
export START_SSH_SERVER

# get the latest vscode-generated auth sock, if available
VSCODE_SSH_AUTH_SOCK=""
mapfile -t vscode_ssh_socks < <(compgen -G "/tmp/vscode-ssh*.sock" || true)
Expand Down Expand Up @@ -57,7 +64,7 @@ as_root_bash() {
fi

if command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
sudo -n bash -lc "$cmd"
sudo -n env "START_SSH_SERVER=${START_SSH_SERVER}" bash -lc "$cmd"
return
fi

Expand All @@ -76,13 +83,18 @@ resolve_path() {
# Prefer the non-root invoker when using sudo
CURRENT_USER="${SUDO_USER:-$(id -un)}"

# Install deps and prep sshd dirs
# Install common dependencies and prep sshd dirs when enabled
as_root_bash '
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive

missing_packages=()
for package in openssh-server uuid-runtime dtach tmux git; do
packages=(dtach tmux git)
if [[ "$START_SSH_SERVER" == "1" ]]; then
packages+=(openssh-server uuid-runtime)
fi

for package in "${packages[@]}"; do
if ! dpkg-query -W -f="\${db:Status-Status}" "$package" 2>/dev/null | grep -qx installed; then
missing_packages+=("$package")
fi
Expand All @@ -96,10 +108,12 @@ else
echo "All apt packages already installed; skipping apt-get install."
fi

mkdir -p /var/run/sshd
chown root:root /var/run/sshd
chmod 0755 /var/run/sshd
mkdir -p /etc/ssh/sshd_config.d
if [[ "$START_SSH_SERVER" == "1" ]]; then
mkdir -p /var/run/sshd
chown root:root /var/run/sshd
chmod 0755 /var/run/sshd
mkdir -p /etc/ssh/sshd_config.d
fi
'

# install GitHub CLI if missing
Expand Down Expand Up @@ -152,6 +166,11 @@ echo "[install]
minimumReleaseAge = 259200" > "$HOME/.bunfig.toml"
append_unique_line "$HOME/.tmux.conf" "set -g mouse on"

if [[ "$START_SSH_SERVER" != "1" ]]; then
echo "Bundled SSH server disabled; common container tools installed."
exit 0
fi

# Use existing password if present, otherwise create it once
if [[ -f "$CRED_FILE" ]]; then
PASS="$(tr -d '\r\n' < "$CRED_FILE")"
Expand Down
22 changes: 17 additions & 5 deletions src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -843,8 +843,9 @@ export async function startRunner(
containerId: string,
port: number,
remoteWorkspaceFolder: string,
startSshServer = true,
): Promise<RunnerCredentials> {
const script = buildStartRunnerScript(port, remoteWorkspaceFolder);
const script = buildStartRunnerScript(port, remoteWorkspaceFolder, startSshServer);
const result = await devcontainerExec(containerId, script, { quiet: true, stdin: bundledRunnerScript });
const summaryLines = getRunnerSummaryLines(result.stdout);
const parsedSummary = parseRunnerCredentials(summaryLines.join("\n"));
Expand All @@ -854,7 +855,7 @@ export async function startRunner(
for (const line of summaryLines) {
console.log(` ${line}`);
}
} else {
} else if (startSshServer) {
const output = result.stdout.trim();
if (output) {
console.log(output);
Expand All @@ -864,7 +865,7 @@ export async function startRunner(
return {
user: parsedSummary.user,
password: parsedSummary.password,
sshPort: parsedSummary.sshPort ?? port,
sshPort: parsedSummary.sshPort ?? (startSshServer ? port : null),
permitRootLogin: parsedSummary.permitRootLogin,
};
}
Expand All @@ -882,8 +883,19 @@ export function buildStartupCommandScript(command: string): string {
return trimmed;
}

export function buildStartRunnerScript(port: number, remoteWorkspaceFolder: string): string {
return `env SSH_PORT=${quoteShell(String(port))} CRED_FILE=${quoteShell(getRunnerCredFile(remoteWorkspaceFolder))} bash -s`;
export function buildStartRunnerScript(
port: number,
remoteWorkspaceFolder: string,
startSshServer = true,
): string {
if (!startSshServer) {
return `env START_SSH_SERVER=${quoteShell("0")} bash -s`;
}

return (
`env START_SSH_SERVER=${quoteShell("1")} SSH_PORT=${quoteShell(String(port))} ` +
`CRED_FILE=${quoteShell(getRunnerCredFile(remoteWorkspaceFolder))} bash -s`
);
}

export async function persistRunnerHostKeys(
Expand Down
9 changes: 8 additions & 1 deletion tests/examples.live.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ describe("example workspaces (real devcontainers)", () => {

expect(up.exitCode).toBe(0);
expect(up.stdout).toContain(`Using ports ${fixture.port},`);
expect(up.stdout).toContain("Bundled SSH server installation skipped");
expect(up.stdout).toContain("Bundled SSH server remains disabled; common container tools were installed.");
expect(up.stdout).not.toContain("SSH server:");

const state = await readJson(fixture.statePath);
Expand All @@ -244,6 +244,13 @@ describe("example workspaces (real devcontainers)", () => {
expect(state.ports[0]).toBe(fixture.port);
expect(state.sshEnabled).toBe(false);

const commonTools = execInContainer(
fixture,
containerId,
"command -v gh && node --version && npm --version && command -v fresh && git --version && tmux -V && command -v dtach",
);
expect(commonTools.exitCode).toBe(0);

const inspect = inspectContainer(fixture, containerId);
for (const port of state.ports as number[]) {
expect(getPublishedHostPort(inspect, port)).toBe(String(port));
Expand Down
Loading