Something went wrong. Try again.
Terraform and Bun scripts to deploy one self-hosted Knot server to Hetzner VM
knot tailscale typescript bun terraform hetzner
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132/** * Shared helpers for the operator scripts. * This file validates env vars and runs SSH and SCP commands. * Keep workflow logic in the entrypoint files. */
import { $ } from "bun";
/** String-valued environment map. */export type EnvSource = Readonly<Record<string, string | undefined>>;
/** Remote SSH or SCP target. */export type RemoteTarget = Readonly<{ host: string; port: number; user: string;}>;
/** Shared SSH and SCP helpers. */export type RemoteOperations = Readonly<{ scpToHost( target: RemoteTarget, sourcePath: string, destinationPath: string, ): Promise<void>; ssh(target: RemoteTarget, command: string): Promise<void>;}>;
/** Returns a required env var. */export function envFrom(envSource: EnvSource, name: string): string { const value = envSource[name]; if (value === undefined || value === "") { throw new Error(`Missing required env var: ${name}`); }
return value;}
/** Ensures every named env var is present. */export function requireEnvVarsFrom( envSource: EnvSource, names: readonly string[],): void { const missing = names.filter((name) => { const value = envSource[name]; return value === undefined || value === ""; });
if (missing.length > 0) { throw new Error(`Missing required env vars: ${missing.join(", ")}`); }}
/** Quotes a value for a POSIX shell command. */export function shellQuote(value: string): string { return `'${value.replaceAll("'", "'\\''")}'`;}
/** Builds the `user@host` address. */function buildRemoteAddress(target: RemoteTarget): string { return `${target.user}@${target.host}`;}
/** Builds SSH arguments. */function buildSshArgs( target: RemoteTarget, command: string,): readonly string[] { return [ "ssh", "-p", String(target.port), "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new", buildRemoteAddress(target), command, ];}
/** Builds SCP arguments. */function buildScpArgs( target: RemoteTarget, sourcePath: string, destinationPath: string,): readonly string[] { return [ "scp", "-P", String(target.port), "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new", sourcePath, `${buildRemoteAddress(target)}:${destinationPath}`, ];}
/** Runs one command and throws on failure. */async function runCommand(argv: readonly string[]): Promise<void> { const command = await $`${argv}`.nothrow(); const exitCode = command.exitCode;
if (exitCode !== 0) { throw new Error( `Command failed with exit code ${exitCode}: ${argv.join(" ")}`, ); }}
/** Copies one file to the remote host. */export async function scpToHost( target: RemoteTarget, sourcePath: string, destinationPath: string,): Promise<void> { await runCommand(buildScpArgs(target, sourcePath, destinationPath));}
/** Runs one shell command on the remote host. */export async function ssh(target: RemoteTarget, command: string): Promise<void> { await runCommand(buildSshArgs(target, command));}
/** Default SSH and SCP helpers. */export const remoteOperations: RemoteOperations = { scpToHost, ssh,};