feat: implement deploy command with SSH support and context management
This commit is contained in:
99
src/lib/ssh.ts
Normal file
99
src/lib/ssh.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { appendFileSync, mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { consola } from "consola";
|
||||
import { execa } from "execa";
|
||||
import type { WPopContext } from "./context.js";
|
||||
import type { DeployEnv } from "./env.js";
|
||||
import { run } from "./run.js";
|
||||
|
||||
export type SshConfig = {
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
privateKey?: string;
|
||||
};
|
||||
|
||||
export type PreparedSsh = SshConfig & {
|
||||
identityFile?: string;
|
||||
};
|
||||
|
||||
export function createSshConfig(env: DeployEnv): SshConfig {
|
||||
return {
|
||||
host: env.REMOTE_HOST,
|
||||
port: env.REMOTE_PORT,
|
||||
user: env.REMOTE_USER,
|
||||
privateKey: env.SSH_PRIVATE_KEY,
|
||||
};
|
||||
}
|
||||
|
||||
export async function prepareSsh(context: WPopContext, config: SshConfig): Promise<PreparedSsh> {
|
||||
const prepared: PreparedSsh = {
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
user: config.user,
|
||||
};
|
||||
|
||||
if (config.privateKey) {
|
||||
prepared.identityFile = writePrivateKey(context, config.privateKey);
|
||||
}
|
||||
|
||||
await addKnownHost(context, prepared);
|
||||
await verifySshAuth(context, prepared);
|
||||
|
||||
return prepared;
|
||||
}
|
||||
|
||||
export function sshTarget(config: SshConfig): string {
|
||||
return `${config.user}@${config.host}`;
|
||||
}
|
||||
|
||||
export function sshArgs(config: PreparedSsh): string[] {
|
||||
const args = ["-p", String(config.port)];
|
||||
|
||||
if (config.identityFile) {
|
||||
args.push("-i", config.identityFile);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export function rsyncSshShell(config: PreparedSsh): string {
|
||||
return ["ssh", ...sshArgs(config)].join(" ");
|
||||
}
|
||||
|
||||
function writePrivateKey(context: WPopContext, privateKey: string): string | undefined {
|
||||
consola.info("Using SSH private key from SSH_PRIVATE_KEY");
|
||||
|
||||
if (context.dryRun) {
|
||||
return "/tmp/wpop-ssh-key";
|
||||
}
|
||||
|
||||
const keyDir = mkdtempSync(join(tmpdir(), "wpop-ssh-"));
|
||||
const keyPath = join(keyDir, "id_ed25519");
|
||||
writeFileSync(keyPath, privateKey.endsWith("\n") ? privateKey : `${privateKey}\n`, {
|
||||
mode: 0o600,
|
||||
});
|
||||
|
||||
return keyPath;
|
||||
}
|
||||
|
||||
async function addKnownHost(context: WPopContext, config: PreparedSsh): Promise<void> {
|
||||
consola.info(`Scanning SSH host key for ${config.host}`);
|
||||
|
||||
if (context.dryRun) {
|
||||
await run(context, "ssh-keyscan", ["-p", String(config.port), "-H", config.host]);
|
||||
return;
|
||||
}
|
||||
|
||||
const { stdout } = await execa("ssh-keyscan", ["-p", String(config.port), "-H", config.host]);
|
||||
const sshDir = join(process.env.HOME ?? context.cwd, ".ssh");
|
||||
mkdirSync(sshDir, { mode: 0o700, recursive: true });
|
||||
appendFileSync(join(sshDir, "known_hosts"), `${stdout}\n`);
|
||||
}
|
||||
|
||||
async function verifySshAuth(context: WPopContext, config: PreparedSsh): Promise<void> {
|
||||
consola.info(`Verifying SSH access to ${sshTarget(config)}`);
|
||||
|
||||
await run(context, "ssh", [...sshArgs(config), "-o", "BatchMode=yes", sshTarget(config), "true"]);
|
||||
}
|
||||
Reference in New Issue
Block a user