@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
263 lines (262 loc) • 11 kB
JavaScript
import { spawn } from "child_process";
import * as net from "net";
import * as fs from "fs";
import * as path from "path";
import { SecretsManagerClient, GetSecretValueCommand, } from "@aws-sdk/client-secrets-manager";
import { detectContext, getPlatformBastionInfo, getDatabaseUrl, rewriteDatabaseUrl, logInfo, logSuccess, logError, logWarn, } from "../../utils/index.js";
const DEFAULT_LOCAL_PORT = 5432;
const MAX_RETRIES = 12;
const RETRY_DELAY_MS = 5000;
function findSstOutputs() {
let dir = process.cwd();
const root = path.parse(dir).root;
while (dir !== root) {
const outputsPath = path.join(dir, ".sst", "outputs.json");
if (fs.existsSync(outputsPath)) {
try {
const content = fs.readFileSync(outputsPath, "utf-8");
const outputs = JSON.parse(content);
if (outputs.databaseUrl) {
const secretArnMatch = outputs.databaseUrl.match(/secretArn=([^&]+)/);
const secretArn = secretArnMatch
? decodeURIComponent(secretArnMatch[1])
: undefined;
return {
databaseUrl: outputs.databaseUrl,
databaseName: outputs.databaseName,
secretArn,
rootDir: dir,
};
}
}
catch {
}
}
dir = path.dirname(dir);
}
return null;
}
function checkPort(port) {
return new Promise((resolve) => {
const socket = new net.Socket();
socket.setTimeout(500);
socket.on("connect", () => { socket.destroy(); resolve(true); });
socket.on("timeout", () => { socket.destroy(); resolve(false); });
socket.on("error", () => { socket.destroy(); resolve(false); });
socket.connect(port, "localhost");
});
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function startTunnelBackground(instanceId, rdsEndpoint, rdsPort, localPort) {
logInfo("Starting database tunnel...");
const tunnel = spawn("aws", [
"ssm",
"start-session",
"--target",
instanceId,
"--document-name",
"AWS-StartPortForwardingSessionToRemoteHost",
"--parameters",
JSON.stringify({
host: [rdsEndpoint],
portNumber: [String(rdsPort)],
localPortNumber: [String(localPort)],
}),
], { stdio: ["ignore", "ignore", "ignore"] });
const maxAttempts = 15;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
if (tunnel.exitCode !== null) {
throw new Error("Tunnel process died");
}
if (await checkPort(localPort)) {
logSuccess(`Tunnel started (PID: ${tunnel.pid})`);
return tunnel;
}
await sleep(1000);
}
tunnel.kill();
throw new Error(`Tunnel failed to start after ${maxAttempts} seconds`);
}
async function fetchSecretWithRetry(client, secretId) {
let lastError;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
const response = await client.send(new GetSecretValueCommand({ SecretId: secretId }));
if (!response.SecretString) {
throw new Error(`Secret ${secretId} has no string value`);
}
return JSON.parse(response.SecretString);
}
catch (error) {
lastError = error;
const isNotFound = error.name === "ResourceNotFoundException";
if (isNotFound && attempt < MAX_RETRIES) {
logWarn(`Secret not found (attempt ${attempt}/${MAX_RETRIES}), waiting...`);
await sleep(RETRY_DELAY_MS);
continue;
}
throw error;
}
}
throw lastError;
}
export async function execCommand(commandArgs, options) {
if (commandArgs.length === 0) {
logError("No command specified");
logInfo("Usage: mesh db exec [options] -- <command> [args...]");
logInfo("");
logInfo("Options:");
logInfo(" -t, --tenant <tenant> Platform tenant (e.g., mesh)");
logInfo(" -e, --env <env> Platform environment (e.g., dev)");
logInfo(" --secret <arn> Use secret ARN directly (skips SSM discovery)");
logInfo(" --db-host <host> Database host (skips SSM lookup)");
logInfo(" --ssl <mode> SSL mode: require, no-verify, disable");
logInfo(" --port <port> Local port for tunnel (default: 5432)");
logInfo(" --stage <stage> Stage for legacy discovery mode");
logInfo("");
logInfo("Examples:");
logInfo(" # Explicit mode (recommended)");
logInfo(" mesh db exec --tenant mesh --env dev --ssl=require -- npx prisma db push");
logInfo("");
logInfo(" # Direct mode (with existing tunnel)");
logInfo(" mesh db exec --secret=arn:aws:... --ssl=require -- pnpm dev");
process.exit(1);
}
const localPort = options.port ? parseInt(options.port, 10) : DEFAULT_LOCAL_PORT;
const secretsManager = new SecretsManagerClient({});
let tunnel = null;
let databaseUrl;
if (options.secret) {
logInfo(`Direct mode: using secret ${options.secret}`);
const tunnelExists = await checkPort(localPort);
if (!tunnelExists) {
logError(`No tunnel found on port ${localPort}`);
logInfo("In direct mode, start a tunnel first with: mesh db connect");
logInfo("Or use discovery mode (without --secret) to auto-start tunnel");
process.exit(1);
}
logSuccess(`Using existing tunnel on port ${localPort}`);
logInfo(`Fetching credentials from secret...`);
const secret = await fetchSecretWithRetry(secretsManager, options.secret);
if (secret.DATABASE_URL) {
databaseUrl = secret.DATABASE_URL;
logSuccess("Got DATABASE_URL from secret");
}
else if (secret.username && secret.password) {
const dbName = secret.dbname ?? secret.database ?? "postgres";
databaseUrl = `postgresql://${secret.username}:${encodeURIComponent(secret.password)}@localhost:${localPort}/${dbName}`;
logSuccess(`Got credentials for user: ${secret.username}`);
}
else {
logError("Secret must contain DATABASE_URL or username/password fields");
process.exit(1);
}
try {
const parsed = new URL(databaseUrl);
parsed.hostname = "localhost";
parsed.port = String(localPort);
if (options.ssl) {
parsed.searchParams.set("sslmode", options.ssl);
if (options.ssl === "require" || options.ssl === "no-verify") {
parsed.searchParams.set("sslaccept", "accept_invalid_certs");
}
}
databaseUrl = parsed.toString();
}
catch {
logWarn("Could not parse DATABASE_URL for rewriting");
}
}
else {
let platformTenant;
let platformEnv;
if (options.tenant && options.env) {
platformTenant = options.tenant;
platformEnv = options.env;
logInfo(`Explicit mode: Platform Tenant=${platformTenant}, Platform Env=${platformEnv}`);
}
else if (options.tenant || options.env) {
logError("Both --tenant and --env are required when using explicit mode");
logInfo("Usage: mesh db exec --tenant mesh --env dev-temporal --app rdc --app-tenant encore --app-stage dev -- <command>");
logInfo("Or omit both to use auto-detection");
process.exit(1);
}
else {
const ctx = detectContext();
platformTenant = ctx.tenant;
platformEnv = ctx.platformEnv;
logInfo(`Discovery mode: Platform=${platformEnv}, Tenant=${platformTenant}`);
}
const appTenant = options.appTenant ?? platformTenant;
const appStage = options.appStage ?? platformEnv;
const appName = options.app;
if (appName) {
logInfo(`App credentials: tenant=${appTenant}, stage=${appStage}, app=${appName}`);
}
const tunnelExists = await checkPort(localPort);
const sstOutputs = findSstOutputs();
if (sstOutputs?.secretArn && tunnelExists) {
logInfo(`Found SST outputs in: ${sstOutputs.rootDir}`);
try {
const secret = await fetchSecretWithRetry(secretsManager, sstOutputs.secretArn);
if (secret.DATABASE_URL) {
databaseUrl = rewriteDatabaseUrl(secret.DATABASE_URL, {
endpoint: `localhost:${localPort}`,
sslMode: options.ssl,
});
logSuccess(`Using DATABASE_URL from SST outputs`);
}
}
catch {
logWarn("Could not fetch from SST outputs, falling back to credential lookup");
}
}
if (!databaseUrl) {
if (!tunnelExists) {
const bastion = await getPlatformBastionInfo(platformTenant, platformEnv);
const rdsService = bastion.services.rds;
if (!rdsService) {
throw new Error("RDS service not available in platform bastion");
}
tunnel = await startTunnelBackground(bastion.instanceId, rdsService.host, rdsService.port, localPort);
}
else {
logSuccess(`Using existing tunnel on port ${localPort}`);
}
const rawUrl = await getDatabaseUrl(appTenant, appStage, { app: appName });
databaseUrl = rewriteDatabaseUrl(rawUrl, {
endpoint: `localhost:${localPort}`,
sslMode: options.ssl,
});
}
}
const env = { ...process.env, DATABASE_URL: databaseUrl };
if (options.ssl === "require" || options.ssl === "no-verify") {
env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
logInfo("Set NODE_TLS_REJECT_UNAUTHORIZED=0 for SSL through tunnel");
}
const cleanup = () => {
if (tunnel) {
logInfo("Stopping tunnel...");
tunnel.kill();
}
};
process.on("exit", cleanup);
process.on("SIGINT", cleanup);
process.on("SIGTERM", cleanup);
const [cmd, ...args] = commandArgs;
logInfo(`Running: ${commandArgs.join(" ")}`);
console.log("");
const child = spawn(cmd, args, { stdio: "inherit", env });
child.on("exit", (code) => {
cleanup();
process.exit(code ?? 0);
});
child.on("error", (err) => {
logError(`Failed to run command: ${err.message}`);
cleanup();
process.exit(1);
});
}