Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
328 changes: 328 additions & 0 deletions packages/devtools/frigg-cli/cleanup-command/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,328 @@
const path = require('path');

Check warning on line 1 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `node:path` over `path`.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYH&open=AZoztOvx-zZLkJWAZKYH&pullRequest=480
const fs = require('fs');

Check warning on line 2 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `node:fs` over `fs`.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYI&open=AZoztOvx-zZLkJWAZKYI&pullRequest=480
const { select, confirm, input } = require('@inquirer/prompts');
const { CloudFormationClient, ListStacksCommand } = require('@aws-sdk/client-cloudformation');
const { EC2Client } = require('@aws-sdk/client-ec2');
const { ElasticLoadBalancingV2Client } = require('@aws-sdk/client-elastic-load-balancing-v2');
const { RDSClient } = require('@aws-sdk/client-rds');
const { LambdaClient } = require('@aws-sdk/client-lambda');

const StackIdentifier = require('@friggframework/devtools/infrastructure/domains/health/domain/value-objects/stack-identifier');
const CleanupOrphanedResourcesUseCase = require('@friggframework/devtools/infrastructure/domains/health/application/use-cases/cleanup-orphaned-resources-use-case');

const AWSResourceDetector = require('@friggframework/devtools/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector');
const ResourceDependencyAnalyzer = require('@friggframework/devtools/infrastructure/domains/health/domain/services/resource-dependency-analyzer');
const ResourceDeletionPlanner = require('@friggframework/devtools/infrastructure/domains/health/domain/services/resource-deletion-planner');
const ResourceDeleterRepository = require('@friggframework/devtools/infrastructure/domains/health/infrastructure/adapters/resource-deleter-repository');
const AuditLogRepository = require('@friggframework/devtools/infrastructure/domains/health/infrastructure/adapters/audit-log-repository');

function formatConsoleOutput(result, options = {}) {

Check failure on line 19 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 24 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYJ&open=AZoztOvx-zZLkJWAZKYJ&pullRequest=480
const lines = [];
const { deletionPlan } = result;

if (!deletionPlan) {
lines.push('');
lines.push('─'.repeat(80));

Check warning on line 25 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYK&open=AZoztOvx-zZLkJWAZKYK&pullRequest=480
lines.push(result.message || 'No orphaned resources found');

Check warning on line 26 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYL&open=AZoztOvx-zZLkJWAZKYL&pullRequest=480
lines.push('');

Check warning on line 27 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYM&open=AZoztOvx-zZLkJWAZKYM&pullRequest=480
return lines.join('\n');
}

lines.push('─'.repeat(80));
lines.push('📊 CLEANUP SUMMARY');

Check warning on line 32 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYN&open=AZoztOvx-zZLkJWAZKYN&pullRequest=480
lines.push('');

Check warning on line 33 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYO&open=AZoztOvx-zZLkJWAZKYO&pullRequest=480
lines.push(`Total resources: ${deletionPlan.totalResources}`);

Check warning on line 34 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYP&open=AZoztOvx-zZLkJWAZKYP&pullRequest=480
lines.push(` • Can delete: ${deletionPlan.deletableCount}`);

Check warning on line 35 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYQ&open=AZoztOvx-zZLkJWAZKYQ&pullRequest=480
lines.push(` • Blocked: ${deletionPlan.blockedCount}`);

Check warning on line 36 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYR&open=AZoztOvx-zZLkJWAZKYR&pullRequest=480
lines.push('');

Check warning on line 37 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYS&open=AZoztOvx-zZLkJWAZKYS&pullRequest=480

if (deletionPlan.resourcesByType) {
lines.push('Resources by type:');
Object.entries(deletionPlan.resourcesByType).forEach(([type, count]) => {
const shortType = type.replace('AWS::EC2::', '');
lines.push(` • ${shortType}: ${count}`);
});
lines.push('');
}

if (deletionPlan.costSavings && deletionPlan.costSavings.monthly > 0) {
lines.push(
`Estimated monthly savings: $${deletionPlan.costSavings.monthly.toFixed(2)}`
);
lines.push('');

Check warning on line 52 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYU&open=AZoztOvx-zZLkJWAZKYU&pullRequest=480
}

const totalPhaseResources =
deletionPlan.phases.phase1.length +
deletionPlan.phases.phase2.length +
deletionPlan.phases.phase3.length;

if (totalPhaseResources > 0) {
lines.push('Deletion order:');
if (deletionPlan.phases.phase1.length > 0) {
lines.push(` Phase 1: ${deletionPlan.phases.phase1.length} resources`);
}
if (deletionPlan.phases.phase2.length > 0) {
lines.push(` Phase 2: ${deletionPlan.phases.phase2.length} resources`);
}
if (deletionPlan.phases.phase3.length > 0) {
lines.push(` Phase 3: ${deletionPlan.phases.phase3.length} resources`);
}
lines.push('');
}

if (deletionPlan.blockedResources && deletionPlan.blockedResources.length > 0) {
lines.push('─'.repeat(80));
lines.push('⚠️ BLOCKED RESOURCES (cannot be deleted):');

Check warning on line 76 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYV&open=AZoztOvx-zZLkJWAZKYV&pullRequest=480
lines.push('');

Check warning on line 77 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYW&open=AZoztOvx-zZLkJWAZKYW&pullRequest=480
deletionPlan.blockedResources.forEach((blocked) => {
const { resource, blockingDependencies } = blocked;
lines.push(` ${resource.physicalId} (${resource.resourceType})`);
blockingDependencies.forEach((dep) => {
lines.push(` • ${dep.type}: ${dep.count} resources`);
});
lines.push('');
});
}

if (deletionPlan.warnings && deletionPlan.warnings.length > 0) {
lines.push('─'.repeat(80));
lines.push('⚠️ SAFETY WARNINGS:');

Check warning on line 90 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYZ&open=AZoztOvx-zZLkJWAZKYZ&pullRequest=480
lines.push('');

Check warning on line 91 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYa&open=AZoztOvx-zZLkJWAZKYa&pullRequest=480
deletionPlan.warnings.forEach((warning) => {
lines.push(` • ${warning}`);
});
lines.push('');
}

if (result.dryRun) {
lines.push('─'.repeat(80));
lines.push('');

Check warning on line 100 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYc&open=AZoztOvx-zZLkJWAZKYc&pullRequest=480
lines.push('💡 To delete these resources, run:');

Check warning on line 101 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYd&open=AZoztOvx-zZLkJWAZKYd&pullRequest=480
lines.push(` frigg cleanup ${result.stackName || '<stack-name>'} --execute`);

Check warning on line 102 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYe&open=AZoztOvx-zZLkJWAZKYe&pullRequest=480
lines.push('');

Check warning on line 103 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYf&open=AZoztOvx-zZLkJWAZKYf&pullRequest=480
} else {
lines.push('─'.repeat(80));
lines.push('');

Check warning on line 106 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYg&open=AZoztOvx-zZLkJWAZKYg&pullRequest=480
lines.push('✅ CLEANUP COMPLETE');

Check warning on line 107 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYh&open=AZoztOvx-zZLkJWAZKYh&pullRequest=480
lines.push('');

Check warning on line 108 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYi&open=AZoztOvx-zZLkJWAZKYi&pullRequest=480
lines.push(`Successfully deleted: ${result.deletedCount} resources`);

Check warning on line 109 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYj&open=AZoztOvx-zZLkJWAZKYj&pullRequest=480
if (result.failedCount > 0) {
lines.push(`Failed: ${result.failedCount} resources`);
}
if (result.skippedCount > 0) {
lines.push(`Skipped (blocked): ${result.skippedCount} resources`);
}
lines.push('');

if (result.costSavings && result.costSavings.monthly > 0) {
lines.push(
`Estimated monthly savings: $${result.costSavings.monthly.toFixed(2)}`
);
lines.push('');

Check warning on line 122 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not call `Array#push()` multiple times.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYk&open=AZoztOvx-zZLkJWAZKYk&pullRequest=480
}
}

return lines.join('\n');
}

function formatJsonOutput(result) {
return JSON.stringify(result, null, 2);
}

async function getStackList(region) {
const client = new CloudFormationClient({ region });
const command = new ListStacksCommand({
StackStatusFilter: [
'CREATE_COMPLETE',
'UPDATE_COMPLETE',
'ROLLBACK_COMPLETE',
'UPDATE_ROLLBACK_COMPLETE',
],
});

const response = await client.send(command);
return response.StackSummaries || [];
}

async function selectStackInteractively(region) {
const stacks = await getStackList(region);

if (stacks.length === 0) {
throw new Error(`No CloudFormation stacks found in region ${region}`);
}

const stackName = await select({
message: 'Select a stack to clean up:',
choices: stacks.map((stack) => ({
name: `${stack.StackName} (${stack.StackStatus})`,
value: stack.StackName,
})),
});

return stackName;
}

async function confirmDeletion(deletionPlan, stackName) {
console.log('');
console.log('═'.repeat(80));
console.log('⚠️ WARNING: You are about to DELETE AWS resources');
console.log('═'.repeat(80));
console.log('');
console.log('This action:');
console.log(' • Cannot be easily undone');
console.log(' • Will permanently delete resources from AWS');
console.log(' • May affect running applications if dependencies exist');
console.log('');
console.log(`Resources to delete: ${deletionPlan.deletableCount}`);
if (deletionPlan.resourcesByType) {
Object.entries(deletionPlan.resourcesByType).forEach(([type, count]) => {
const shortType = type.replace('AWS::EC2::', '');
console.log(` • ${shortType}: ${count}`);
});
}
console.log('');

const confirmationText = await input({
message: `Type 'delete ${stackName}' to confirm:`,
});

return confirmationText === `delete ${stackName}`;
}

async function runCleanupCommand(stackName, options) {

Check failure on line 193 in packages/devtools/frigg-cli/cleanup-command/index.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 26 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=friggframework_frigg&issues=AZoztOvx-zZLkJWAZKYm&open=AZoztOvx-zZLkJWAZKYm&pullRequest=480
const region = options.region || process.env.AWS_REGION || 'us-east-1';
const dryRun = !options.execute;
const autoConfirm = options.yes || false;
const outputFormat = options.output || options.format || 'console';
const resourceTypeFilter = options['resourceType'] || null;
const logicalIdPattern = options['logicalId'] || null;

if (outputFormat === 'console') {
console.log('');
console.log('═'.repeat(80));
console.log(' 🧹 FRIGG CLEANUP - Orphaned Resources');
console.log('═'.repeat(80));
console.log('');
}

let resolvedStackName = stackName;
if (!resolvedStackName) {
resolvedStackName = await selectStackInteractively(region);
}

if (outputFormat === 'console') {
console.log('');
console.log(`Stack: ${resolvedStackName}`);
console.log(`Region: ${region}`);
console.log(`Mode: ${dryRun ? 'DRY-RUN (no resources will be deleted)' : 'EXECUTE (resources WILL be deleted)'}`);
console.log('');
}

const stackIdentifier = new StackIdentifier({
stackName: resolvedStackName,
region,
accountId: '000000000000',
});

const ec2Client = new EC2Client({ region });
const elbClient = new ElasticLoadBalancingV2Client({ region });
const rdsClient = new RDSClient({ region });
const lambdaClient = new LambdaClient({ region });

const useCase = new CleanupOrphanedResourcesUseCase({
resourceDetector: new AWSResourceDetector({ region }),
dependencyAnalyzer: new ResourceDependencyAnalyzer({
ec2Client,
elbClient,
rdsClient,
lambdaClient,
}),
deletionPlanner: new ResourceDeletionPlanner(),
deleterRepository: new ResourceDeleterRepository({ ec2Client, region }),
auditRepository: new AuditLogRepository(),
});

if (!dryRun && outputFormat === 'console') {
console.log('Analyzing orphaned resources...');
console.log('');

const dryRunResult = await useCase.execute({
stackIdentifier,
dryRun: true,
resourceTypeFilter,
logicalIdPattern,
});

if (
!dryRunResult.deletionPlan ||
dryRunResult.deletionPlan.deletableCount === 0
) {
console.log(formatConsoleOutput(dryRunResult));
return;
}

console.log(formatConsoleOutput(dryRunResult));

if (!autoConfirm) {
const confirmed = await confirmDeletion(
dryRunResult.deletionPlan,
resolvedStackName
);
if (!confirmed) {
console.log('');
console.log('Cleanup cancelled.');
console.log('');
return;
}
}

console.log('');
console.log('Starting deletion...');
}

const progressHandler =
outputFormat === 'console'
? (progress) => {
if (progress.phase) {
console.log(`\n${progress.message}`);
} else if (progress.current) {
const status = progress.success ? '✓' : '✗';
console.log(
` [${progress.current}/${progress.total}] ${status} ${progress.physicalId}`
);
}
}
: null;

if (dryRun && outputFormat === 'console') {
console.log('Analyzing orphaned resources...');
console.log('');
}

const result = await useCase.execute({
stackIdentifier,
dryRun,
resourceTypeFilter,
logicalIdPattern,
onProgress: progressHandler,
});

if (outputFormat === 'json') {
console.log(formatJsonOutput(result));
} else {
console.log(formatConsoleOutput(result));
}

if (options.outputFile) {
const outputPath = path.resolve(options.outputFile);
const content =
outputFormat === 'json'
? formatJsonOutput(result)
: formatConsoleOutput(result);
fs.writeFileSync(outputPath, content, 'utf8');
console.log(`Report written to: ${outputPath}`);
}
}

module.exports = runCleanupCommand;
16 changes: 15 additions & 1 deletion packages/devtools/frigg-cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const { uiCommand } = require('./ui-command');
const { dbSetupCommand } = require('./db-setup-command');
const { doctorCommand } = require('./doctor-command');
const { repairCommand } = require('./repair-command');
const cleanupCommand = require('./cleanup-command');

const program = new Command();

Expand Down Expand Up @@ -168,6 +169,19 @@ program
.option('-v, --verbose', 'enable verbose output')
.action(repairCommand);

program
.command('cleanup [stackName]')
.description('Clean up duplicate orphaned resources not in current stack template')
.option('-r, --region <region>', 'AWS region (defaults to AWS_REGION env var or us-east-1)')
.option('--execute', 'execute deletion (default is dry-run)')
.option('--resource-type <type>', 'filter by resource type (e.g., AWS::EC2::VPC)')
.option('--logical-id <pattern>', 'filter by logical ID pattern (supports * wildcard)')
.option('-y, --yes', 'skip confirmation prompts')
.option('-f, --format <format>', 'output format (console or json)', 'console')
.option('--output-file <path>', 'save report to file')
.option('-v, --verbose', 'enable verbose output')
.action(cleanupCommand);

program.parse(process.argv);

module.exports = { initCommand, installCommand, startCommand, buildCommand, deployCommand, generateIamCommand, uiCommand, dbSetupCommand, doctorCommand, repairCommand };
module.exports = { initCommand, installCommand, startCommand, buildCommand, deployCommand, generateIamCommand, uiCommand, dbSetupCommand, doctorCommand, repairCommand, cleanupCommand };
Loading
Loading