UNPKG

ecs-pf

Version:

CLI for port-forwarding to RDS via AWS ECS

529 lines (528 loc) 23.9 kB
import { DescribeRegionsCommand } from "@aws-sdk/client-ec2"; import { DescribeClustersCommand, DescribeServicesCommand, DescribeTasksCommand, ListClustersCommand, ListServicesCommand, ListTasksCommand, UpdateServiceCommand, } from "@aws-sdk/client-ecs"; import { DescribeDBInstancesCommand, } from "@aws-sdk/client-rds"; import { isEmpty } from "remeda"; import { failure, parseClusterArn, parseClusterName, parseContainerName, parseDatabaseEngine, parseDBEndpoint, parseDBInstanceIdentifier, parsePort, parseRegionName, parseRuntimeId, parseServiceArn, parseServiceName, parseTaskArn, parseTaskId, parseTaskStatus, success, } from "./types.js"; import { messages } from "./utils/index.js"; export async function getECSClustersWithExecCapability(ecsClient) { const allClustersResult = await getECSClustersResult(ecsClient); if (!allClustersResult.success) { return allClustersResult; } const allClusters = allClustersResult.data; const execCheckPromises = allClusters.map(async (cluster) => { const hasExecCapability = await checkECSExecCapability(ecsClient, cluster); return { cluster, hasExecCapability }; }); const execCheckResults = await Promise.all(execCheckPromises); const clustersWithExec = execCheckResults .filter(({ hasExecCapability }) => hasExecCapability) .map(({ cluster }) => cluster); return success(clustersWithExec); } export async function getAWSRegions(ec2Client) { return getAWSRegionsResult(ec2Client); } export async function getECSClusters(ecsClient) { return getECSClustersResult(ecsClient); } export async function getECSTasks(ecsClient, cluster) { return getECSTasksResult(ecsClient, cluster); } export async function getRDSInstances(rdsClient) { return getRDSInstancesResult(rdsClient); } export async function getECSTasksWithExecCapability(ecsClient, cluster) { const allTasksResult = await getECSTasksResult(ecsClient, cluster); if (!allTasksResult.success) { return allTasksResult; } const runningTasks = allTasksResult.data.filter((task) => task.taskStatus === "RUNNING"); return success(runningTasks); } export async function getECSTaskContainers(params) { const { ecsClient, clusterName, taskArn } = params; try { const describeCommand = new DescribeTasksCommand({ cluster: clusterName, tasks: [taskArn], }); const response = await ecsClient.send(describeCommand); if (!response.tasks || isEmpty(response.tasks)) { return failure("Task not found"); } const [task] = response.tasks; if (!task) { return failure("Task data not found"); } const containers = []; if (task.containers) { for (const container of task.containers) { if (container.name && container.lastStatus === "RUNNING") { const containerNameResult = parseContainerName(container.name); if (containerNameResult.success) { containers.push(containerNameResult.data); } } } } return success(containers); } catch (error) { if (error instanceof Error) { if (error.name === "ClusterNotFoundException") { return failure(`ECS cluster "${clusterName}" not found. Please verify the cluster exists.`); } if (error.name === "TaskNotFoundException") { return failure(`ECS task not found. Please verify the task exists and is running.`); } } return failure(`Failed to get task containers: ${error instanceof Error ? error.message : String(error)}`); } } async function getECSClustersResult(ecsClient) { try { const listCommand = new ListClustersCommand({}); const listResponse = await ecsClient.send(listCommand); if (!listResponse.clusterArns || isEmpty(listResponse.clusterArns)) { return success([]); } const describeCommand = new DescribeClustersCommand({ clusters: listResponse.clusterArns, }); const describeResponse = await ecsClient.send(describeCommand); const clusters = []; if (describeResponse.clusters) { for (const cluster of describeResponse.clusters) { if (cluster.clusterName && cluster.clusterArn) { const clusterNameResult = parseClusterName(cluster.clusterName); const clusterArnResult = parseClusterArn(cluster.clusterArn); if (clusterNameResult.success && clusterArnResult.success) { clusters.push({ clusterName: clusterNameResult.data, clusterArn: clusterArnResult.data, }); } } } } return success(clusters); } catch (error) { if (error instanceof Error) { if (error.name === "UnauthorizedOperation" || error.name === "AccessDenied") { return failure("Access denied to ECS clusters. Please check your IAM policies."); } if (error.message.includes("region")) { return failure("ECS service is not available in the specified region."); } } return failure(`Failed to get ECS clusters: ${error instanceof Error ? error.message : String(error)}`); } } async function getECSTasksResult(ecsClient, cluster) { try { const servicesCommand = new ListServicesCommand({ cluster: cluster.clusterName, }); const servicesResponse = await ecsClient.send(servicesCommand); const tasks = []; if (servicesResponse.serviceArns) { for (const serviceArn of servicesResponse.serviceArns) { const serviceNameStr = serviceArn.split("/").pop() || serviceArn; const serviceNameResult = parseServiceName(serviceNameStr); if (!serviceNameResult.success) { continue; } const tasksCommand = new ListTasksCommand({ cluster: cluster.clusterName, serviceName: serviceNameStr, desiredStatus: "RUNNING", }); const tasksResponse = await ecsClient.send(tasksCommand); if (tasksResponse.taskArns) { const describeCommand = new DescribeTasksCommand({ cluster: cluster.clusterName, tasks: tasksResponse.taskArns, }); const describeResponse = await ecsClient.send(describeCommand); if (describeResponse.tasks) { for (const task of describeResponse.tasks) { if (task.taskArn && task.containers && task.containers.length > 0 && task.lastStatus === "RUNNING") { const taskIdStr = task.taskArn.split("/").pop() || task.taskArn; const clusterFullNameStr = cluster.clusterArn.split("/").pop() || cluster.clusterName; const runtimeIdStr = task.containers[0]?.runtimeId || ""; const taskIdResult = parseTaskId(taskIdStr); const clusterNameResult = parseClusterName(clusterFullNameStr); const runtimeIdResult = parseRuntimeId(runtimeIdStr); const realTaskArnResult = parseTaskArn(task.taskArn); const taskStatusResult = parseTaskStatus(task.lastStatus || "UNKNOWN"); if (runtimeIdResult.success && taskIdResult.success && clusterNameResult.success && realTaskArnResult.success && taskStatusResult.success && serviceNameResult.success) { const targetArnStr = `ecs:${clusterFullNameStr}_${taskIdStr}_${runtimeIdStr}`; const targetArnResult = parseTaskArn(targetArnStr); if (targetArnResult.success) { const displayName = serviceNameStr; tasks.push({ taskArn: targetArnResult.data, realTaskArn: realTaskArnResult.data, displayName: displayName, runtimeId: runtimeIdResult.data, taskId: taskIdResult.data, clusterName: clusterNameResult.data, serviceName: serviceNameResult.data, taskStatus: taskStatusResult.data, createdAt: task.createdAt, }); } } } } } } } } return success(tasks); } catch (error) { if (error instanceof Error) { if (error.name === "ClusterNotFoundException") { return failure(`ECS cluster "${cluster.clusterName}" not found. Please verify the cluster exists.`); } if (error.name === "UnauthorizedOperation" || error.name === "AccessDenied") { return failure("Access denied to ECS tasks. Please check your IAM policies."); } } return failure(`Failed to get ECS tasks: ${error instanceof Error ? error.message : String(error)}`); } } async function getAWSRegionsResult(ec2Client) { try { const command = new DescribeRegionsCommand({}); const response = await ec2Client.send(command); const regions = []; if (response.Regions) { for (const region of response.Regions) { if (region.RegionName) { const regionNameResult = parseRegionName(region.RegionName); if (regionNameResult.success) { regions.push({ regionName: regionNameResult.data, optInStatus: region.OptInStatus || "opt-in-not-required", }); } } } } const priorityRegions = [ "ap-northeast-1", "ap-northeast-2", "us-east-1", "us-west-2", "eu-west-1", ]; const sortedRegions = regions.sort((a, b) => { const aIndex = priorityRegions.indexOf(a.regionName); const bIndex = priorityRegions.indexOf(b.regionName); if (aIndex !== -1 && bIndex !== -1) { return aIndex - bIndex; } else if (aIndex !== -1) { return -1; } else if (bIndex !== -1) { return 1; } else { return a.regionName.localeCompare(b.regionName); } }); return success(sortedRegions); } catch (error) { if (error instanceof Error) { if (error.name === "UnauthorizedOperation" || error.name === "AccessDenied") { return failure("Access denied to AWS regions. Please check your AWS credentials and IAM permissions."); } } return failure(`Failed to get AWS regions: ${error instanceof Error ? error.message : String(error)}`); } } async function getRDSInstancesResult(rdsClient) { try { const command = new DescribeDBInstancesCommand({}); const response = await rdsClient.send(command); const rdsInstances = []; if (response.DBInstances) { for (const db of response.DBInstances) { if (db.DBInstanceIdentifier && db.Endpoint?.Address && db.Engine && db.DBInstanceStatus === "available") { const dbIdResult = parseDBInstanceIdentifier(db.DBInstanceIdentifier); const endpointResult = parseDBEndpoint(db.Endpoint.Address); const portResult = parsePort(db.Endpoint.Port || 5432); const engineResult = parseDatabaseEngine(db.Engine); if (dbIdResult.success && endpointResult.success && portResult.success && engineResult.success) { rdsInstances.push({ dbInstanceIdentifier: dbIdResult.data, endpoint: endpointResult.data, port: portResult.data, engine: engineResult.data, dbInstanceClass: db.DBInstanceClass || "unknown", dbInstanceStatus: "available", allocatedStorage: db.AllocatedStorage || 0, availabilityZone: db.AvailabilityZone || "unknown", vpcSecurityGroups: db.VpcSecurityGroups?.map((sg) => sg.VpcSecurityGroupId || "") || [], dbSubnetGroup: db.DBSubnetGroup?.DBSubnetGroupName || undefined, createdTime: db.InstanceCreateTime || undefined, }); } } } } const sortedInstances = rdsInstances.sort((a, b) => a.dbInstanceIdentifier.localeCompare(b.dbInstanceIdentifier)); return success(sortedInstances); } catch (error) { if (error instanceof Error) { if (error.name === "UnauthorizedOperation" || error.name === "AccessDenied") { return failure("Access denied to RDS instances. Please check your IAM policies."); } } return failure(`Failed to get RDS instances: ${error instanceof Error ? error.message : String(error)}`); } } export async function checkECSExecCapability(ecsClient, cluster) { try { const describeCommand = new DescribeClustersCommand({ clusters: [cluster.clusterArn], include: ["CONFIGURATIONS"], }); const response = await ecsClient.send(describeCommand); if (!response.clusters || isEmpty(response.clusters)) { return false; } const clusterData = response.clusters[0]; if (clusterData?.configuration?.executeCommandConfiguration) { return true; } return true; } catch { return false; } } export async function getECSServices(ecsClient, cluster) { try { const listCommand = new ListServicesCommand({ cluster: cluster.clusterName, }); const listResponse = await ecsClient.send(listCommand); if (!listResponse.serviceArns || isEmpty(listResponse.serviceArns)) { return success([]); } const describeCommand = new DescribeServicesCommand({ cluster: cluster.clusterName, services: listResponse.serviceArns, }); const describeResponse = await ecsClient.send(describeCommand); const services = []; if (describeResponse.services) { for (const service of describeResponse.services) { if (service.serviceName && service.serviceArn) { const serviceNameResult = parseServiceName(service.serviceName); const serviceArnResult = parseServiceArn(service.serviceArn); if (serviceNameResult.success && serviceArnResult.success) { services.push({ serviceName: serviceNameResult.data, serviceArn: serviceArnResult.data, clusterName: cluster.clusterName, status: service.status || "UNKNOWN", taskDefinition: service.taskDefinition || "", enableExecuteCommand: service.enableExecuteCommand || false, desiredCount: service.desiredCount || 0, runningCount: service.runningCount || 0, pendingCount: service.pendingCount || 0, }); } } } } return success(services); } catch (error) { if (error instanceof Error) { if (error.name === "ClusterNotFoundException") { return failure(`ECS cluster "${cluster.clusterName}" not found.`); } if (error.name === "UnauthorizedOperation" || error.name === "AccessDenied") { return failure("Access denied to ECS services. Please check your IAM policies."); } } return failure(`Failed to get ECS services: ${error instanceof Error ? error.message : String(error)}`); } } export async function getAllECSServices(ecsClient) { const clustersResult = await getECSClusters(ecsClient); if (!clustersResult.success) { return clustersResult; } const BATCH_SIZE = 5; const clusters = clustersResult.data; const allServices = []; for (let i = 0; i < clusters.length; i += BATCH_SIZE) { const batch = clusters.slice(i, i + BATCH_SIZE); const batchStart = i + 1; const batchEnd = Math.min(i + BATCH_SIZE, clusters.length); process.stdout.write(`\rProcessing clusters ${batchStart}-${batchEnd}/${clusters.length}...`); const servicePromises = batch.map(async (cluster) => { const servicesResult = await getECSServices(ecsClient, cluster); return servicesResult.success ? servicesResult.data : []; }); try { const serviceArrays = await Promise.all(servicePromises); allServices.push(...serviceArrays.flat()); } catch (error) { return failure(`Failed to get services from clusters: ${error instanceof Error ? error.message : String(error)}`); } } messages.clearCurrentLine(); return success(allServices); } export async function getAllECSServicesWithoutExec(ecsClient) { const servicesResult = await getAllECSServices(ecsClient); if (!servicesResult.success) { return servicesResult; } const servicesWithoutExec = servicesResult.data.filter((service) => !service.enableExecuteCommand); return success(servicesWithoutExec); } export async function getECSServicesWithoutExec(ecsClient, cluster) { const servicesResult = await getECSServices(ecsClient, cluster); if (!servicesResult.success) { return servicesResult; } const servicesWithoutExec = servicesResult.data.filter((service) => !service.enableExecuteCommand); return success(servicesWithoutExec); } export async function enableECSExecForService(ecsClient, clusterName, serviceName) { try { const describeCommand = new DescribeServicesCommand({ cluster: clusterName, services: [serviceName], }); const describeResponse = await ecsClient.send(describeCommand); if (!describeResponse.services || isEmpty(describeResponse.services)) { return failure(`Service "${serviceName}" not found in cluster "${clusterName}"`); } const service = describeResponse.services[0]; const previousState = service?.enableExecuteCommand || false; if (previousState) { const serviceNameResult = parseServiceName(serviceName); const clusterNameResult = parseClusterName(clusterName); if (!serviceNameResult.success || !clusterNameResult.success) { return failure("Invalid service or cluster name format"); } return success({ serviceName: serviceNameResult.data, clusterName: clusterNameResult.data, previousState: true, newState: true, success: true, }); } const updateCommand = new UpdateServiceCommand({ cluster: clusterName, service: serviceName, enableExecuteCommand: true, }); await ecsClient.send(updateCommand); const serviceNameResult = parseServiceName(serviceName); const clusterNameResult = parseClusterName(clusterName); if (!serviceNameResult.success || !clusterNameResult.success) { return failure("Invalid service or cluster name format"); } return success({ serviceName: serviceNameResult.data, clusterName: clusterNameResult.data, previousState: false, newState: true, success: true, }); } catch (error) { const serviceNameResult = parseServiceName(serviceName); const clusterNameResult = parseClusterName(clusterName); if (!serviceNameResult.success || !clusterNameResult.success) { return failure("Invalid service or cluster name format"); } let errorMessage = `Failed to enable exec for service "${serviceName}": `; if (error instanceof Error) { if (error.name === "ServiceNotFoundException") { errorMessage += "Service not found"; } else if (error.name === "ClusterNotFoundException") { errorMessage += "Cluster not found"; } else if (error.name === "UnauthorizedOperation" || error.name === "AccessDenied") { errorMessage += "Access denied. Please check your IAM policies"; } else { errorMessage += error.message; } } else { errorMessage += String(error); } return success({ serviceName: serviceNameResult.data, clusterName: clusterNameResult.data, previousState: false, newState: false, success: false, error: errorMessage, }); } } export async function enableECSExecForServices(ecsClient, clusterName, serviceNames) { const results = []; for (const serviceName of serviceNames) { const result = await enableECSExecForService(ecsClient, clusterName, serviceName); if (result.success) { results.push(result.data); } else { const serviceNameResult = parseServiceName(serviceName); const clusterNameResult = parseClusterName(clusterName); if (serviceNameResult.success && clusterNameResult.success) { results.push({ serviceName: serviceNameResult.data, clusterName: clusterNameResult.data, previousState: false, newState: false, success: false, error: result.error, }); } } } return success(results); }