UNPKG

ecs-pf

Version:

CLI for port-forwarding to RDS via AWS ECS

218 lines (217 loc) 9.43 kB
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(); }