UNPKG

ecs-pf

Version:

CLI for port-forwarding to RDS via AWS ECS

302 lines (301 loc) 13.1 kB
import { EC2Client } from "@aws-sdk/client-ec2"; import { ECSClient } from "@aws-sdk/client-ecs"; import { input, search } from "@inquirer/prompts"; import { isEmpty, isString } from "remeda"; import { getAWSRegions, getECSClustersWithExecCapability, getECSTaskContainers, getECSTasksWithExecCapability, } from "../aws-services.js"; import { searchClusters, searchContainers, searchRegions, searchTasks, } from "../search.js"; import { executeECSCommand } from "../session.js"; import { parseClusterName, parseContainerName, parseRegionName, parseTaskArn, parseTaskId, unwrapBrandedString, } from "../types.js"; import { askRetry, displayFriendlyError, messages } from "../utils/index.js"; import { displayDryRunResult, generateExecDryRun } from "./dry-run.js"; const DEFAULT_PAGE_SIZE = 50; export async function execECSTaskWithSimpleUIInternal(options) { let retryCount = 0; const maxRetries = 3; while (retryCount <= maxRetries) { try { await execECSTaskWithSimpleUIFlow(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; } } } } async function execECSTaskWithSimpleUIFlow(options) { if (options.dryRun) { messages.info("Starting AWS ECS execute command tool with Simple UI (DRY RUN)..."); } const selections = {}; const region = options.region ? (() => { const regionResult = parseRegionName(options.region); if (!regionResult.success) throw new Error(regionResult.error); return regionResult.data; })() : await (async () => { messages.ui.displayExecSelectionState({ ...selections, region: selections.region ? selections.region : undefined, }); messages.warning("Getting available AWS regions..."); const regionsResult = await getAWSRegions(new EC2Client({ region: "us-east-1" })); if (!regionsResult.success) throw new Error(regionsResult.error); const regions = regionsResult.data; if (isEmpty(regions)) { throw new Error("Failed to get AWS regions"); } process.stdout.write("\x1b[1A"); process.stdout.write("\x1b[2K"); process.stdout.write("\r"); const selectedRegion = await search({ message: "Search and select AWS region:", source: async (input) => await searchRegions(regions, input || ""), pageSize: DEFAULT_PAGE_SIZE, }); if (!isString(selectedRegion)) { throw new Error("Invalid region selection"); } const regionResult = parseRegionName(selectedRegion); if (!regionResult.success) throw new Error(regionResult.error); return regionResult.data; })(); selections.region = region; messages.ui.displayExecSelectionState({ ...selections, region: selections.region ? unwrapBrandedString(selections.region) : undefined, }); const ecsClient = new ECSClient({ region: unwrapBrandedString(region) }); const selectedCluster = options.cluster ? await (async () => { const clusterResult = parseClusterName(options.cluster); if (!clusterResult.success) throw new Error(clusterResult.error); selections.cluster = clusterResult.data; messages.warning("Getting ECS clusters..."); const clustersResult = await getECSClustersWithExecCapability(ecsClient); if (!clustersResult.success) throw new Error(clustersResult.error); const clusters = clustersResult.data; const targetClusterName = clusterResult.data; const cluster = clusters.find((c) => c.clusterName === targetClusterName); if (!cluster) { throw new Error(`ECS cluster not found or does not support exec: ${options.cluster}`); } return cluster; })() : await (async () => { messages.warning("Getting ECS clusters with exec capability..."); const clustersResult = await getECSClustersWithExecCapability(ecsClient); if (!clustersResult.success) throw new Error(clustersResult.error); const clusters = clustersResult.data; if (isEmpty(clusters)) { throw new Error("No ECS clusters found with exec capability"); } process.stdout.write("\x1b[1A"); process.stdout.write("\x1b[2K"); process.stdout.write("\r"); const cluster = await search({ message: "Search and select ECS cluster:", source: async (input) => await searchClusters(clusters, input || ""), pageSize: DEFAULT_PAGE_SIZE, }); if (!cluster || typeof cluster !== "object" || !("clusterName" in cluster) || !("clusterArn" in cluster)) { throw new Error("Invalid cluster selection"); } const clusterNameResult = parseClusterName(cluster.clusterName); if (!clusterNameResult.success) throw new Error(clusterNameResult.error); selections.cluster = clusterNameResult.data; return cluster; })(); messages.ui.displayExecSelectionState({ ...selections, region: selections.region ? unwrapBrandedString(selections.region) : undefined, cluster: selections.cluster ? unwrapBrandedString(selections.cluster) : undefined, }); const selectedTask = options.task ? await (async () => { const taskIdResult = parseTaskId(options.task); const taskArnResult = parseTaskArn(options.task); selections.task = taskIdResult.success ? taskIdResult.data : taskArnResult.success ? taskArnResult.data : undefined; if (!selections.task) throw new Error(`Invalid task id or arn: ${options.task}`); messages.warning("Getting ECS tasks..."); const tasksResult = await getECSTasksWithExecCapability(ecsClient, selectedCluster); if (!tasksResult.success) throw new Error(tasksResult.error); const tasks = tasksResult.data; const task = tasks.find((t) => (taskIdResult.success && t.taskId === taskIdResult.data) || (taskArnResult.success && t.taskArn === taskArnResult.data)); if (!task) { throw new Error(`ECS task not found or does not support exec: ${options.task}`); } return task; })() : await (async () => { messages.warning("Getting ECS tasks with exec capability..."); const tasksResult = await getECSTasksWithExecCapability(ecsClient, selectedCluster); if (!tasksResult.success) throw new Error(tasksResult.error); const tasks = tasksResult.data; if (isEmpty(tasks)) { throw new Error("No ECS tasks found with exec capability in this cluster"); } process.stdout.write("\x1b[1A"); process.stdout.write("\x1b[2K"); process.stdout.write("\r"); const selectedTaskArn = await search({ message: "Search and select ECS task:", source: async (input) => await searchTasks(tasks, input || ""), pageSize: DEFAULT_PAGE_SIZE, }); if (typeof selectedTaskArn !== "string") { throw new Error("Invalid task selection"); } const taskArnResult = parseTaskArn(selectedTaskArn); if (!taskArnResult.success) throw new Error(taskArnResult.error); const task = tasks.find((t) => t.taskArn === taskArnResult.data); if (!task) { throw new Error(`Selected task not found: ${selectedTaskArn}`); } selections.task = task.taskId; return task; })(); messages.ui.displayExecSelectionState({ ...selections, region: selections.region ? unwrapBrandedString(selections.region) : undefined, cluster: selections.cluster ? unwrapBrandedString(selections.cluster) : undefined, task: selections.task ? unwrapBrandedString(selections.task) : undefined, }); const selectedContainer = options.container ? (() => { const containerResult = parseContainerName(options.container); if (!containerResult.success) throw new Error(containerResult.error); selections.container = containerResult.data; return containerResult.data; })() : await (async () => { messages.warning("Getting container list..."); const containersResult = await getECSTaskContainers({ ecsClient, clusterName: selectedCluster.clusterName, taskArn: selectedTask.realTaskArn, }); if (!containersResult.success) throw new Error(containersResult.error); const containers = containersResult.data; if (isEmpty(containers)) { throw new Error("No containers found in this task"); } process.stdout.write("\x1b[1A"); process.stdout.write("\x1b[2K"); process.stdout.write("\r"); const selectedContainerName = await search({ message: "Search and select container:", source: async (input) => await searchContainers(containers.map((c) => String(c)), input || ""), pageSize: DEFAULT_PAGE_SIZE, }); if (typeof selectedContainerName !== "string") { throw new Error("Invalid container selection"); } const containerResult = parseContainerName(selectedContainerName); if (!containerResult.success) throw new Error(containerResult.error); const container = containerResult.data; selections.container = container; return container; })(); messages.ui.displayExecSelectionState({ ...selections, region: selections.region ? selections.region : undefined, cluster: selections.cluster ? selections.cluster : undefined, task: selections.task ? selections.task : undefined, container: selections.container ? selections.container : undefined, }); const command = options.command ? (() => { selections.command = options.command; return options.command; })() : await (async () => { const cmd = await input({ message: "Enter command to execute:", default: "/bin/bash", }); selections.command = cmd; return cmd; })(); messages.ui.displayExecSelectionState({ ...selections, region: selections.region ? unwrapBrandedString(selections.region) : undefined, cluster: selections.cluster ? unwrapBrandedString(selections.cluster) : undefined, task: selections.task ? unwrapBrandedString(selections.task) : undefined, container: selections.container ? unwrapBrandedString(selections.container) : undefined, command: selections.command, }); if (options.dryRun) { const dryRunResult = generateExecDryRun({ region, cluster: selectedCluster.clusterName, task: selectedTask.taskId, container: selectedContainer, command, }); displayDryRunResult(dryRunResult); messages.success("Dry run completed successfully."); } else { await executeECSCommand({ region, clusterName: selectedCluster.clusterName, taskArn: selectedTask.realTaskArn, containerName: selectedContainer, command, }); } }