Skip to content
Merged
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
122 changes: 63 additions & 59 deletions src/lib/nodes/ssh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ import chalk from "chalk";
import ora from "ora";
import { Shescape } from "shescape";

import { getAuthToken } from "../../helpers/config.ts";
import { apiClient } from "../../apiClient.ts";
import { getAuthToken, loadConfig } from "../../helpers/config.ts";
import {
logAndQuit,
logSessionTokenExpiredAndQuit,
} from "../../helpers/errors.ts";
import { getApiUrl } from "../../helpers/urls.ts";
import { handleNodesError, nodesClient } from "../../nodesClient.ts";
import type { components } from "../../schema.ts";
import { jsonOption } from "./utils.ts";

type SshInfo = components["schemas"]["vmorch_GetSshResponse"];

const ssh = new Command("ssh")
.description(`SSH into a VM on a node.

Expand Down Expand Up @@ -46,7 +49,7 @@ Examples:

\x1b[2m# SSH with a specific username\x1b[0m
$ sf nodes ssh jenson@my-node

\x1b[2m# SSH directly to a VM ID\x1b[0m
$ sf nodes ssh root@vm_xxxxxxxxxxxxxxxxxxxxx
`,
Expand All @@ -67,75 +70,76 @@ Examples:
logAndQuit(`Invalid SSH destination string: ${destination}`);
}

let vmId: string;
const sshSpinner = ora("Fetching SSH information...").start();
const config = await loadConfig();
const token = await getAuthToken();

let hostKeyAlias = "";
let data: SshInfo | undefined;

// If the ID doesn't start with vm_, assume it's a node name/ID
// Try v2 endpoint for non-vm_ IDs
if (!nodeOrVmId.startsWith("vm_")) {
const client = await nodesClient();
const spinner = ora("Fetching node information...").start();
const v2Response = await fetch(
`${config.api_url}/v2/nodes/${nodeOrVmId}/ssh`,
{
method: "GET",
headers: { Authorization: `Bearer ${token}` },
},
);

try {
const node = await client.nodes.get(nodeOrVmId);
spinner.succeed(`Node found for name ${chalk.cyan(nodeOrVmId)}.`);

if (!node?.current_vm) {
spinner.fail(
`Node ${chalk.cyan(
nodeOrVmId,
)} does not have a current VM. VMs can take up to 5-10 minutes to spin up.`,
);
process.exit(1);
}
if (v2Response.ok) {
data = await v2Response.json();
hostKeyAlias = `${nodeOrVmId}.v2.nodes.sfcompute.dev`;
}
}

vmId = node.current_vm.id;
} catch {
spinner.info(
`No node found for name ${chalk.cyan(
nodeOrVmId,
)}. Interpreting as VM ID...`,
);
// Fall back to v0 flow if v2 didn't resolve
if (!data) {
let vmId: string;

if (!nodeOrVmId.startsWith("vm_")) {
const client = await nodesClient();
try {
const node = await client.nodes.get(nodeOrVmId);
if (!node?.current_vm) {
sshSpinner.fail(
`Node ${chalk.cyan(
nodeOrVmId,
)} does not have a current VM. VMs can take up to 5-10 minutes to spin up.`,
);
process.exit(1);
}
vmId = node.current_vm.id;
} catch {
vmId = nodeOrVmId;
}
} else {
vmId = nodeOrVmId;
}
} else {
vmId = nodeOrVmId;
}

const sshSpinner = ora("Fetching SSH information...").start();
const baseUrl = await getApiUrl("vms_ssh_get");
const params = new URLSearchParams();
params.append("vm_id", vmId);
const url = `${baseUrl}?${params.toString()}`;
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${await getAuthToken()}`,
},
});
const client = await apiClient(token);
const { response, data: sshData } = await client.GET("/v0/vms/ssh", {
params: { query: { vm_id: vmId } },
});

if (!response.ok) {
if (response.status === 401) {
sshSpinner.stop();
logSessionTokenExpiredAndQuit();
}

sshSpinner.fail(
`Failed to retrieve SSH information for ${chalk.cyan(
vmId,
)}: ${response.statusText}`,
);
process.exit(1);
if (!response.ok || !sshData) {
sshSpinner.fail(
`Failed to retrieve SSH information for ${chalk.cyan(
vmId,
)}: ${response.statusText}`,
);
process.exit(1);
}

data = sshData;
hostKeyAlias = `${vmId}.vms.sfcompute.dev`;
}

const data = (await response.json()) as {
ssh_hostname: string;
ssh_port: number;
ssh_host_keys:
| {
key_type: string;
base64_encoded_key: string;
}[]
| undefined;
};
sshSpinner.succeed("SSH information fetched successfully.");

if (options.json) {
Expand All @@ -162,7 +166,7 @@ Examples:
let knownHostsCommand = ["/usr/bin/env", "printf", "%s %s %s\\n"];
for (const sshHostKey of sshHostKeys) {
knownHostsCommand = knownHostsCommand.concat([
`${vmId}.vms.sfcompute.dev`,
hostKeyAlias,
sshHostKey.key_type,
sshHostKey.base64_encoded_key,
]);
Expand All @@ -177,7 +181,7 @@ Examples:
cmd = cmd.concat(["-o", `KnownHostsCommand=${knownHostsCommand_str}`]);
}

cmd = cmd.concat(["-o", `HostKeyAlias=${vmId}.vms.sfcompute.dev`]);
cmd = cmd.concat(["-o", `HostKeyAlias=${hostKeyAlias}`]);
cmd = cmd.concat([sshDestination]);

let shescape: undefined | Shescape;
Expand Down