Skip to content

Commit 7da4ae3

Browse files
authored
Enhance ECS cluster and task selection with more detailed information and navigation (#45)
- Updated `listClusters()` to return detailed cluster information including services, tasks, and container instances count - Modified `selectCluster()` to display additional cluster details in the selection menu - Improved `selectTask()` and `selectContainer()` with sorting, more informative display, and back navigation support - Added ability to go back and re-select clusters or tasks during the interactive selection process - Enhanced error handling and logging for cluster, task, and container information retrieval - update packages to latest
1 parent 4edcef9 commit 7da4ae3

File tree

3 files changed

+418
-208
lines changed

3 files changed

+418
-208
lines changed

index.js

Lines changed: 173 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,6 @@ const config = new Conf({
4949
},
5050
});
5151

52-
53-
5452
const SYNC_INTERVAL = 1000 * 60 * 60; // 1 hour
5553

5654
// Fancy banner
@@ -168,30 +166,93 @@ const initAWS = async () => {
168166
};
169167

170168
/**
171-
* Lists ECS clusters
169+
* Lists ECS clusters along with their service, task, and container instance counts
172170
* @param {ECS} ecs - AWS ECS client
173-
* @returns {Promise<string[]>} Array of cluster names
171+
* @returns {Promise<Array<{clusterName: string, servicesCount: number, tasksCount: number, containerInstancesCount: number}>>} Clusters with details
174172
*/
175173
async function listClusters(ecs) {
176174
try {
177175
const spinner = ora("Fetching clusters...").start();
178176
const { clusterArns } = await ecs.listClusters({});
179177
spinner.succeed("Clusters fetched");
180178

181-
return clusterArns.map((arn) => arn.split("/").pop());
179+
const clusters = await Promise.all(
180+
clusterArns.map(async (arn) => {
181+
const clusterName = arn.split("/").pop();
182+
183+
let servicesCount = 0;
184+
let tasksCount = 0;
185+
let containerInstancesCount = 0;
186+
187+
// Fetch services count
188+
try {
189+
const servicesResult = await ecs.listServices({
190+
cluster: clusterName,
191+
});
192+
servicesCount = servicesResult.serviceArns
193+
? servicesResult.serviceArns.length
194+
: 0;
195+
} catch (err) {
196+
logger.error(
197+
chalk.red(
198+
`Error fetching services for cluster ${clusterName}: ${err.message}`
199+
)
200+
);
201+
}
202+
203+
// Fetch tasks count
204+
try {
205+
const tasksResult = await ecs.listTasks({ cluster: clusterName });
206+
tasksCount = tasksResult.taskArns ? tasksResult.taskArns.length : 0;
207+
} catch (err) {
208+
logger.error(
209+
chalk.red(
210+
`Error fetching tasks for cluster ${clusterName}: ${err.message}`
211+
)
212+
);
213+
}
214+
215+
// Fetch container instances count
216+
try {
217+
const containerInstancesResult = await ecs.listContainerInstances({
218+
cluster: clusterName,
219+
});
220+
containerInstancesCount =
221+
containerInstancesResult.containerInstanceArns
222+
? containerInstancesResult.containerInstanceArns.length
223+
: 0;
224+
} catch (err) {
225+
logger.error(
226+
chalk.red(
227+
`Error fetching container instances for cluster ${clusterName}: ${err.message}`
228+
)
229+
);
230+
}
231+
232+
return {
233+
clusterName,
234+
servicesCount,
235+
tasksCount,
236+
containerInstancesCount,
237+
};
238+
})
239+
);
240+
241+
return clusters;
182242
} catch (err) {
183243
logger.error(chalk.red(err.message));
184244
}
185245
}
186246

187247
/**
188-
* Prompts user to select an ECS cluster
248+
* Prompts user to select an ECS cluster,
249+
* now displaying the number of services, tasks, and container instances.
189250
* @param {ECS} ecs - AWS ECS client
190251
* @returns {Promise<string>} Selected cluster name
191252
*/
192253
async function selectCluster(ecs) {
193254
const clusters = await listClusters(ecs);
194-
if (clusters.length === 0) {
255+
if (!clusters || clusters.length === 0) {
195256
logger.warn(chalk.yellow("No clusters found."));
196257
process.exit(0);
197258
}
@@ -202,21 +263,26 @@ async function selectCluster(ecs) {
202263
message: chalk.blue("Select ECS cluster:"),
203264
prefix: "🚀",
204265
choices: clusters.map((c) => ({
205-
name: chalk.green(c),
206-
value: c,
266+
name:
267+
chalk.green(c.clusterName) +
268+
chalk.yellow(
269+
` (Services: ${c.servicesCount}, Tasks: ${c.tasksCount}, Container Instances: ${c.containerInstancesCount})`
270+
),
271+
value: c.clusterName,
207272
})),
208273
},
209274
]);
210275
return cluster;
211276
}
212277

213278
/**
214-
* Prompts user to select a task within a cluster
279+
* Prompts user to select a task within a cluster, optionally allowing going back.
215280
* @param {ECS} ecs - AWS ECS client
216281
* @param {string} cluster - Cluster name
217-
* @returns {Promise<string>} Selected task ARN
282+
* @param {boolean} allowBack - Whether to allow going back (adds a "← Go Back" option)
283+
* @returns {Promise<string>} Selected task ARN or '__BACK__'
218284
*/
219-
async function selectTask(ecs, cluster) {
285+
async function selectTask(ecs, cluster, allowBack = false) {
220286
try {
221287
const spinner = ora("Fetching tasks...").start();
222288
const { taskArns } = await ecs.listTasks({ cluster });
@@ -234,23 +300,56 @@ async function selectTask(ecs, cluster) {
234300

235301
spinner.succeed("Tasks fetched");
236302

303+
// Sort the tasks alphabetically by the task definition name.
304+
tasks.sort((a, b) => {
305+
const aName = (a.taskDefinitionArn.split("/").pop() || "").toLowerCase();
306+
const bName = (b.taskDefinitionArn.split("/").pop() || "").toLowerCase();
307+
return aName.localeCompare(bName);
308+
});
309+
310+
// Build choices array
311+
const choices = tasks.map((task) => {
312+
// Extract the task definition name
313+
const taskDefName = task.taskDefinitionArn.split("/").pop();
314+
// Extract the task ID from the task ARN and get its last 6 characters
315+
const taskId = task.taskArn.split("/").pop();
316+
const shortTaskId = taskId.slice(-6);
317+
// Format the task started at time if available
318+
const startedAt = task.startedAt
319+
? new Date(task.startedAt).toLocaleString()
320+
: "N/A";
321+
return {
322+
name: `${chalk.green(taskDefName)} ${chalk.yellow(
323+
`(ID: ${shortTaskId}, ${task.lastStatus}, started at: ${startedAt})`
324+
)}`,
325+
value: task.taskArn,
326+
};
327+
});
328+
329+
// Add the "Go Back" option if allowed
330+
if (allowBack) {
331+
choices.unshift({
332+
name: chalk.blue("← Go Back"),
333+
value: "__BACK__",
334+
});
335+
}
336+
237337
const { taskArn } = await inquirer.prompt([
238338
{
239339
type: "list",
240340
name: "taskArn",
241341
message: chalk.blue("Select task:"),
242342
prefix: "📦",
243-
choices: tasks.map((task) => ({
244-
name: `${chalk.green(
245-
task.taskDefinitionArn.split("/").pop()
246-
)} ${chalk.yellow(`(${task.lastStatus})`)}`,
247-
value: task.taskArn,
248-
})),
343+
choices,
344+
loop: false,
345+
pageSize: choices.length,
249346
},
250347
]);
348+
251349
return taskArn;
252350
} catch (err) {
253351
logger.error(chalk.red(err.message));
352+
process.exit(1);
254353
}
255354
}
256355

@@ -279,21 +378,42 @@ async function getTaskDetails(ecs, cluster, taskArn) {
279378
}
280379

281380
/**
282-
* Prompts user to select a container within a task
381+
* Prompts user to select a container within a task, optionally allowing to go back.
283382
* @param {ECS} ecs - AWS ECS client
284383
* @param {string} cluster - Cluster name
285384
* @param {string} taskArn - Task ARN
286-
* @returns {Promise<string>} Selected container name
385+
* @param {boolean} allowBack - Whether to allow going back (adds a "← Go Back" option)
386+
* @returns {Promise<string>} Selected container name or '__BACK__'
287387
*/
288-
async function selectContainer(ecs, cluster, taskArn) {
388+
async function selectContainer(ecs, cluster, taskArn, allowBack = false) {
289389
const spinner = ora("Fetching container details...").start();
290390
const task = await getTaskDetails(ecs, cluster, taskArn);
291391
const containers = task.containers;
292392
spinner.succeed("Container details fetched");
293393

294-
if (containers.length === 1) {
394+
let choices = [];
395+
396+
// If only one container and back navigation is not requested, auto-select it.
397+
// Otherwise, present a list including a "Go Back" option if allowed.
398+
if (containers.length === 1 && !allowBack) {
295399
logger.info(chalk.dim("Single container detected, auto-selecting..."));
296400
return containers[0].name;
401+
} else {
402+
choices = containers.map((container) => {
403+
return {
404+
name: `${chalk.green(container.name)} ${chalk.yellow(
405+
`(${container.lastStatus})`
406+
)}`,
407+
value: container.name,
408+
};
409+
});
410+
411+
if (allowBack) {
412+
choices.unshift({
413+
name: chalk.blue("← Go Back"),
414+
value: "__BACK__",
415+
});
416+
}
297417
}
298418

299419
const { containerName } = await inquirer.prompt([
@@ -302,12 +422,7 @@ async function selectContainer(ecs, cluster, taskArn) {
302422
name: "containerName",
303423
message: chalk.blue("Select container:"),
304424
prefix: "🐳",
305-
choices: containers.map((container) => ({
306-
name: `${chalk.green(container.name)} ${chalk.yellow(
307-
`(${container.lastStatus})`
308-
)}`,
309-
value: container.name,
310-
})),
425+
choices,
311426
},
312427
]);
313428

@@ -497,16 +612,37 @@ program
497612
.action(async () => {
498613
try {
499614
const ecs = await initAWS();
500-
const cluster = await selectCluster(ecs);
501-
const taskArn = await selectTask(ecs, cluster);
502-
const containerName = await selectContainer(ecs, cluster, taskArn);
615+
let cluster, taskArn, containerName;
616+
617+
// Cluster selection (top-level; no back option here)
618+
cluster = await selectCluster(ecs);
619+
620+
// Wrap prompts in loops to allow backward navigation.
621+
while (true) {
622+
// Task selection: allow going back to re-select cluster.
623+
taskArn = await selectTask(ecs, cluster, true);
624+
if (taskArn === "__BACK__") {
625+
cluster = await selectCluster(ecs);
626+
continue; // go back and select a new cluster
627+
}
503628

504-
logger.info(
505-
chalk.green(
506-
`🚀 Connecting to container ${chalk.bold(containerName)}...`
507-
)
508-
);
509-
await executeCommand(cluster, taskArn, containerName);
629+
// Container selection: allow going back to re-select task.
630+
while (true) {
631+
containerName = await selectContainer(ecs, cluster, taskArn, true);
632+
if (containerName === "__BACK__") {
633+
// Go back to task selection
634+
break;
635+
}
636+
637+
logger.info(
638+
chalk.green(
639+
`🚀 Connecting to container ${chalk.bold(containerName)}...`
640+
)
641+
);
642+
await executeCommand(cluster, taskArn, containerName);
643+
process.exit(0);
644+
}
645+
}
510646
} catch (err) {
511647
logger.error(chalk.red(err.message));
512648
process.exit(1);

0 commit comments

Comments
 (0)