UNPKG

ecs-pf

Version:

CLI for port-forwarding to RDS via AWS ECS

229 lines (228 loc) 11.7 kB
import { spawn } from "node:child_process"; import { messages } from "./utils/index.js"; export async function startSSMSession(params) { const { taskArn, rdsInstance, rdsPort, localPort, reproducibleCommand } = params; const parameters = { host: [rdsInstance.endpoint], portNumber: [String(rdsPort)], localPortNumber: [String(localPort)], }; const parametersJson = JSON.stringify(parameters); const commandString = `aws ssm start-session --target ${taskArn} --parameters '${parametersJson}' --document-name AWS-StartPortForwardingSessionToRemoteHost`; messages.empty(); messages.success(`🌈 RDS connection will be available at localhost:${localPort}`); messages.empty(); return new Promise((resolve, reject) => { const state = { isUserTermination: false, hasSessionStarted: false, }; const child = spawn(commandString, [], { stdio: ["inherit", "pipe", "pipe"], env: process.env, shell: true, }); child.stdout?.on("data", (data) => { const output = data.toString(); process.stdout.write(output); if (output.includes("Starting session") || output.includes("Port forwarding started") || output.includes("Waiting for connections") || output.includes("Port forwarding session started") || (output.includes("Session") && output.includes("started"))) { if (!state.hasSessionStarted) { state.hasSessionStarted = true; clearTimeout(timeout); } } }); child.stderr?.on("data", (data) => { const output = data.toString(); if (output.includes("TargetNotConnected")) { messages.error("Cannot connect to target"); messages.error("Please verify that the ECS task is running and SSM Agent is enabled"); child.kill("SIGTERM"); reject(new Error("Cannot connect to target")); return; } else if (output.includes("AccessDenied")) { messages.error("Access denied"); messages.error("Please verify you have SSM-related IAM permissions"); child.kill("SIGTERM"); reject(new Error("Access denied")); return; } else if (output.includes("InvalidTarget")) { messages.error("Invalid target"); messages.error("Please verify the specified ECS task exists and is running"); child.kill("SIGTERM"); reject(new Error("Invalid target")); return; } process.stderr.write(output); }); child.on("error", (error) => { clearTimeout(timeout); messages.error(`Command execution error: ${error.message}`); if (error.message.includes("ENOENT")) { reject(new Error("AWS CLI may not be installed")); } else if (error.message.includes("EACCES")) { reject(new Error("No permission to execute AWS CLI")); } else { reject(new Error(`Command execution error: ${error.message}`)); } }); child.on("close", (code, signal) => { clearTimeout(timeout); if (signal === "SIGINT" || code === 130 || state.isUserTermination) { messages.success("Process completed successfully"); messages.empty(); messages.info("Command to execute:"); messages.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); messages.info(commandString); messages.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); messages.empty(); if (reproducibleCommand) { messages.info("To reproduce this connection, use:"); messages.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); messages.info(reproducibleCommand); messages.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); messages.empty(); } resolve(); return; } if (code === 0) { messages.success("Process completed successfully"); messages.empty(); messages.info("Command to execute:"); messages.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); messages.info(commandString); messages.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); messages.empty(); if (reproducibleCommand) { messages.info("To reproduce this connection, use:"); messages.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); messages.info(reproducibleCommand); messages.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); messages.empty(); } resolve(); } else { let errorMessage = `Session terminated with error code ${code}`; switch (code) { case 1: errorMessage += "\nGeneral error. Please check your AWS CLI configuration and permissions"; break; case 2: errorMessage += "\nConfiguration file or parameter issue"; break; case 255: errorMessage += "\nConnection error or timeout. Please check network connection and target status"; break; default: errorMessage += "\nUnexpected error. Please check AWS CLI logs"; } reject(new Error(errorMessage)); } }); process.on("SIGINT", () => { if (!state.isUserTermination) { state.isUserTermination = true; child.kill("SIGINT"); } }); const timeout = setTimeout(() => { if (!state.hasSessionStarted) { state.hasSessionStarted = true; messages.success("Port forwarding session should be active"); messages.info("If connection fails, the session may still be starting. Please wait a moment and try again."); } }, 5000); }); } export async function executeECSCommand(params) { const { region, clusterName, taskArn, containerName, command } = params; const commandString = `aws ecs execute-command --region ${String(region)} --cluster ${String(clusterName)} --task ${String(taskArn)} --container ${String(containerName)} --command "${command}" --interactive`; messages.empty(); messages.success(`🚀 Executing command in ECS container: ${containerName}`); messages.info(`Command: ${command}`); messages.empty(); return new Promise((resolve, reject) => { const state = { isUserTermination: false }; const child = spawn(commandString, [], { stdio: "inherit", env: process.env, shell: true, }); child.on("error", (error) => { messages.error(`Command execution error: ${error.message}`); if (error.message.includes("ENOENT")) { reject(new Error("AWS CLI may not be installed")); } else if (error.message.includes("EACCES")) { reject(new Error("No permission to execute AWS CLI")); } else { reject(new Error(`Command execution error: ${error.message}`)); } }); child.on("close", (code, signal) => { if (signal === "SIGINT" || code === 130 || state.isUserTermination) { messages.success("ECS exec session completed"); messages.empty(); messages.info("Command executed:"); messages.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); messages.info(commandString); messages.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); messages.empty(); resolve(); return; } if (code === 0) { messages.success("ECS exec session completed successfully"); messages.empty(); messages.info("Command executed:"); messages.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); messages.info(commandString); messages.info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); messages.empty(); resolve(); } else { let errorMessage = `ECS exec command failed with error code ${code}`; switch (code) { case 1: errorMessage += "\nGeneral error. Please check your AWS CLI configuration and permissions"; break; case 2: errorMessage += "\nConfiguration file or parameter issue"; break; case 254: errorMessage += "\nECS exec not enabled for this task. Please ensure the task definition has enableExecuteCommand: true"; break; case 255: errorMessage += "\nConnection error or timeout. Please check network connection and task status"; break; default: errorMessage += "\nUnexpected error. Please check AWS CLI logs"; } reject(new Error(errorMessage)); } }); process.on("SIGINT", () => { if (!state.isUserTermination) { state.isUserTermination = true; child.kill("SIGINT"); } }); }); }