/** * One-time bootstrap entrypoint for a new host. * It applies the server and firewall targets, waits for cloud-init, and finishes setup over public SSH. * Use this once per host, then return to `bun run tf apply`. */ import { $ } from "bun"; import { mkdtemp, rm } from "node:fs/promises"; import { join } from "node:path"; import { envFrom, remoteOperations as defaultBootstrapRemoteOperations, requireEnvVarsFrom, shellQuote, type EnvSource, type RemoteOperations, type RemoteTarget, } from "./lib"; import { buildTerraformBaseEnvironment, type TerraformEnvironment, } from "./tf"; /** Runs one Terraform command. */ export type BootstrapCommandRunner = ( args: readonly string[], terraformEnvironment: TerraformEnvironment, ) => Promise; /** Reads one Terraform output. */ export type TerraformOutputReader = ( outputName: string, terraformEnvironment: TerraformEnvironment, ) => Promise; /** Narrow remote surface used by the CLI. */ export type BootstrapRemoteOperations = Pick; /** Line-oriented output sink. */ export type OutputWriter = (line: string) => void; /** Retry sleep function. */ export type SleepFunction = (milliseconds: number) => Promise; const bootstrapRequiredEnvNames = ["TAILSCALE_AUTH_KEY"] as const; const bootstrapTargetArgs = [ "-target=hcloud_firewall.knot_public", "-target=hcloud_server.knot", ] as const; const bootstrapReadinessCommand = "test -f /var/lib/cloud/instance/boot-finished"; const bootstrapWaitPollMilliseconds = 5_000; const bootstrapWaitTimeoutMilliseconds = 10 * 60 * 1_000; /** Returns the usage text. */ export function usage(): string { return [ "Usage:", " bun run bootstrap [terraform-apply args...]", "", "Provision the server and firewall, then bootstrap the host over public SSH on port 22.", ].join("\n"); } /** Builds the Terraform apply arguments for bootstrap. */ export function buildBootstrapApplyArgs( args: readonly string[] = [], ): readonly string[] { const forwardedArgs = args[0] === "--" ? args.slice(1) : args; return ["apply", ...bootstrapTargetArgs, ...forwardedArgs]; } /** Builds the remote bootstrap command. */ export function buildBootstrapCommand(): string { const script = [ "trap 'rm -f /root/bootstrap/tailscale-auth.key' EXIT", "install -d -m 0700 /root/bootstrap", "install -d -m 0755 /etc/apt/keyrings", "curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc", "chmod a+r /etc/apt/keyrings/docker.asc", 'architecture="$(dpkg --print-architecture)"', "printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable\\n' \"$architecture\" >/etc/apt/sources.list.d/docker.list", "curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.noarmor.gpg -o /usr/share/keyrings/tailscale-archive-keyring.gpg", "curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.tailscale-keyring.list -o /etc/apt/sources.list.d/tailscale.list", "apt-get update", "apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin tailscale", "systemctl enable --now docker containerd tailscaled", 'tailscale up --reset --auth-key="$(cat /root/bootstrap/tailscale-auth.key)"', "printf 'Port 2222\\n' >/etc/ssh/sshd_config.d/70-knot-admin.conf", "if systemctl cat ssh.socket >/dev/null 2>&1; then systemctl disable --now ssh.socket; fi", "systemctl enable ssh.service", "systemctl restart ssh.service", "ss -H -ltnp 'sport = :22' | sed -n 's/.*pid=\\([0-9][0-9]*\\).*/\\1/p' | sort -u | xargs -r kill", ].join(";\n"); return `sh -ceu ${shellQuote(script)}`; } /** Builds the Terraform env for bootstrap. */ export function buildBootstrapTerraformEnvironment( envSource: EnvSource = Bun.env, ): TerraformEnvironment { requireEnvVarsFrom(envSource, bootstrapRequiredEnvNames); return buildTerraformBaseEnvironment(envSource); } /** Waits for cloud-init to finish. */ export async function waitForBootstrapReadiness( target: RemoteTarget, bootstrapRemoteOperations: BootstrapRemoteOperations = defaultBootstrapRemoteOperations, outputWriter: OutputWriter = console.log, sleep: SleepFunction = Bun.sleep, now: () => number = Date.now, timeoutMilliseconds = bootstrapWaitTimeoutMilliseconds, pollMilliseconds = bootstrapWaitPollMilliseconds, ): Promise { const startedAt = now(); let attemptCount = 0; while (true) { try { await bootstrapRemoteOperations.ssh(target, bootstrapReadinessCommand); return; } catch (error) { const elapsedMilliseconds = now() - startedAt; if (elapsedMilliseconds >= timeoutMilliseconds) { const message = error instanceof Error ? error.message : String(error); throw new Error( `Timed out waiting for bootstrap readiness on ${target.host}:${target.port}. Last error: ${message}`, ); } attemptCount += 1; outputWriter( `Waiting for bootstrap readiness on ${target.host}:${target.port} (attempt ${attemptCount}).`, ); await sleep(pollMilliseconds); } } } /** Copies the Tailscale key and runs bootstrap. */ export async function bootstrapHost( target: RemoteTarget, tailscaleAuthKeyFilePath: string, bootstrapRemoteOperations: BootstrapRemoteOperations = defaultBootstrapRemoteOperations, ): Promise { await bootstrapRemoteOperations.ssh(target, "install -d -m 0700 /root/bootstrap"); await bootstrapRemoteOperations.scpToHost( target, tailscaleAuthKeyFilePath, "/root/bootstrap/tailscale-auth.key", ); await bootstrapRemoteOperations.ssh(target, buildBootstrapCommand()); } /** Runs Terraform and returns the exit code. */ export async function runTerraformCommand( args: readonly string[], terraformEnvironment: TerraformEnvironment, ): Promise { const terraform = await $`${["terraform", "-chdir=terraform", ...args]}` .env(terraformEnvironment) .nothrow(); return terraform.exitCode; } /** Reads one Terraform output. */ export async function readTerraformOutput( outputName: string, terraformEnvironment: TerraformEnvironment, ): Promise { const terraform = await $`${[ "terraform", "-chdir=terraform", "output", "-raw", outputName, ]}` .env(terraformEnvironment) .quiet() .nothrow(); const exitCode = terraform.exitCode; if (exitCode !== 0) { throw new Error( `Command failed with exit code ${exitCode}: terraform -chdir=terraform output -raw ${outputName}`, ); } return (await terraform.text()).trim(); } /** Runs the bootstrap workflow. */ export async function main( args: readonly string[] = Bun.argv.slice(2), envSource: EnvSource = Bun.env, bootstrapCommandRunner: BootstrapCommandRunner = runTerraformCommand, terraformOutputReader: TerraformOutputReader = readTerraformOutput, lowLevelBootstrapRemoteOperations: BootstrapRemoteOperations = defaultBootstrapRemoteOperations, outputWriter: OutputWriter = console.log, sleep: SleepFunction = Bun.sleep, now: () => number = Date.now, ): Promise { if (args.includes("--help") || args.includes("-h") || args.includes("help")) { outputWriter(usage()); return 0; } const terraformEnvironment = buildBootstrapTerraformEnvironment(envSource); const applyExitCode = await bootstrapCommandRunner( buildBootstrapApplyArgs(args), terraformEnvironment, ); if (applyExitCode !== 0) { return applyExitCode; } const bootstrapHostName = ( await terraformOutputReader("bootstrap_host", terraformEnvironment) ).trim(); const stagingDirectory = await mkdtemp(`${envSource.TMPDIR ?? "/tmp"}/knotty-bootstrap-`); const tailscaleAuthKeyFilePath = join(stagingDirectory, "tailscale-auth.key"); try { await Bun.write( tailscaleAuthKeyFilePath, `${envFrom(envSource, "TAILSCALE_AUTH_KEY")}\n`, ); const target = { host: bootstrapHostName, port: 22, user: "root", } as const; try { await waitForBootstrapReadiness( target, lowLevelBootstrapRemoteOperations, outputWriter, sleep, now, ); } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new Error( `Public bootstrap could not reach ${bootstrapHostName}:22. If this host has already been bootstrapped, stop using \`bun run bootstrap\` and switch to \`bun run tf apply\`. Last error: ${message}`, ); } await bootstrapHost( target, tailscaleAuthKeyFilePath, lowLevelBootstrapRemoteOperations, ); } finally { await rm(stagingDirectory, { force: true, recursive: true }); } return 0; } if (import.meta.main) { try { const exitCode = await main(); process.exit(exitCode); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(message); process.exit(1); } }