UNPKG

@fiftyten/db-toolkit

Version:

Complete database toolkit: connections, migration, and operations via AWS Session Manager

712 lines โ€ข 37.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MigrationManager = void 0; const client_cloudformation_1 = require("@aws-sdk/client-cloudformation"); const client_database_migration_service_1 = require("@aws-sdk/client-database-migration-service"); const chalk_1 = __importDefault(require("chalk")); const readline = __importStar(require("readline")); const mfa_auth_1 = require("./mfa-auth"); const cloudformation_manager_1 = require("./cloudformation-manager"); // Helper function to prompt for confirmation function promptConfirmation(message) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve) => { rl.question(`${message} (y/N): `, (answer) => { rl.close(); resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'); }); }); } class MigrationManager { constructor(region = 'us-west-1') { this.mfaAuthenticated = false; this.region = region; this.cfnClient = new client_cloudformation_1.CloudFormationClient({ region }); this.dmsClient = new client_database_migration_service_1.DatabaseMigrationServiceClient({ region }); this.cfnManager = new cloudformation_manager_1.CloudFormationManager(region); this.mfaAuth = new mfa_auth_1.MfaAuthenticator(region); } /** * Handle AWS API calls with automatic MFA authentication */ async callWithMfaRetry(operation) { try { return await operation(); } catch (error) { // Check if this is an MFA-related error and we haven't already authenticated if (this.mfaAuth.isMfaRequired(error) && !this.mfaAuthenticated) { console.log(chalk_1.default.yellow('โš ๏ธ MFA authentication required for AWS access')); // Attempt MFA authentication const credentials = await this.mfaAuth.authenticateWithMfa(); this.mfaAuth.applyCredentials(credentials); // Recreate clients with new credentials const clientConfig = { region: this.region, credentials: { accessKeyId: credentials.accessKeyId, secretAccessKey: credentials.secretAccessKey, sessionToken: credentials.sessionToken } }; this.cfnClient = new client_cloudformation_1.CloudFormationClient(clientConfig); this.dmsClient = new client_database_migration_service_1.DatabaseMigrationServiceClient(clientConfig); // Mark as authenticated to prevent re-prompting this.mfaAuthenticated = true; // Retry the operation return await operation(); } // Re-throw if not MFA-related or already authenticated throw error; } } /** * Discover available target databases from CloudFormation stacks */ async discoverTargetDatabases(environment) { console.log(chalk_1.default.blue('๐Ÿ” Discovering available target databases...')); const targetDatabases = []; const stackName = `indicator-storage-infra-${environment}`; try { const stack = await this.callWithMfaRetry(async () => { const command = new client_cloudformation_1.DescribeStacksCommand({ StackName: stackName }); return await this.cfnClient.send(command); }); const stackInfo = stack.Stacks?.[0]; if (!stackInfo) { console.log(chalk_1.default.yellow(`โš ๏ธ Storage infrastructure stack not found: ${stackName}`)); return targetDatabases; } const outputs = stackInfo.Outputs || []; // Look for indicator database const indicatorSecretArn = outputs.find(output => output.OutputKey === 'IndicatorDatabaseSecretArn')?.OutputValue; const indicatorEndpoint = outputs.find(output => output.OutputKey === 'IndicatorDatabaseEndpoint')?.OutputValue; const indicatorPort = outputs.find(output => output.OutputKey === 'IndicatorDatabasePort')?.OutputValue; if (indicatorSecretArn) { targetDatabases.push({ name: 'indicator', friendlyName: 'Indicator Database', secretArn: indicatorSecretArn, endpoint: indicatorEndpoint, port: indicatorPort, }); } // Look for copytrading database (if it exists) const copytradingSecretArn = outputs.find(output => output.OutputKey === 'CopytradingDatabaseSecretArn')?.OutputValue; if (copytradingSecretArn) { const copytradingEndpoint = outputs.find(output => output.OutputKey === 'CopytradingDatabaseEndpoint')?.OutputValue; const copytradingPort = outputs.find(output => output.OutputKey === 'CopytradingDatabasePort')?.OutputValue; targetDatabases.push({ name: 'copytrading', friendlyName: 'Copy Trading Database', secretArn: copytradingSecretArn, endpoint: copytradingEndpoint, port: copytradingPort, }); } // Look for any other database patterns const otherSecretOutputs = outputs.filter(output => output.OutputKey?.endsWith('DatabaseSecretArn') && !['IndicatorDatabaseSecretArn', 'CopytradingDatabaseSecretArn'].includes(output.OutputKey)); for (const secretOutput of otherSecretOutputs) { const baseName = secretOutput.OutputKey.replace('DatabaseSecretArn', ''); const endpointKey = `${baseName}DatabaseEndpoint`; const portKey = `${baseName}DatabasePort`; const endpoint = outputs.find(output => output.OutputKey === endpointKey)?.OutputValue; const port = outputs.find(output => output.OutputKey === portKey)?.OutputValue; targetDatabases.push({ name: baseName.toLowerCase(), friendlyName: `${baseName} Database`, secretArn: secretOutput.OutputValue, endpoint, port, }); } if (targetDatabases.length > 0) { console.log(chalk_1.default.green(`โœ… Found ${targetDatabases.length} target database(s):`)); targetDatabases.forEach(db => { console.log(` ${chalk_1.default.yellow(db.friendlyName)} (${db.name})`); if (db.endpoint) { console.log(chalk_1.default.gray(` ${db.endpoint}:${db.port}`)); } }); console.log(''); } else { console.log(chalk_1.default.yellow('โš ๏ธ No target databases found in storage infrastructure')); console.log(chalk_1.default.gray(' Make sure the storage infrastructure is deployed first')); console.log(''); } } catch (error) { console.log(chalk_1.default.yellow(`โš ๏ธ Could not access storage infrastructure stack: ${stackName}`)); console.log(chalk_1.default.gray(' Stack may not be deployed or you may not have access')); console.log(''); } return targetDatabases; } /** * Deploy migration infrastructure using CloudFormation */ async deployMigration(config) { console.log(chalk_1.default.blue('๐Ÿš€ Deploying database migration infrastructure...')); console.log(''); console.log(chalk_1.default.green('๐Ÿ“‹ Migration Configuration:')); console.log(` Environment: ${chalk_1.default.yellow(config.environment)}`); console.log(` Source Database: ${chalk_1.default.yellow(config.legacyEndpoint + '/' + config.legacyDatabase)}`); console.log(` Target Secret: ${chalk_1.default.yellow(config.targetSecretArn)}`); console.log(` Migration Type: ${chalk_1.default.yellow(config.migrationType || 'full-load-and-cdc')}`); console.log(''); // Confirm deployment const confirm = await promptConfirmation('Deploy migration infrastructure with these settings?'); if (!confirm) { console.log(chalk_1.default.yellow('Migration deployment cancelled.')); return; } // Deploy using CloudFormation API directly const stackName = `indicator-migration-stack-${config.environment}`; await this.cfnManager.deployStack({ stackName, region: this.region, parameters: { environmentName: config.environment, vpcId: '', // Will be auto-discovered subnetIds: [], // Will be auto-discovered legacyEndpoint: config.legacyEndpoint, legacyDatabase: config.legacyDatabase, legacyUsername: config.legacyUsername, legacyPassword: config.legacyPassword, targetSecretArn: config.targetSecretArn, migrationType: config.migrationType, notificationEmails: config.notificationEmails } }); } /** * Validate that both source and target endpoint connections are successful */ async validateEndpointConnections(environment) { console.log(chalk_1.default.blue('๐Ÿ” Validating endpoint connections...')); const stackName = `indicator-migration-stack-${environment}`; const outputs = await this.cfnManager.getStackOutputs(stackName); const sourceEndpointArn = outputs['SourceEndpointArn']; const targetEndpointArn = outputs['TargetEndpointArn']; const replicationInstanceArn = outputs['ReplicationInstanceArn']; if (!sourceEndpointArn || !targetEndpointArn || !replicationInstanceArn) { throw new Error('Missing endpoint or replication instance ARNs in stack outputs'); } console.log(chalk_1.default.gray(` Source ARN: ${sourceEndpointArn}`)); console.log(chalk_1.default.gray(` Target ARN: ${targetEndpointArn}`)); console.log(chalk_1.default.gray(` Replication Instance ARN: ${replicationInstanceArn}`)); console.log(''); // Test source endpoint connection console.log(chalk_1.default.blue(' Testing source endpoint connection...')); await this.testAndWaitForConnection(replicationInstanceArn, sourceEndpointArn, 'source'); // Test target endpoint connection console.log(chalk_1.default.blue(' Testing target endpoint connection...')); await this.testAndWaitForConnection(replicationInstanceArn, targetEndpointArn, 'target'); console.log(chalk_1.default.green('โœ… All endpoint connections validated successfully!')); console.log(''); } /** * Test connection and wait for successful result with simplified retry logic */ async testAndWaitForConnection(replicationInstanceArn, endpointArn, endpointType) { const maxRetries = 2; // Reduce retries to avoid MFA issues let retryCount = 0; while (retryCount < maxRetries) { try { if (retryCount > 0) { console.log(chalk_1.default.yellow(` ๐Ÿ”„ Retrying ${endpointType} endpoint connection (attempt ${retryCount + 1}/${maxRetries})...`)); await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10 seconds before retry } // Start connection test console.log(chalk_1.default.blue(` ๐Ÿงช Starting ${endpointType} endpoint connection test...`)); const testCommand = new client_database_migration_service_1.TestConnectionCommand({ ReplicationInstanceArn: replicationInstanceArn, EndpointArn: endpointArn }); await this.dmsClient.send(testCommand); // Wait for connection test to complete with reduced polling let attempts = 0; const maxAttempts = 18; // 3 minutes max wait per attempt (18 * 10s) while (attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10 seconds try { // Use direct client call to avoid MFA retry loops during polling const describeCommand = new client_database_migration_service_1.DescribeConnectionsCommand({ Filters: [ { Name: 'replication-instance-arn', Values: [replicationInstanceArn] }, { Name: 'endpoint-arn', Values: [endpointArn] } ] }); const connections = await this.dmsClient.send(describeCommand); const connection = connections.Connections?.[0]; console.log(chalk_1.default.gray(` ๐Ÿ“Š Connection status: ${connection?.Status || 'unknown'}`)); if (connection?.Status === 'successful') { console.log(chalk_1.default.green(` โœ… ${endpointType} endpoint connection successful`)); return; // Success! Exit the retry loop } else if (connection?.Status === 'failed') { const error = connection.LastFailureMessage || 'Connection test failed'; console.log(chalk_1.default.red(` โŒ ${endpointType} endpoint connection failed:`)); console.log(chalk_1.default.red(` ${error}`)); // If this is the last retry, throw the error if (retryCount === maxRetries - 1) { throw new Error(`${endpointType} endpoint connection failed after ${maxRetries} attempts: ${error}`); } // Otherwise, break out of the wait loop to retry break; } // Still testing, continue waiting process.stdout.write(`\r โณ Testing ${endpointType} endpoint connection... (${attempts + 1}/${maxAttempts})`); } catch (apiError) { // Handle session token errors gracefully - don't fail the entire test if (apiError instanceof Error && apiError.message.includes('Cannot call GetSessionToken with session credentials')) { console.log(chalk_1.default.yellow(` โš ๏ธ Session token limitation - continuing with existing credentials`)); // Continue the loop, don't break or fail } else { console.log(chalk_1.default.yellow(` โš ๏ธ API error while checking connection status: ${apiError instanceof Error ? apiError.message : String(apiError)}`)); } // Continue waiting in both cases } attempts++; } // If we get here, the test timed out if (retryCount === maxRetries - 1) { throw new Error(`${endpointType} endpoint connection test timed out after ${maxRetries} attempts (3 minutes each)`); } console.log(chalk_1.default.yellow(` โฑ๏ธ ${endpointType} endpoint connection test timed out, retrying...`)); } catch (error) { console.log(chalk_1.default.red(` โŒ Error during ${endpointType} endpoint test: ${error instanceof Error ? error.message : String(error)}`)); if (retryCount === maxRetries - 1) { // Final attempt failed, re-throw the error throw error; } } retryCount++; } } /** * Get migration task ARN from stack outputs */ async getMigrationTaskArn(environment) { const stackName = `indicator-migration-stack-${environment}`; const outputs = await this.cfnManager.getStackOutputs(stackName); const taskArn = outputs['MigrationTaskArn']; if (!taskArn) { throw new Error(`Migration task ARN not found in stack outputs for environment: ${environment}`); } return taskArn; } /** * Start migration task */ async startMigration(environment) { console.log(chalk_1.default.blue('๐Ÿš€ Starting database migration...')); try { const taskArn = await this.getMigrationTaskArn(environment); console.log(` Task ARN: ${chalk_1.default.gray(taskArn)}`); console.log(''); // Validate endpoint connections before starting await this.validateEndpointConnections(environment); // Confirm start const confirm = await promptConfirmation('Start full database migration (full-load + CDC)?'); if (!confirm) { console.log(chalk_1.default.yellow('Migration start cancelled.')); return; } await this.callWithMfaRetry(async () => { const command = new client_database_migration_service_1.StartReplicationTaskCommand({ ReplicationTaskArn: taskArn, StartReplicationTaskType: 'start-replication' }); return await this.dmsClient.send(command); }); console.log(chalk_1.default.green('โœ… Migration task started successfully!')); console.log(''); console.log(chalk_1.default.blue('๐Ÿ“Š Monitor progress with:')); console.log(` ${chalk_1.default.cyan(`fiftyten-db migrate status ${environment}`)}`); console.log(''); console.log(chalk_1.default.gray('๐Ÿ’ก The migration will:')); console.log(chalk_1.default.gray(' 1. Perform full load of all data')); console.log(chalk_1.default.gray(' 2. Start change data capture (CDC) for ongoing replication')); console.log(chalk_1.default.gray(' 3. Continue until manually stopped')); } catch (error) { console.error(chalk_1.default.red('โŒ Failed to start migration:'), error instanceof Error ? error.message : String(error)); throw error; } } /** * Stop migration task */ async stopMigration(environment) { console.log(chalk_1.default.blue('๐Ÿ›‘ Stopping database migration...')); try { const taskArn = await this.getMigrationTaskArn(environment); // Confirm stop const confirm = await promptConfirmation('Stop the migration task? This will halt all data replication.'); if (!confirm) { console.log(chalk_1.default.yellow('Migration stop cancelled.')); return; } await this.callWithMfaRetry(async () => { const command = new client_database_migration_service_1.StopReplicationTaskCommand({ ReplicationTaskArn: taskArn }); return await this.dmsClient.send(command); }); console.log(chalk_1.default.green('โœ… Migration task stopped successfully!')); } catch (error) { console.error(chalk_1.default.red('โŒ Failed to stop migration:'), error instanceof Error ? error.message : String(error)); throw error; } } /** * Get migration status */ async getMigrationStatus(environment) { const taskArn = await this.getMigrationTaskArn(environment); const response = await this.callWithMfaRetry(async () => { const command = new client_database_migration_service_1.DescribeReplicationTasksCommand({ Filters: [ { Name: 'replication-task-arn', Values: [taskArn] } ] }); return await this.dmsClient.send(command); }); const task = response.ReplicationTasks?.[0]; if (!task) { throw new Error(`Migration task not found: ${taskArn}`); } return { taskArn: task.ReplicationTaskArn, taskId: task.ReplicationTaskIdentifier, status: task.Status, progress: task.ReplicationTaskStats?.FullLoadProgressPercent || 0, fullLoadProgressPercent: task.ReplicationTaskStats?.FullLoadProgressPercent, cdcStartDate: task.ReplicationTaskStats?.StartDate, stopReason: task.StopReason, replicationTaskCreationDate: task.ReplicationTaskCreationDate, replicationTaskStartDate: task.ReplicationTaskStartDate, }; } /** * Show migration status */ async showMigrationStatus(environment) { console.log(chalk_1.default.blue(`๐Ÿ“Š Migration Status - ${environment.toUpperCase()}`)); console.log(''); try { const status = await this.getMigrationStatus(environment); // Status overview console.log(chalk_1.default.green('๐Ÿ”„ Task Information:')); console.log(` Task ID: ${chalk_1.default.yellow(status.taskId)}`); console.log(` Status: ${this.getStatusColor(status.status)}`); console.log(` Progress: ${chalk_1.default.yellow(status.progress + '%')}`); if (status.replicationTaskCreationDate) { console.log(` Created: ${chalk_1.default.gray(status.replicationTaskCreationDate.toLocaleString())}`); } if (status.replicationTaskStartDate) { console.log(` Started: ${chalk_1.default.gray(status.replicationTaskStartDate.toLocaleString())}`); } if (status.cdcStartDate) { console.log(` CDC Started: ${chalk_1.default.gray(status.cdcStartDate.toLocaleString())}`); } if (status.stopReason) { console.log(` Stop Reason: ${chalk_1.default.red(status.stopReason)}`); } console.log(''); // Get table statistics if task is running if (status.status === 'running' || status.status === 'stopped') { await this.showTableStatistics(status.taskArn); } // Show available commands console.log(chalk_1.default.blue('๐Ÿ”ง Available Commands:')); if (status.status === 'ready' || status.status === 'stopped') { console.log(` Start: ${chalk_1.default.cyan(`fiftyten-db migrate start ${environment}`)}`); } if (status.status === 'running') { console.log(` Stop: ${chalk_1.default.cyan(`fiftyten-db migrate stop ${environment}`)}`); } console.log(` Validate: ${chalk_1.default.cyan(`fiftyten-db migrate validate ${environment}`)}`); console.log(` Cleanup: ${chalk_1.default.cyan(`fiftyten-db migrate cleanup ${environment}`)}`); } catch (error) { console.error(chalk_1.default.red('โŒ Failed to get migration status:'), error instanceof Error ? error.message : String(error)); throw error; } } /** * Show table migration statistics */ async showTableStatistics(taskArn) { try { const response = await this.callWithMfaRetry(async () => { const command = new client_database_migration_service_1.DescribeTableStatisticsCommand({ ReplicationTaskArn: taskArn }); return await this.dmsClient.send(command); }); const stats = response.TableStatistics; if (!stats || stats.length === 0) { console.log(chalk_1.default.gray(' No table statistics available yet')); return; } console.log(chalk_1.default.green('๐Ÿ“‹ Table Statistics:')); console.log(''); // Header console.log(chalk_1.default.gray(' Table Name'.padEnd(30) + 'State'.padEnd(15) + 'Rows'.padEnd(10) + 'Errors')); console.log(chalk_1.default.gray(' ' + '-'.repeat(70))); // Table rows stats.forEach(stat => { const tableName = (stat.TableName || '').padEnd(30); const state = this.getTableStateColor(stat.TableState || '').padEnd(15); const rows = (stat.FullLoadRows?.toString() || '0').padEnd(10); const errors = stat.FullLoadErrorRows?.toString() || '0'; console.log(` ${tableName}${state}${rows}${errors}`); }); console.log(''); // Summary const totalRows = stats.reduce((sum, stat) => sum + (stat.FullLoadRows || 0), 0); const totalErrors = stats.reduce((sum, stat) => sum + (stat.FullLoadErrorRows || 0), 0); console.log(chalk_1.default.green('๐Ÿ“ˆ Summary:')); console.log(` Total Tables: ${chalk_1.default.yellow(stats.length)}`); console.log(` Total Rows: ${chalk_1.default.yellow(totalRows.toLocaleString())}`); console.log(` Total Errors: ${totalErrors > 0 ? chalk_1.default.red(totalErrors) : chalk_1.default.green(totalErrors)}`); console.log(''); } catch (error) { console.log(chalk_1.default.yellow('โš ๏ธ Could not retrieve table statistics')); } } /** * Validate migration data */ async validateMigration(environment) { console.log(chalk_1.default.blue('๐Ÿ” Validating migration data...')); console.log(''); try { const status = await this.getMigrationStatus(environment); if (status.status !== 'running' && status.status !== 'stopped') { console.log(chalk_1.default.yellow('โš ๏ธ Migration task must be running or stopped to validate data')); return; } // Get detailed table statistics const response = await this.callWithMfaRetry(async () => { const command = new client_database_migration_service_1.DescribeTableStatisticsCommand({ ReplicationTaskArn: status.taskArn }); return await this.dmsClient.send(command); }); const stats = response.TableStatistics || []; console.log(chalk_1.default.green('๐Ÿ“Š Migration Validation Report:')); console.log(''); let totalTables = 0; let completedTables = 0; let tablesWithErrors = 0; let totalRows = 0; let totalErrors = 0; stats.forEach(stat => { totalTables++; totalRows += stat.FullLoadRows || 0; totalErrors += stat.FullLoadErrorRows || 0; if (stat.TableState === 'Table completed') { completedTables++; } if ((stat.FullLoadErrorRows || 0) > 0) { tablesWithErrors++; } }); // Overall status const completionRate = totalTables > 0 ? (completedTables / totalTables) * 100 : 0; const errorRate = totalRows > 0 ? (totalErrors / totalRows) * 100 : 0; console.log(chalk_1.default.green('โœ… Overall Status:')); console.log(` Completion Rate: ${completionRate >= 100 ? chalk_1.default.green(completionRate.toFixed(1) + '%') : chalk_1.default.yellow(completionRate.toFixed(1) + '%')}`); console.log(` Error Rate: ${errorRate === 0 ? chalk_1.default.green(errorRate.toFixed(2) + '%') : chalk_1.default.red(errorRate.toFixed(2) + '%')}`); console.log(` Tables Completed: ${chalk_1.default.yellow(completedTables)} / ${chalk_1.default.yellow(totalTables)}`); console.log(` Total Rows Migrated: ${chalk_1.default.yellow(totalRows.toLocaleString())}`); console.log(` Total Errors: ${totalErrors === 0 ? chalk_1.default.green(totalErrors) : chalk_1.default.red(totalErrors)}`); console.log(''); // Tables with errors if (tablesWithErrors > 0) { console.log(chalk_1.default.red('โš ๏ธ Tables with Errors:')); stats.forEach(stat => { if ((stat.FullLoadErrorRows || 0) > 0) { console.log(` ${chalk_1.default.yellow(stat.TableName)}: ${chalk_1.default.red(stat.FullLoadErrorRows)} errors`); } }); console.log(''); } // Recommendations console.log(chalk_1.default.blue('๐Ÿ’ก Recommendations:')); if (completionRate < 100) { console.log(chalk_1.default.yellow(' โ€ข Migration still in progress - wait for completion before cutover')); } if (totalErrors > 0) { console.log(chalk_1.default.yellow(' โ€ข Review error logs in CloudWatch: /aws/dms/task/migration-task-' + environment)); console.log(chalk_1.default.yellow(' โ€ข Consider manual data fixes for errored records')); } if (completionRate === 100 && totalErrors === 0) { console.log(chalk_1.default.green(' โ€ข Migration completed successfully - ready for application cutover')); console.log(chalk_1.default.green(' โ€ข Consider stopping CDC when ready: fiftyten-db migrate stop ' + environment)); } } catch (error) { console.error(chalk_1.default.red('โŒ Failed to validate migration:'), error instanceof Error ? error.message : String(error)); throw error; } } /** * Cleanup migration infrastructure */ async cleanupMigration(environment) { console.log(chalk_1.default.blue('๐Ÿงน Cleaning up migration infrastructure...')); console.log(''); try { // Check if migration is still running const status = await this.getMigrationStatus(environment); if (status.status === 'running') { console.log(chalk_1.default.red('โŒ Cannot cleanup - migration task is still running')); console.log(` Stop the migration first: ${chalk_1.default.cyan(`fiftyten-db migrate stop ${environment}`)}`); return; } // Confirm cleanup const confirm = await promptConfirmation('This will destroy all migration infrastructure. Are you sure?'); if (!confirm) { console.log(chalk_1.default.yellow('Cleanup cancelled.')); return; } const stackName = `indicator-migration-stack-${environment}`; // Step 1: Remove security group rules that were added to external security groups try { console.log(chalk_1.default.blue('๐Ÿ”ง Step 1: Cleaning up security group rules...')); // Get DMS security group ID from stack outputs const stackInfo = await this.callWithMfaRetry(async () => { const command = new client_cloudformation_1.DescribeStacksCommand({ StackName: stackName }); return await this.cfnClient.send(command); }); const stack = stackInfo.Stacks?.[0]; const dmsSecurityGroupId = stack?.Outputs?.find(output => output.OutputKey === 'DMSSecurityGroupId')?.OutputValue; if (dmsSecurityGroupId) { // Get endpoints from stack outputs try { const legacyEndpointOutput = stack?.Outputs?.find(output => output.OutputKey === 'LegacyEndpoint')?.OutputValue; const targetEndpointOutput = stack?.Outputs?.find(output => output.OutputKey === 'TargetEndpoint')?.OutputValue; if (legacyEndpointOutput && targetEndpointOutput) { const discoveryResult = await this.cfnManager.discoverDatabaseSecurityGroups(legacyEndpointOutput, targetEndpointOutput); await this.cfnManager.cleanupSecurityGroupRules(dmsSecurityGroupId, discoveryResult.legacySecurityGroupIds, discoveryResult.targetSecurityGroupIds); } else { console.log(chalk_1.default.yellow('โš ๏ธ Could not find endpoint information in stack outputs - skipping rule cleanup')); console.log(chalk_1.default.gray(` Legacy: ${legacyEndpointOutput}, Target: ${targetEndpointOutput}`)); } } catch (error) { console.log(chalk_1.default.yellow('โš ๏ธ Could not discover database security groups for cleanup')); console.log(chalk_1.default.gray(` ${error instanceof Error ? error.message : String(error)}`)); } } else { console.log(chalk_1.default.yellow('โš ๏ธ Could not find DMS security group ID - skipping rule cleanup')); } } catch (error) { console.log(chalk_1.default.yellow('โš ๏ธ Could not cleanup security group rules - proceeding with stack deletion')); console.log(chalk_1.default.gray(` ${error instanceof Error ? error.message : String(error)}`)); } // Step 2: Delete CloudFormation stack console.log(chalk_1.default.blue('๐Ÿ”ง Step 2: Deleting CloudFormation stack...')); await this.cfnManager.deleteStack(stackName); } catch (error) { if (error instanceof Error && error.message.includes('not found')) { console.log(chalk_1.default.yellow('โš ๏ธ Migration stack not found - may already be cleaned up')); return; } console.error(chalk_1.default.red('โŒ Failed to cleanup migration:'), error instanceof Error ? error.message : String(error)); throw error; } } /** * Get colored status text */ getStatusColor(status) { switch (status.toLowerCase()) { case 'running': return chalk_1.default.green(status); case 'stopped': return chalk_1.default.yellow(status); case 'ready': return chalk_1.default.blue(status); case 'failed': case 'failed-move': return chalk_1.default.red(status); default: return chalk_1.default.gray(status); } } /** * Get colored table state text */ getTableStateColor(state) { switch (state.toLowerCase()) { case 'table completed': return chalk_1.default.green(state); case 'table loading': return chalk_1.default.yellow(state); case 'table error': return chalk_1.default.red(state); default: return chalk_1.default.gray(state); } } } exports.MigrationManager = MigrationManager; //# sourceMappingURL=migration-manager.js.map