238 lines
7.8 KiB
TypeScript
238 lines
7.8 KiB
TypeScript
import { GluegunMenuToolbox } from '@lenne.tech/gluegun-menu';
|
|
import chalk = require('chalk');
|
|
import { defaultMenuSettings } from '../../../../globals';
|
|
import { getTestStacksInfo } from '../../../../common/testservers';
|
|
const { AutoComplete } = require('enquirer');
|
|
|
|
module.exports = {
|
|
name: 'ssh-to-test-server',
|
|
alias: ['sts'],
|
|
description: 'SSH into a test server in ECS (a e sts)',
|
|
hidden: false,
|
|
run: async (toolbox: GluegunMenuToolbox) => {
|
|
const { system, strings, print } = toolbox;
|
|
|
|
try {
|
|
// Get all test stacks
|
|
print.info('Fetching available test servers...');
|
|
const testStacks = await getTestStacksInfo(toolbox);
|
|
|
|
if (testStacks.length === 0) {
|
|
print.error('No test servers found');
|
|
return;
|
|
}
|
|
|
|
// Filter only running/complete stacks
|
|
const availableStacks = testStacks.filter((stack) =>
|
|
['CREATE_COMPLETE', 'UPDATE_COMPLETE'].includes(stack.status),
|
|
);
|
|
|
|
if (availableStacks.length === 0) {
|
|
print.error('No running test servers found');
|
|
return;
|
|
}
|
|
|
|
// Create options for server selection
|
|
const options = availableStacks.map((stack) => {
|
|
const statusColor =
|
|
stack.status === 'CREATE_COMPLETE' ||
|
|
stack.status === 'UPDATE_COMPLETE'
|
|
? chalk.green('✓')
|
|
: chalk.yellow('⚠');
|
|
return {
|
|
name: `${statusColor} ${stack.name} (${stack.branch})`,
|
|
value: stack.name,
|
|
};
|
|
});
|
|
|
|
// Let user choose a server
|
|
const prompt = new AutoComplete({
|
|
name: 'testserver',
|
|
message: 'Choose a test server to SSH into:',
|
|
choices: options,
|
|
limit: Math.min(options.length, 10),
|
|
});
|
|
|
|
const selectedStackName = await prompt.run();
|
|
const selectedStack = availableStacks.find(
|
|
(stack) => stack.name === selectedStackName,
|
|
);
|
|
|
|
if (!selectedStack) {
|
|
print.error('Selected stack not found');
|
|
return;
|
|
}
|
|
|
|
print.info(`Selected: ${chalk.yellow(selectedStackName)}`);
|
|
|
|
// Get the actual ECS cluster name from CloudFormation stack
|
|
print.info('Getting ECS cluster name from CloudFormation stack...');
|
|
const stackDetailsResponse = await system.run(
|
|
`aws cloudformation describe-stacks --stack-name ${selectedStackName} --output json`,
|
|
{ trim: true },
|
|
);
|
|
|
|
const stackDetails = JSON.parse(stackDetailsResponse);
|
|
|
|
if (!stackDetails.Stacks || stackDetails.Stacks.length === 0) {
|
|
print.error('Stack details not found');
|
|
return;
|
|
}
|
|
|
|
const stack = stackDetails.Stacks[0];
|
|
let clusterName = null;
|
|
|
|
// Try to find cluster name from stack outputs first
|
|
if (stack.Outputs) {
|
|
const clusterOutput = stack.Outputs.find(
|
|
(output) =>
|
|
output.OutputKey.toLowerCase().includes('cluster') ||
|
|
output.OutputKey.toLowerCase().includes('ecs'),
|
|
);
|
|
if (clusterOutput) {
|
|
clusterName = clusterOutput.OutputValue;
|
|
}
|
|
}
|
|
|
|
// If not found in outputs, try to get from stack resources
|
|
if (!clusterName) {
|
|
print.info(
|
|
'Cluster name not found in outputs, checking stack resources...',
|
|
);
|
|
const resourcesResponse = await system.run(
|
|
`aws cloudformation describe-stack-resources --stack-name ${selectedStackName} --output json`,
|
|
{ trim: true },
|
|
);
|
|
|
|
const resources = JSON.parse(resourcesResponse);
|
|
const clusterResource = resources.StackResources.find(
|
|
(resource) => resource.ResourceType === 'AWS::ECS::Cluster',
|
|
);
|
|
|
|
if (clusterResource) {
|
|
clusterName = clusterResource.PhysicalResourceId;
|
|
}
|
|
}
|
|
|
|
// If still not found, fall back to stack name (original behavior)
|
|
if (!clusterName) {
|
|
print.warning(
|
|
'Could not find ECS cluster name from stack, using stack name as fallback',
|
|
);
|
|
clusterName = selectedStackName;
|
|
}
|
|
|
|
print.info(`Using ECS cluster: ${chalk.cyan(clusterName)}`);
|
|
|
|
// Get running tasks for the cluster
|
|
print.info('Fetching ECS tasks...');
|
|
const tasksResponse = await system.run(
|
|
`aws ecs list-tasks --cluster ${clusterName} --desired-status RUNNING --output json`,
|
|
{ trim: true },
|
|
);
|
|
|
|
const tasks = JSON.parse(tasksResponse);
|
|
|
|
if (!tasks.taskArns || tasks.taskArns.length === 0) {
|
|
print.error(`No running tasks found in cluster ${clusterName}`);
|
|
return;
|
|
}
|
|
|
|
// Get task details to find containers
|
|
print.info('Getting task details...');
|
|
const taskDetailsResponse = await system.run(
|
|
`aws ecs describe-tasks --cluster ${clusterName} --tasks ${tasks.taskArns.join(' ')} --output json`,
|
|
{ trim: true },
|
|
);
|
|
|
|
const taskDetails = JSON.parse(taskDetailsResponse);
|
|
|
|
if (!taskDetails.tasks || taskDetails.tasks.length === 0) {
|
|
print.error('No task details found');
|
|
return;
|
|
}
|
|
|
|
// Find the first task with containers
|
|
let selectedTask = null;
|
|
let selectedContainer = null;
|
|
|
|
for (const task of taskDetails.tasks) {
|
|
if (task.containers && task.containers.length > 0) {
|
|
selectedTask = task;
|
|
// Try to find a web/app container first, otherwise use the first one
|
|
const webContainer = task.containers.find(
|
|
(container) =>
|
|
container.name.toLowerCase().includes('web') ||
|
|
container.name.toLowerCase().includes('app') ||
|
|
container.name.toLowerCase().includes('photowall'),
|
|
);
|
|
selectedContainer = webContainer || task.containers[0];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!selectedTask || !selectedContainer) {
|
|
print.error('No suitable task/container found');
|
|
return;
|
|
}
|
|
|
|
// Extract task ID from ARN
|
|
const taskId = selectedTask.taskArn.split('/').pop();
|
|
|
|
print.info(`Task ID: ${chalk.cyan(taskId)}`);
|
|
print.info(`Container: ${chalk.cyan(selectedContainer.name)}`);
|
|
print.info(`Cluster: ${chalk.cyan(clusterName)}`);
|
|
|
|
// Build the SSH command
|
|
const sshCommand = `aws ecs execute-command --cluster ${clusterName} --task ${taskId} --container ${selectedContainer.name} --interactive --command "/bin/bash"`;
|
|
|
|
print.info('\nSSH command to connect to the container:');
|
|
print.info(chalk.yellow(sshCommand));
|
|
print.info(
|
|
'\n' +
|
|
chalk.green(
|
|
'You can use this command to connect to the container manually.',
|
|
),
|
|
);
|
|
print.info(
|
|
chalk.dim(
|
|
'Note: Type "exit" to disconnect from the container when connected.\n',
|
|
),
|
|
);
|
|
|
|
const { confirmPaste } = await toolbox.prompt.ask([
|
|
{
|
|
type: 'confirm',
|
|
name: 'confirmPaste',
|
|
message:
|
|
'Would you like to put this command in your clipboard for easy pasting?',
|
|
initial: true,
|
|
},
|
|
]);
|
|
|
|
if (confirmPaste) {
|
|
await toolbox.system.run(
|
|
`echo "${sshCommand.replace(/"/g, '\\"')}" | pbcopy`,
|
|
);
|
|
print.info(chalk.green('SSH command copied to clipboard!'));
|
|
} else {
|
|
print.info(chalk.dim('Command not copied to clipboard.'));
|
|
}
|
|
} catch (error) {
|
|
if (error.message && error.message.includes('was cancelled')) {
|
|
print.info('Operation cancelled by user');
|
|
} else {
|
|
print.error(`Error: ${error.message || error}`);
|
|
print.error('Make sure:');
|
|
print.error('1. AWS CLI is configured and you have proper permissions');
|
|
print.error('2. The ECS cluster exists and has running tasks');
|
|
print.error('3. ECS Exec is enabled on the service/task');
|
|
}
|
|
}
|
|
|
|
if (toolbox.fromMenu()) {
|
|
await toolbox.menu.showMenu('aws ecs', defaultMenuSettings);
|
|
}
|
|
},
|
|
};
|