ecs-pf
Version:
CLI for port-forwarding to RDS via AWS ECS
230 lines (229 loc) • 9.13 kB
JavaScript
import { EC2Client } from "@aws-sdk/client-ec2";
import { ECSClient } from "@aws-sdk/client-ecs";
import { RDSClient } from "@aws-sdk/client-rds";
import { input, search } from "@inquirer/prompts";
import { isDefined, isEmpty } from "remeda";
import { getAWSRegions, getECSClusters, getECSClustersWithExecCapability, getECSTasks, getRDSInstances, } from "../aws-services.js";
import { isTaskArnShape } from "../regex.js";
import { searchClusters, searchRDS, searchRegions, searchTasks, } from "../search.js";
import { parsePort } from "../types/parsers.js";
import { isFailure, parseRegionName, parseTaskArn } from "../types.js";
import { findAvailablePortSafe, getDefaultPortForEngine, messages, } from "../utils/index.js";
const DEFAULT_PAGE_SIZE = 50;
function isECSCluster(value) {
return (typeof value === "object" &&
value !== null &&
"clusterName" in value &&
"clusterArn" in value);
}
function isRDSInstance(value) {
return (typeof value === "object" &&
value !== null &&
"dbInstanceIdentifier" in value &&
"endpoint" in value);
}
export async function selectRegion(options) {
if (options.region) {
messages.success(`Region (from CLI): ${options.region}`);
return options.region;
}
const defaultRegion = await (async () => {
try {
const testClient = new EC2Client({});
return await testClient.config.region();
}
catch {
return undefined;
}
})();
const defaultEc2Client = new EC2Client({ region: "us-east-1" });
messages.warning("Getting available AWS regions...");
const regionsResult = await getAWSRegions(defaultEc2Client);
if (isFailure(regionsResult)) {
throw new Error(`Failed to get AWS regions: ${regionsResult.error}`);
}
const regions = regionsResult.data;
if (isEmpty(regions)) {
throw new Error("No AWS regions available");
}
if (defaultRegion) {
messages.info(`Default region from AWS config: ${defaultRegion}`);
}
messages.info("filtered as you type (↑↓ to select, Enter to confirm)");
const selectedValue = await search({
message: "Search and select AWS region:",
source: async (input) => {
return await searchRegions(regions, input || "", defaultRegion);
},
pageSize: DEFAULT_PAGE_SIZE,
});
if (typeof selectedValue !== "string") {
throw new Error("Invalid region selection");
}
const regionParseResult = parseRegionName(selectedValue);
if (isFailure(regionParseResult)) {
throw new Error(`Invalid region selected: ${regionParseResult.error}`);
}
return regionParseResult.data;
}
export async function selectCluster(ecsClient, options) {
if (options.cluster) {
messages.warning("Getting ECS clusters...");
const clustersResult = await getECSClusters(ecsClient);
if (isFailure(clustersResult)) {
throw new Error(`Failed to get ECS clusters: ${clustersResult.error}`);
}
const cluster = clustersResult.data.find((c) => c.clusterName === options.cluster);
if (!cluster) {
throw new Error(`ECS cluster not found: ${options.cluster}`);
}
messages.success(`Cluster (from CLI): ${options.cluster}`);
return cluster;
}
messages.warning("Getting ECS clusters with exec capability...");
const clustersResult = await getECSClustersWithExecCapability(ecsClient);
if (isFailure(clustersResult)) {
throw new Error(`Failed to get ECS clusters: ${clustersResult.error}`);
}
const clusters = clustersResult.data;
if (isEmpty(clusters)) {
throw new Error("No ECS clusters found with exec capability. Please ensure your clusters have ECS exec enabled.");
}
messages.info(`Found ${clusters.length} clusters with ECS exec capability`);
messages.info("filtered as you type (↑↓ to select, Enter to confirm)");
const selectedValue = await search({
message: "Search and select ECS cluster:",
source: async (input) => {
return await searchClusters(clusters, input || "");
},
pageSize: DEFAULT_PAGE_SIZE,
});
if (!isECSCluster(selectedValue)) {
throw new Error("Invalid cluster selection");
}
return selectedValue;
}
export async function selectTask(ecsClient, cluster, options) {
if (options.task) {
messages.success(`Task (from CLI): ${options.task}`);
const parseResult = parseTaskArn(options.task);
if (isFailure(parseResult)) {
throw new Error(`Invalid task ARN from CLI: ${parseResult.error}`);
}
return parseResult.data;
}
messages.warning("Getting ECS tasks...");
const tasksResult = await getECSTasks(ecsClient, cluster);
if (isFailure(tasksResult)) {
throw new Error(`Failed to get ECS tasks: ${tasksResult.error}`);
}
const tasks = tasksResult.data;
if (isEmpty(tasks)) {
throw new Error("No running ECS tasks found");
}
const selectedValue = await search({
message: "Search and select ECS task:",
source: async (input) => {
return await searchTasks(tasks, input || "");
},
pageSize: DEFAULT_PAGE_SIZE,
});
if (!isTaskArnShape(selectedValue)) {
throw new Error("Invalid task selection");
}
const parseResult = parseTaskArn(selectedValue);
if (isFailure(parseResult)) {
throw new Error(`Invalid task ARN: ${parseResult.error}`);
}
return parseResult.data;
}
export async function selectRDSInstance(rdsClient, options) {
if (options.rds) {
messages.warning("Getting RDS instances...");
const rdsInstancesResult = await getRDSInstances(rdsClient);
if (isFailure(rdsInstancesResult)) {
throw new Error(`Failed to get RDS instances: ${rdsInstancesResult.error}`);
}
const rdsInstance = rdsInstancesResult.data.find((r) => r.dbInstanceIdentifier === options.rds);
if (!rdsInstance) {
throw new Error(`RDS instance not found: ${options.rds}`);
}
messages.success(`RDS (from CLI): ${options.rds}`);
return rdsInstance;
}
messages.warning("Getting RDS instances...");
const rdsInstancesResult = await getRDSInstances(rdsClient);
if (isFailure(rdsInstancesResult)) {
throw new Error(`Failed to get RDS instances: ${rdsInstancesResult.error}`);
}
const rdsInstances = rdsInstancesResult.data;
if (isEmpty(rdsInstances)) {
throw new Error("No RDS instances found");
}
const selectedValue = await search({
message: "Search and select RDS instance:",
source: async (input) => {
return await searchRDS(rdsInstances, input || "");
},
pageSize: DEFAULT_PAGE_SIZE,
});
if (!isRDSInstance(selectedValue)) {
throw new Error("Invalid RDS instance selection");
}
return selectedValue;
}
export async function getRDSPort(rdsInstance, options) {
if (isDefined(options.rdsPort)) {
const rdsPort = `${options.rdsPort}`;
messages.success(`RDS Port (from CLI): ${rdsPort}`);
return rdsPort;
}
const actualRDSPort = rdsInstance.port;
const fallbackPort = getDefaultPortForEngine(rdsInstance.engine);
const rdsPort = `${actualRDSPort || fallbackPort}`;
messages.success(`RDS Port (auto-detected): ${rdsPort}`);
return rdsPort;
}
export async function getLocalPort(options) {
if (isDefined(options.localPort)) {
const localPort = `${options.localPort}`;
messages.success(`Local Port (from CLI): ${localPort}`);
return localPort;
}
const availablePortResult = await findAvailablePortSafe(8888);
if (availablePortResult.success) {
const port = Number(availablePortResult.data);
messages.success(`Local Port (auto-selected): ${port}`);
return `${port}`;
}
messages.warning("Could not find available port automatically. Please specify manually:");
const localPortInput = await input({
message: "Enter local port number:",
default: "8888",
validate: (inputValue) => {
const parseResult = parsePort(inputValue || "8888");
return parseResult.success ? true : `Invalid port: ${parseResult.error}`;
},
});
return localPortInput;
}
export async function selectAllResources(options) {
const region = await selectRegion(options);
const ecsClient = new ECSClient({ region });
const rdsClient = new RDSClient({ region });
const cluster = await selectCluster(ecsClient, options);
const taskArn = await selectTask(ecsClient, cluster, options);
const rdsInstance = await selectRDSInstance(rdsClient, options);
const rdsPort = await getRDSPort(rdsInstance, options);
const localPort = await getLocalPort(options);
return {
region,
cluster,
taskArn,
rdsInstance,
rdsPort,
localPort,
ecsClient,
rdsClient,
};
}