ecs-pf
Version:
CLI for port-forwarding to RDS via AWS ECS
218 lines (217 loc) • 9.43 kB
JavaScript
import { EC2Client } from "@aws-sdk/client-ec2";
import { ECSClient } from "@aws-sdk/client-ecs";
import { search } from "@inquirer/prompts";
import { isEmpty } from "remeda";
import { enableECSExecForService, enableECSExecForServices, getAllECSServices, getAWSRegions, getECSClusters, getECSServicesWithoutExec, } from "./aws-services.js";
import { searchRegions, searchServices } from "./search.js";
import { parseClusterArn, parseClusterName, parseProcessClusterServicesParams, parseRegionName, unwrapBrandedString, } from "./types.js";
import { displayFriendlyError, messages } from "./utils/index.js";
const DEFAULT_PAGE_SIZE = 50;
export async function enableECSExec(options) {
let selectedRegion = options.region;
if (!selectedRegion) {
const ec2Client = new EC2Client({});
const regionsResult = await getAWSRegions(ec2Client);
if (!regionsResult.success) {
throw new Error(regionsResult.error);
}
const selectedRegionValue = await search({
message: "Select AWS region:",
source: async (input) => await searchRegions(regionsResult.data, input || ""),
pageSize: DEFAULT_PAGE_SIZE,
});
if (typeof selectedRegionValue !== "string") {
throw new Error("Invalid region selection");
}
selectedRegion = selectedRegionValue;
}
const regionResult = parseRegionName(selectedRegion);
if (!regionResult.success) {
throw new Error(`Invalid region: ${selectedRegion}`);
}
const ecsClient = new ECSClient({
region: unwrapBrandedString(regionResult.data),
});
try {
if (options.cluster && options.service) {
await enableExecForSpecificService(ecsClient, options);
}
else if (options.cluster) {
await enableExecForClusterServices(ecsClient, options);
}
else {
await enableExecInteractive(ecsClient, options);
}
}
catch (error) {
displayFriendlyError(error);
throw error;
}
}
async function enableExecForSpecificService(ecsClient, options) {
if (!options.cluster || !options.service) {
throw new Error("Both cluster and service must be specified");
}
const clusterName = options.cluster;
const serviceName = options.service;
messages.info(`Enabling exec for service "${serviceName}" in cluster "${clusterName}"...`);
if (options.dryRun) {
messages.info("DRY RUN: Would execute the following AWS command:");
messages.info(`aws ecs update-service --cluster ${clusterName} --service ${serviceName} --enable-execute-command`);
return;
}
const result = await enableECSExecForService(ecsClient, clusterName, serviceName);
if (!result.success) {
throw new Error(result.error);
}
displayEnableExecResult(result.data);
}
async function enableExecForClusterServices(ecsClient, options) {
if (!options.cluster) {
throw new Error("Cluster must be specified");
}
const clusterResult = parseClusterName(options.cluster);
if (!clusterResult.success) {
throw new Error(`Invalid cluster name: ${options.cluster}`);
}
const clusterArnString = `arn:aws:ecs:*:*:cluster/${options.cluster}`;
const clusterArnResult = parseClusterArn(clusterArnString);
if (!clusterArnResult.success) {
throw new Error(`Failed to create cluster ARN: ${clusterArnResult.error}`);
}
const cluster = {
clusterName: clusterResult.data,
clusterArn: clusterArnResult.data,
};
await processClusterServices({ ecsClient, cluster, options });
}
async function enableExecInteractive(ecsClient, options) {
messages.info("Starting interactive ECS exec enablement...");
messages.warning("Getting ECS clusters...");
const clustersResult = await getECSClusters(ecsClient);
if (!clustersResult.success) {
throw new Error(clustersResult.error);
}
const clusterCount = clustersResult.data.length;
messages.clearPreviousLine();
messages.info(`Found ${clusterCount} clusters. Getting services...`);
const allServicesResult = await getAllECSServices(ecsClient);
if (!allServicesResult.success) {
throw new Error(allServicesResult.error);
}
messages.clearPreviousLine();
messages.info(`Found ${allServicesResult.data.length} total services. Filtering exec-disabled services...`);
const servicesWithoutExec = allServicesResult.data.filter((service) => !service.enableExecuteCommand);
messages.clearPreviousLine();
if (isEmpty(servicesWithoutExec)) {
messages.success("All services in all clusters already have exec enabled!");
return;
}
messages.info(`Found ${servicesWithoutExec.length} services without exec enabled across all clusters`);
const selectedServices = await search({
message: "Select services to enable exec (you can select multiple services):",
source: async (input) => await searchServices(servicesWithoutExec, input || ""),
pageSize: DEFAULT_PAGE_SIZE,
});
if (!selectedServices ||
typeof selectedServices !== "object" ||
!("serviceName" in selectedServices)) {
throw new Error("Invalid service selection");
}
const service = selectedServices;
if (options.dryRun) {
messages.info("DRY RUN: Would enable exec for the following service:");
messages.info(` aws ecs update-service --cluster ${service.clusterName} --service ${service.serviceName} --enable-execute-command`);
return;
}
const clusterNameStr = unwrapBrandedString(service.clusterName);
const serviceNameStr = unwrapBrandedString(service.serviceName);
if (!clusterNameStr || !serviceNameStr) {
throw new Error("Invalid cluster or service name");
}
const result = await enableECSExecForService(ecsClient, clusterNameStr, serviceNameStr);
if (!result.success) {
throw new Error(result.error);
}
displayEnableExecResult(result.data);
}
async function processClusterServices(params) {
const parseResult = parseProcessClusterServicesParams(params);
if (!parseResult.success) {
throw new Error(parseResult.error);
}
const { ecsClient, cluster, options } = parseResult.data;
messages.info(`Processing cluster: ${cluster.clusterName}`);
const servicesResult = await getECSServicesWithoutExec(ecsClient, cluster);
if (!servicesResult.success) {
messages.error(`Failed to get services for cluster ${cluster.clusterName}: ${servicesResult.error}`);
return;
}
if (isEmpty(servicesResult.data)) {
messages.success(` All services in cluster "${cluster.clusterName}" already have exec enabled`);
return;
}
messages.info(` Found ${servicesResult.data.length} services without exec enabled`);
if (options.dryRun) {
messages.info(" DRY RUN: Would enable exec for:");
for (const service of servicesResult.data) {
messages.info(` ${service.serviceName}`);
}
return;
}
const serviceNames = servicesResult.data
.map((service) => unwrapBrandedString(service.serviceName))
.filter((name) => name !== undefined);
const clusterNameStr = unwrapBrandedString(cluster.clusterName);
if (!clusterNameStr) {
throw new Error("Invalid cluster name");
}
const results = await enableECSExecForServices(ecsClient, clusterNameStr, serviceNames);
if (!results.success) {
messages.error(`Failed to enable exec for cluster ${cluster.clusterName}: ${results.error}`);
return;
}
displayEnableExecResults(results.data);
}
function displayEnableExecResult(result) {
messages.empty();
if (result.success) {
if (result.previousState) {
messages.info(`Service "${result.serviceName}" already had exec enabled`);
}
else {
messages.success(`Successfully enabled exec for service "${result.serviceName}"`);
}
}
else {
messages.error(`Failed to enable exec for service "${result.serviceName}": ${result.error}`);
}
messages.empty();
}
function displayEnableExecResults(results) {
messages.empty();
messages.info("Enable exec results:");
messages.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
let successCount = 0;
let alreadyEnabledCount = 0;
let failureCount = 0;
for (const result of results) {
if (result.success) {
if (result.previousState) {
messages.info(` ✓ ${result.serviceName} (already enabled)`);
alreadyEnabledCount++;
}
else {
messages.success(` ✓ ${result.serviceName} (enabled)`);
successCount++;
}
}
else {
messages.error(` ✗ ${result.serviceName} (failed: ${result.error})`);
failureCount++;
}
}
messages.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
messages.info(`Summary: ${successCount} enabled, ${alreadyEnabledCount} already enabled, ${failureCount} failed`);
messages.empty();
}