ecs-pf
Version:
CLI for port-forwarding to RDS via AWS ECS
150 lines (149 loc) • 6.52 kB
JavaScript
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,
});
}