UNPKG

ecs-pf

Version:

CLI for port-forwarding to RDS via AWS ECS

150 lines (149 loc) 6.52 kB
import { ECSClient } from "@aws-sdk/client-ecs"; import { RDSClient } from "@aws-sdk/client-rds"; import { isDefined } from "remeda"; import { startSSMSession } from "../session.js"; import { isFailure, parsePort, parseTaskId, unwrapBrandedNumber, unwrapBrandedString, } from "../types.js"; import { askRetry, displayFriendlyError, getDefaultPortForEngine, messages, } from "../utils/index.js"; import { generateReproducibleCommand } from "./command-generation.js"; import { displayDryRunResult, generateConnectDryRun } from "./dry-run.js"; import { selectCluster, selectRDSInstance, selectRegion, selectTask, } from "./resource-selection.js"; import { promptForLocalPort } from "./user-prompts.js"; export async function connectToRDS(options = { dryRun: false }) { if (options.dryRun) { await connectToRDSDryRun(options); return; } let retryCount = 0; const maxRetries = 3; while (retryCount <= maxRetries) { try { await connectToRDSInternal(options); return; } catch (error) { retryCount++; displayFriendlyError(error); if (retryCount <= maxRetries) { messages.warning(`Retry count: ${retryCount}/${maxRetries + 1}`); const shouldRetry = await askRetry(); if (!shouldRetry) { messages.info("Process interrupted"); return; } messages.info("Retrying...\n"); } else { messages.error("Maximum retry count reached. Terminating process."); messages.gray("If the problem persists, please check the above solutions."); throw error; } } } } export async function connectToRDSDryRun(options) { messages.info("Starting AWS ECS RDS connection tool (DRY RUN)..."); const region = await selectRegion(options); const ecsClient = new ECSClient({ region }); const rdsClient = new RDSClient({ region }); const selectedCluster = await selectCluster(ecsClient, options); messages.success(`Cluster: ${unwrapBrandedString(selectedCluster.clusterName)}`); const selectedTaskArn = await selectTask(ecsClient, selectedCluster, options); messages.success(`Task: ${unwrapBrandedString(selectedTaskArn)}`); messages.warning("Getting RDS instances..."); const selectedRDS = await selectRDSInstance(rdsClient, options); messages.success(`RDS: ${unwrapBrandedString(selectedRDS.dbInstanceIdentifier)}`); const taskIdStr = String(selectedTaskArn).split("_")[1] || String(selectedTaskArn); const taskIdResult = parseTaskId(taskIdStr); if (isFailure(taskIdResult)) { throw new Error(`Invalid task ID: ${taskIdResult.error}`); } const taskId = taskIdResult.data; const rdsPort = isDefined(options.rdsPort) ? (() => { messages.success(`RDS Port (from CLI): ${options.rdsPort}`); return options.rdsPort; })() : (() => { const actualRDSPort = selectedRDS.port; const fallbackPortNumber = getDefaultPortForEngine(selectedRDS.engine); const portToUse = actualRDSPort ? unwrapBrandedNumber(actualRDSPort) : fallbackPortNumber; const portResult = parsePort(portToUse); if (isFailure(portResult)) { throw new Error(`Invalid RDS port: ${portResult.error}`); } messages.success(`RDS Port (auto-detected): ${unwrapBrandedNumber(portResult.data)}`); return portResult.data; })(); const localPort = isDefined(options.localPort) ? (() => { messages.success(`Local Port (from CLI): ${unwrapBrandedNumber(options.localPort)}`); return options.localPort; })() : await promptForLocalPort(); const dryRunResult = generateConnectDryRun({ region, cluster: selectedCluster.clusterName, task: taskId, rdsInstance: selectedRDS, rdsPort, localPort, }); displayDryRunResult(dryRunResult); messages.success("Dry run completed successfully."); } async function connectToRDSInternal(options) { const region = await selectRegion(options); const ecsClient = new ECSClient({ region }); const rdsClient = new RDSClient({ region }); const selectedCluster = await selectCluster(ecsClient, options); messages.success(`Cluster: ${unwrapBrandedString(selectedCluster.clusterName)}`); const selectedTaskArn = await selectTask(ecsClient, selectedCluster, options); messages.success(`Task: ${unwrapBrandedString(selectedTaskArn)}`); messages.warning("Getting RDS instances..."); const selectedRDS = await selectRDSInstance(rdsClient, options); messages.success(`RDS: ${unwrapBrandedString(selectedRDS.dbInstanceIdentifier)}`); const taskArn = selectedTaskArn; const rdsPort = isDefined(options.rdsPort) ? (() => { messages.success(`RDS Port (from CLI): ${unwrapBrandedNumber(options.rdsPort)}`); return options.rdsPort; })() : (() => { const actualRDSPort = selectedRDS.port; const fallbackPortNumber = getDefaultPortForEngine(selectedRDS.engine); const portToUse = actualRDSPort ? unwrapBrandedNumber(actualRDSPort) : fallbackPortNumber; const portResult = parsePort(portToUse); if (isFailure(portResult)) { throw new Error(`Invalid RDS port: ${portResult.error}`); } messages.success(`RDS Port (auto-detected): ${unwrapBrandedNumber(portResult.data)}`); return portResult.data; })(); const localPort = isDefined(options.localPort) ? (() => { messages.success(`Local Port (from CLI): ${unwrapBrandedNumber(options.localPort)}`); return options.localPort; })() : await promptForLocalPort(); const reproducibleCommand = generateReproducibleCommand({ region, cluster: selectedCluster.clusterName, task: taskArn, rds: selectedRDS.dbInstanceIdentifier, rdsPort, localPort, }); messages.info("Selected task:"); messages.info(String(selectedTaskArn)); await startSSMSession({ taskArn, rdsInstance: selectedRDS, rdsPort, localPort, reproducibleCommand, }); }