shared/database/local/databaseTunnel.ts (95 lines of code) (raw):

import { DATABASE_PORT, getDatabaseProxyName } from "../database"; import { Stage } from "../../types/stage"; import { exec } from "child_process"; import { promisify } from "util"; import { RDS } from "@aws-sdk/client-rds"; import { standardAwsConfig } from "../../awsIntegration"; import { ENVIRONMENT_VARIABLE_KEYS } from "../../environmentVariables"; import { getDatabaseJumpHost } from "./getDatabaseJumpHost"; import prompts from "prompts"; const runCommandPromise = promisify(exec); export const isThereExistingTunnel = async ( dbProxyEndpoint: string ): Promise<boolean> => { const existingTunnelProcessResult = await runCommandPromise( `ps -ef | grep ssh | grep 5432 | grep -v grep || true` ); if (existingTunnelProcessResult.stderr?.trim()) { console.log(existingTunnelProcessResult.stdout); console.error(existingTunnelProcessResult.stderr); throw Error("Failed to lookup any existing tunnels"); } if (existingTunnelProcessResult.stdout.includes(dbProxyEndpoint)) { return true; } else if ( existingTunnelProcessResult.stdout.trim().length > 0 && !existingTunnelProcessResult.stdout.includes(dbProxyEndpoint) ) { console.log(existingTunnelProcessResult.stdout); console.error(existingTunnelProcessResult.stderr); throw Error( `It looks like there is an existing tunnel on localhost:${DATABASE_PORT} but not to ${dbProxyEndpoint}. You will need to kill this manually.` ); } return false; }; const SSH_TUNNEL_OPTIONS = "-o ExitOnForwardFailure=yes -o ServerAliveInterval=10 -o ServerAliveCountMax=2"; export const establishTunnelToDBProxy = async ( stage: Stage, instanceID: string, dbProxyEndpoint: string ) => { console.log("Fetching SSH details..."); const ssmResult = await runCommandPromise( `ssm ssh --profile workflow -i ${instanceID} --raw` ); if (!ssmResult.stdout?.trim()) { console.log(ssmResult.stdout); console.error(ssmResult.stderr); throw Error("Failed to establish get SSH command"); } console.log("SSH details fetched, establishing SSH tunnel..."); const sshCommand = ssmResult.stdout; // ssh doesn't seem to play nicely with 'exec' so we have to fire and forget then check if the tunnel is established runCommandPromise( `${sshCommand} -f -N ${SSH_TUNNEL_OPTIONS} -L ${DATABASE_PORT}:${dbProxyEndpoint}:${DATABASE_PORT}` ).catch(console.error); await new Promise((resolve) => setTimeout(resolve, 7500)); // wait before checking the if the tunnel is established if (await isThereExistingTunnel(dbProxyEndpoint)) { console.log(`SSH tunnel established on localhost:${DATABASE_PORT} 🎉`); } else { throw Error("Failed to establish SSH tunnel"); } }; export async function createDatabaseTunnel(defaultSelection?: { stage: Stage; }) { const { stage } = defaultSelection || (await prompts({ type: "select", name: "stage", message: "Stage?", choices: [ { title: "CODE", value: "CODE", selected: true }, { title: "PROD", value: "PROD" }, ], })); const DBProxyName = getDatabaseProxyName(stage); const dbProxyResponse = await new RDS(standardAwsConfig).describeDBProxies({ DBProxyName, }); const { Endpoint } = dbProxyResponse.DBProxies![0]!; process.env[ENVIRONMENT_VARIABLE_KEYS.databaseHostname] = Endpoint!; console.log(`DB Proxy Hostname: ${Endpoint!}`); if (await isThereExistingTunnel(Endpoint!)) { console.log( `It looks like there is already a suitable SSH tunnel established on localhost:${DATABASE_PORT} 🎉` ); } else { const jumpHostInstanceId = await getDatabaseJumpHost(stage); await establishTunnelToDBProxy(stage, jumpHostInstanceId, Endpoint!); } return stage; }