@friggframework/devtools 2.0.0-next.45 → 2.0.0-next.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/infrastructure/ARCHITECTURE.md +487 -0
- package/infrastructure/HEALTH.md +468 -0
- package/infrastructure/README.md +51 -0
- package/infrastructure/__tests__/postgres-config.test.js +914 -0
- package/infrastructure/__tests__/template-generation.test.js +687 -0
- package/infrastructure/create-frigg-infrastructure.js +1 -1
- package/infrastructure/docs/POSTGRES-CONFIGURATION.md +630 -0
- package/infrastructure/{DEPLOYMENT-INSTRUCTIONS.md → docs/deployment-instructions.md} +3 -3
- package/infrastructure/{IAM-POLICY-TEMPLATES.md → docs/iam-policy-templates.md} +9 -10
- package/infrastructure/domains/database/aurora-builder.js +809 -0
- package/infrastructure/domains/database/aurora-builder.test.js +950 -0
- package/infrastructure/domains/database/aurora-discovery.js +87 -0
- package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
- package/infrastructure/domains/database/aurora-resolver.js +210 -0
- package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
- package/infrastructure/domains/database/migration-builder.js +633 -0
- package/infrastructure/domains/database/migration-builder.test.js +294 -0
- package/infrastructure/domains/database/migration-resolver.js +163 -0
- package/infrastructure/domains/database/migration-resolver.test.js +337 -0
- package/infrastructure/domains/health/application/ports/IPropertyReconciler.js +164 -0
- package/infrastructure/domains/health/application/ports/IResourceDetector.js +129 -0
- package/infrastructure/domains/health/application/ports/IResourceImporter.js +142 -0
- package/infrastructure/domains/health/application/ports/IStackRepository.js +131 -0
- package/infrastructure/domains/health/application/ports/index.js +26 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
- package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +221 -0
- package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +152 -0
- package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +535 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +213 -0
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
- package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
- package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
- package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
- package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
- package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
- package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
- package/infrastructure/domains/health/domain/entities/issue.js +299 -0
- package/infrastructure/domains/health/domain/entities/issue.test.js +528 -0
- package/infrastructure/domains/health/domain/entities/property-mismatch.js +108 -0
- package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +275 -0
- package/infrastructure/domains/health/domain/entities/resource.js +159 -0
- package/infrastructure/domains/health/domain/entities/resource.test.js +432 -0
- package/infrastructure/domains/health/domain/entities/stack-health-report.js +306 -0
- package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +601 -0
- package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
- package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +971 -0
- package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +1150 -0
- package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
- package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
- package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +419 -0
- package/infrastructure/domains/health/domain/services/health-score-calculator.js +248 -0
- package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +504 -0
- package/infrastructure/domains/health/domain/services/import-progress-monitor.js +195 -0
- package/infrastructure/domains/health/domain/services/import-template-generator.js +435 -0
- package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
- package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +234 -0
- package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +431 -0
- package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
- package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
- package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -0
- package/infrastructure/domains/health/domain/value-objects/health-score.js +138 -0
- package/infrastructure/domains/health/domain/value-objects/health-score.test.js +267 -0
- package/infrastructure/domains/health/domain/value-objects/property-mutability.js +161 -0
- package/infrastructure/domains/health/domain/value-objects/property-mutability.test.js +198 -0
- package/infrastructure/domains/health/domain/value-objects/resource-state.js +167 -0
- package/infrastructure/domains/health/domain/value-objects/resource-state.test.js +196 -0
- package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +192 -0
- package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +262 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +784 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +1133 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +565 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +554 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +318 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +398 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +777 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +580 -0
- package/infrastructure/domains/integration/integration-builder.js +397 -0
- package/infrastructure/domains/integration/integration-builder.test.js +593 -0
- package/infrastructure/domains/integration/integration-resolver.js +170 -0
- package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
- package/infrastructure/domains/integration/websocket-builder.js +69 -0
- package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
- package/infrastructure/domains/networking/vpc-builder.js +1829 -0
- package/infrastructure/domains/networking/vpc-builder.test.js +1262 -0
- package/infrastructure/domains/networking/vpc-discovery.js +177 -0
- package/infrastructure/domains/networking/vpc-discovery.test.js +350 -0
- package/infrastructure/domains/networking/vpc-resolver.js +324 -0
- package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
- package/infrastructure/domains/parameters/ssm-builder.js +79 -0
- package/infrastructure/domains/parameters/ssm-builder.test.js +189 -0
- package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
- package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
- package/infrastructure/{iam-generator.js → domains/security/iam-generator.js} +2 -2
- package/infrastructure/domains/security/kms-builder.js +366 -0
- package/infrastructure/domains/security/kms-builder.test.js +374 -0
- package/infrastructure/domains/security/kms-discovery.js +80 -0
- package/infrastructure/domains/security/kms-discovery.test.js +177 -0
- package/infrastructure/domains/security/kms-resolver.js +96 -0
- package/infrastructure/domains/security/kms-resolver.test.js +216 -0
- package/infrastructure/domains/shared/base-builder.js +112 -0
- package/infrastructure/domains/shared/base-resolver.js +186 -0
- package/infrastructure/domains/shared/base-resolver.test.js +305 -0
- package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
- package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
- package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
- package/infrastructure/domains/shared/cloudformation-discovery.js +375 -0
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +590 -0
- package/infrastructure/domains/shared/environment-builder.js +119 -0
- package/infrastructure/domains/shared/environment-builder.test.js +247 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.js +544 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +377 -0
- package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
- package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
- package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
- package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
- package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
- package/infrastructure/domains/shared/resource-discovery.js +192 -0
- package/infrastructure/domains/shared/resource-discovery.test.js +552 -0
- package/infrastructure/domains/shared/types/app-definition.js +205 -0
- package/infrastructure/domains/shared/types/discovery-result.js +106 -0
- package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
- package/infrastructure/domains/shared/types/index.js +46 -0
- package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
- package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +380 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +248 -0
- package/infrastructure/domains/shared/utilities/handler-path-resolver.js +134 -0
- package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +268 -0
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +55 -0
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +138 -0
- package/infrastructure/{env-validator.js → domains/shared/validation/env-validator.js} +2 -1
- package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
- package/infrastructure/esbuild.config.js +53 -0
- package/infrastructure/infrastructure-composer.js +87 -0
- package/infrastructure/{serverless-template.test.js → infrastructure-composer.test.js} +115 -24
- package/infrastructure/scripts/build-prisma-layer.js +553 -0
- package/infrastructure/scripts/build-prisma-layer.test.js +102 -0
- package/infrastructure/{build-time-discovery.js → scripts/build-time-discovery.js} +80 -48
- package/infrastructure/{build-time-discovery.test.js → scripts/build-time-discovery.test.js} +5 -4
- package/layers/prisma/nodejs/package.json +8 -0
- package/management-ui/server/utils/cliIntegration.js +1 -1
- package/management-ui/server/utils/environment/awsParameterStore.js +29 -18
- package/package.json +11 -11
- package/frigg-cli/.eslintrc.js +0 -141
- package/frigg-cli/__tests__/unit/commands/build.test.js +0 -251
- package/frigg-cli/__tests__/unit/commands/db-setup.test.js +0 -548
- package/frigg-cli/__tests__/unit/commands/install.test.js +0 -400
- package/frigg-cli/__tests__/unit/commands/ui.test.js +0 -346
- package/frigg-cli/__tests__/unit/utils/database-validator.test.js +0 -366
- package/frigg-cli/__tests__/unit/utils/error-messages.test.js +0 -304
- package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +0 -486
- package/frigg-cli/__tests__/utils/mock-factory.js +0 -270
- package/frigg-cli/__tests__/utils/prisma-mock.js +0 -194
- package/frigg-cli/__tests__/utils/test-fixtures.js +0 -463
- package/frigg-cli/__tests__/utils/test-setup.js +0 -287
- package/frigg-cli/build-command/index.js +0 -65
- package/frigg-cli/db-setup-command/index.js +0 -193
- package/frigg-cli/deploy-command/index.js +0 -175
- package/frigg-cli/generate-command/__tests__/generate-command.test.js +0 -301
- package/frigg-cli/generate-command/azure-generator.js +0 -43
- package/frigg-cli/generate-command/gcp-generator.js +0 -47
- package/frigg-cli/generate-command/index.js +0 -332
- package/frigg-cli/generate-command/terraform-generator.js +0 -555
- package/frigg-cli/generate-iam-command.js +0 -118
- package/frigg-cli/index.js +0 -75
- package/frigg-cli/index.test.js +0 -158
- package/frigg-cli/init-command/backend-first-handler.js +0 -756
- package/frigg-cli/init-command/index.js +0 -93
- package/frigg-cli/init-command/template-handler.js +0 -143
- package/frigg-cli/install-command/backend-js.js +0 -33
- package/frigg-cli/install-command/commit-changes.js +0 -16
- package/frigg-cli/install-command/environment-variables.js +0 -127
- package/frigg-cli/install-command/environment-variables.test.js +0 -136
- package/frigg-cli/install-command/index.js +0 -54
- package/frigg-cli/install-command/install-package.js +0 -13
- package/frigg-cli/install-command/integration-file.js +0 -30
- package/frigg-cli/install-command/logger.js +0 -12
- package/frigg-cli/install-command/template.js +0 -90
- package/frigg-cli/install-command/validate-package.js +0 -75
- package/frigg-cli/jest.config.js +0 -124
- package/frigg-cli/package.json +0 -54
- package/frigg-cli/start-command/index.js +0 -149
- package/frigg-cli/start-command/start-command.test.js +0 -297
- package/frigg-cli/test/init-command.test.js +0 -180
- package/frigg-cli/test/npm-registry.test.js +0 -319
- package/frigg-cli/ui-command/index.js +0 -154
- package/frigg-cli/utils/app-resolver.js +0 -319
- package/frigg-cli/utils/backend-path.js +0 -25
- package/frigg-cli/utils/database-validator.js +0 -161
- package/frigg-cli/utils/error-messages.js +0 -257
- package/frigg-cli/utils/npm-registry.js +0 -167
- package/frigg-cli/utils/prisma-runner.js +0 -280
- package/frigg-cli/utils/process-manager.js +0 -199
- package/frigg-cli/utils/repo-detection.js +0 -405
- package/infrastructure/aws-discovery.js +0 -1176
- package/infrastructure/aws-discovery.test.js +0 -1220
- package/infrastructure/serverless-template.js +0 -2094
- /package/infrastructure/{WEBSOCKET-CONFIGURATION.md → docs/WEBSOCKET-CONFIGURATION.md} +0 -0
- /package/infrastructure/{GENERATE-IAM-DOCS.md → docs/generate-iam-command.md} +0 -0
- /package/infrastructure/{iam-generator.test.js → domains/security/iam-generator.test.js} +0 -0
- /package/infrastructure/{frigg-deployment-iam-stack.yaml → domains/security/templates/frigg-deployment-iam-stack.yaml} +0 -0
- /package/infrastructure/{iam-policy-basic.json → domains/security/templates/iam-policy-basic.json} +0 -0
- /package/infrastructure/{iam-policy-full.json → domains/security/templates/iam-policy-full.json} +0 -0
- /package/infrastructure/{run-discovery.js → scripts/run-discovery.js} +0 -0
|
@@ -0,0 +1,1419 @@
|
|
|
1
|
+
# Specification: Cleanup Command for Orphaned Resources
|
|
2
|
+
|
|
3
|
+
**Version**: 1.0.0
|
|
4
|
+
**Status**: Draft
|
|
5
|
+
**Created**: 2025-10-27
|
|
6
|
+
**Author**: Claude Code (following user requirements)
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Create a new `frigg cleanup --orphaned` command to safely delete duplicate orphaned resources that are not part of the current CloudFormation stack template. This command helps users clean up leftover resources from previous deployments or failed stacks.
|
|
11
|
+
|
|
12
|
+
## Business Context
|
|
13
|
+
|
|
14
|
+
### Problem Statement
|
|
15
|
+
|
|
16
|
+
After implementing logical ID mapping and deduplication for `frigg repair --import`, we discovered that many stacks have duplicate orphaned resources:
|
|
17
|
+
|
|
18
|
+
- Resources with CloudFormation tags (indicating they were once managed)
|
|
19
|
+
- NOT referenced in the current build template (indicating they're no longer needed)
|
|
20
|
+
- Example: 3 VPCs all tagged `FriggVPC`, but only 1 is in the current template
|
|
21
|
+
|
|
22
|
+
These duplicate resources:
|
|
23
|
+
- Cost money (especially VPCs, NAT Gateways, Elastic IPs)
|
|
24
|
+
- Clutter AWS accounts
|
|
25
|
+
- Cause confusion during troubleshooting
|
|
26
|
+
- Are potential security risks (orphaned security groups with open rules)
|
|
27
|
+
|
|
28
|
+
### User Story
|
|
29
|
+
|
|
30
|
+
> As a DevOps engineer managing Frigg applications
|
|
31
|
+
> I want to safely delete duplicate orphaned resources
|
|
32
|
+
> So that I can reduce costs, improve account hygiene, and eliminate security risks
|
|
33
|
+
> While avoiding accidental deletion of resources that are still in use
|
|
34
|
+
|
|
35
|
+
### Real-World Example
|
|
36
|
+
|
|
37
|
+
From `quo-integrations-dev` stack:
|
|
38
|
+
- **16 orphaned resources detected**
|
|
39
|
+
- **5 will be imported** (in build template)
|
|
40
|
+
- **11 are duplicates** that should be cleaned up:
|
|
41
|
+
- 2 extra VPCs ($36/month each for NAT Gateway)
|
|
42
|
+
- 7 extra Subnets ($0 but clutter)
|
|
43
|
+
- 2 extra SecurityGroups (potential security risk)
|
|
44
|
+
|
|
45
|
+
**Estimated monthly savings**: ~$72 for just the NAT Gateways
|
|
46
|
+
|
|
47
|
+
## Requirements
|
|
48
|
+
|
|
49
|
+
### Functional Requirements
|
|
50
|
+
|
|
51
|
+
#### FR-1: Command Interface
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Dry-run mode (default) - show what would be deleted
|
|
55
|
+
frigg cleanup --orphaned
|
|
56
|
+
frigg cleanup --orphaned --dry-run
|
|
57
|
+
|
|
58
|
+
# Execute deletion
|
|
59
|
+
frigg cleanup --orphaned --execute
|
|
60
|
+
|
|
61
|
+
# Target specific stack
|
|
62
|
+
frigg cleanup --orphaned --stack quo-integrations-dev --execute
|
|
63
|
+
|
|
64
|
+
# Auto-confirm (skip confirmation prompts)
|
|
65
|
+
frigg cleanup --orphaned --execute --yes
|
|
66
|
+
|
|
67
|
+
# Clean up specific resource types only
|
|
68
|
+
frigg cleanup --orphaned --resource-type AWS::EC2::VPC --execute
|
|
69
|
+
frigg cleanup --orphaned --resource-type AWS::EC2::Subnet --execute
|
|
70
|
+
|
|
71
|
+
# Filter by logical ID pattern
|
|
72
|
+
frigg cleanup --orphaned --logical-id "Frigg*" --execute
|
|
73
|
+
|
|
74
|
+
# JSON output for scripting
|
|
75
|
+
frigg cleanup --orphaned --output json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### FR-2: Safety Features
|
|
79
|
+
|
|
80
|
+
**CRITICAL SAFETY REQUIREMENTS**:
|
|
81
|
+
|
|
82
|
+
1. **Dry-run by default**: Unless `--execute` is specified, only show what would be deleted
|
|
83
|
+
2. **Dependency checking**: Detect and warn about dependencies before deletion
|
|
84
|
+
3. **Confirmation prompts**: Require explicit user confirmation before deletion
|
|
85
|
+
4. **Deletion order**: Delete resources in correct order (subnets before VPCs, etc.)
|
|
86
|
+
5. **Rollback protection**: Create snapshots/backups where possible
|
|
87
|
+
6. **Audit logging**: Log all deletion attempts and results
|
|
88
|
+
|
|
89
|
+
**Dependency Detection Examples**:
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
// VPC dependency check
|
|
93
|
+
if (resourceType === 'AWS::EC2::VPC') {
|
|
94
|
+
const dependencies = [
|
|
95
|
+
await checkForSubnets(vpcId),
|
|
96
|
+
await checkForSecurityGroups(vpcId),
|
|
97
|
+
await checkForNATGateways(vpcId),
|
|
98
|
+
await checkForInternetGateways(vpcId),
|
|
99
|
+
await checkForVPCEndpoints(vpcId),
|
|
100
|
+
await checkForRouteTables(vpcId),
|
|
101
|
+
await checkForNetworkACLs(vpcId),
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
if (dependencies.some(d => d.hasResources)) {
|
|
105
|
+
throw new Error('Cannot delete VPC: has dependent resources');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Security Group dependency check
|
|
110
|
+
if (resourceType === 'AWS::EC2::SecurityGroup') {
|
|
111
|
+
const usage = [
|
|
112
|
+
await checkForEC2Instances(sgId),
|
|
113
|
+
await checkForRDSInstances(sgId),
|
|
114
|
+
await checkForLambdaFunctions(sgId),
|
|
115
|
+
await checkForLoadBalancers(sgId),
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
if (usage.some(u => u.hasResources)) {
|
|
119
|
+
throw new Error('Cannot delete SecurityGroup: in use by other resources');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
#### FR-3: Deletion Order
|
|
125
|
+
|
|
126
|
+
Resources must be deleted in dependency order:
|
|
127
|
+
|
|
128
|
+
**Phase 1 - Detach Dependencies**:
|
|
129
|
+
1. VPC Endpoints
|
|
130
|
+
2. NAT Gateway attachments
|
|
131
|
+
3. Internet Gateway attachments
|
|
132
|
+
4. Route table associations
|
|
133
|
+
|
|
134
|
+
**Phase 2 - Delete Dependent Resources**:
|
|
135
|
+
1. NAT Gateways
|
|
136
|
+
2. Internet Gateways
|
|
137
|
+
3. Route Tables (non-default)
|
|
138
|
+
4. Network ACLs (non-default)
|
|
139
|
+
5. Subnets
|
|
140
|
+
6. Security Groups (non-default)
|
|
141
|
+
|
|
142
|
+
**Phase 3 - Delete Core Resources**:
|
|
143
|
+
1. VPCs
|
|
144
|
+
|
|
145
|
+
**Implementation**:
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
const DELETION_ORDER = [
|
|
149
|
+
{ type: 'AWS::EC2::VPCEndpoint', phase: 1 },
|
|
150
|
+
{ type: 'AWS::EC2::NatGateway', phase: 2 },
|
|
151
|
+
{ type: 'AWS::EC2::InternetGateway', phase: 2 },
|
|
152
|
+
{ type: 'AWS::EC2::RouteTable', phase: 2 },
|
|
153
|
+
{ type: 'AWS::EC2::NetworkAcl', phase: 2 },
|
|
154
|
+
{ type: 'AWS::EC2::Subnet', phase: 2 },
|
|
155
|
+
{ type: 'AWS::EC2::SecurityGroup', phase: 2 },
|
|
156
|
+
{ type: 'AWS::EC2::VPC', phase: 3 },
|
|
157
|
+
];
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### FR-4: Terminal Output
|
|
161
|
+
|
|
162
|
+
**Dry-Run Mode** (default):
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
$ frigg cleanup --orphaned
|
|
166
|
+
|
|
167
|
+
🧹 Frigg Cleanup - Orphaned Resources
|
|
168
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
169
|
+
|
|
170
|
+
Stack: quo-integrations-dev
|
|
171
|
+
Region: us-east-1
|
|
172
|
+
Mode: DRY-RUN (no resources will be deleted)
|
|
173
|
+
|
|
174
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
175
|
+
|
|
176
|
+
📋 Analyzing duplicate orphaned resources...
|
|
177
|
+
|
|
178
|
+
Found 11 duplicate resources not in build template:
|
|
179
|
+
|
|
180
|
+
VPCs (2):
|
|
181
|
+
⚠️ vpc-0e2351eac99adcb83 (FriggVPC)
|
|
182
|
+
• Tags: quo-integrations-dev, Stage: dev
|
|
183
|
+
• Cost: ~$36/month (NAT Gateway)
|
|
184
|
+
• Dependencies: 3 subnets, 1 security group
|
|
185
|
+
• Status: Can be deleted (after dependencies)
|
|
186
|
+
|
|
187
|
+
⚠️ vpc-020a0365610c05f0b (FriggVPC)
|
|
188
|
+
• Tags: quo-integrations-dev, Stage: dev
|
|
189
|
+
• Cost: ~$36/month (NAT Gateway)
|
|
190
|
+
• Dependencies: 2 subnets, 1 security group
|
|
191
|
+
• Status: Can be deleted (after dependencies)
|
|
192
|
+
|
|
193
|
+
Subnets (7):
|
|
194
|
+
✓ subnet-0123456789abcdef0 (FriggPrivateSubnet1)
|
|
195
|
+
• Parent VPC: vpc-0e2351eac99adcb83
|
|
196
|
+
• Status: Can be deleted
|
|
197
|
+
|
|
198
|
+
✓ subnet-0123456789abcdef1 (FriggPrivateSubnet2)
|
|
199
|
+
• Parent VPC: vpc-0e2351eac99adcb83
|
|
200
|
+
• Status: Can be deleted
|
|
201
|
+
|
|
202
|
+
... (5 more subnets)
|
|
203
|
+
|
|
204
|
+
SecurityGroups (2):
|
|
205
|
+
⚠️ sg-0123456789abcdef0 (FriggLambdaSecurityGroup)
|
|
206
|
+
• VPC: vpc-0e2351eac99adcb83
|
|
207
|
+
• Rules: 2 ingress, 1 egress
|
|
208
|
+
• In use by: 0 resources
|
|
209
|
+
• Status: Can be deleted
|
|
210
|
+
|
|
211
|
+
⚠️ sg-0123456789abcdef1 (FriggLambdaSecurityGroup)
|
|
212
|
+
• VPC: vpc-020a0365610c05f0b
|
|
213
|
+
• Rules: 2 ingress, 1 egress
|
|
214
|
+
• In use by: 0 resources
|
|
215
|
+
• Status: Can be deleted
|
|
216
|
+
|
|
217
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
218
|
+
|
|
219
|
+
📊 CLEANUP SUMMARY
|
|
220
|
+
|
|
221
|
+
Total resources: 11
|
|
222
|
+
• VPCs: 2 (can delete)
|
|
223
|
+
• Subnets: 7 (can delete)
|
|
224
|
+
• SecurityGroups: 2 (can delete)
|
|
225
|
+
|
|
226
|
+
Estimated monthly savings: $72
|
|
227
|
+
|
|
228
|
+
Deletion order:
|
|
229
|
+
Phase 1: Remove attachments (0 resources)
|
|
230
|
+
Phase 2: Delete dependent resources (9 resources)
|
|
231
|
+
Phase 3: Delete core resources (2 VPCs)
|
|
232
|
+
|
|
233
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
234
|
+
|
|
235
|
+
⚠️ SAFETY WARNINGS
|
|
236
|
+
|
|
237
|
+
• This operation cannot be easily undone
|
|
238
|
+
• Resources will be permanently deleted from AWS
|
|
239
|
+
• Verify no applications depend on these resources
|
|
240
|
+
• Consider taking backups if needed
|
|
241
|
+
|
|
242
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
243
|
+
|
|
244
|
+
To delete these resources, run:
|
|
245
|
+
frigg cleanup --orphaned --execute
|
|
246
|
+
|
|
247
|
+
To review individual resources:
|
|
248
|
+
frigg cleanup --orphaned --output json | jq .
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Execute Mode** (with confirmation):
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
$ frigg cleanup --orphaned --execute
|
|
255
|
+
|
|
256
|
+
🧹 Frigg Cleanup - Orphaned Resources
|
|
257
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
258
|
+
|
|
259
|
+
Stack: quo-integrations-dev
|
|
260
|
+
Region: us-east-1
|
|
261
|
+
Mode: EXECUTE (resources WILL be deleted)
|
|
262
|
+
|
|
263
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
264
|
+
|
|
265
|
+
⚠️ WARNING: You are about to DELETE 11 AWS resources
|
|
266
|
+
|
|
267
|
+
This action:
|
|
268
|
+
• Cannot be easily undone
|
|
269
|
+
• Will permanently delete resources from AWS
|
|
270
|
+
• May affect running applications if dependencies exist
|
|
271
|
+
• Will save approximately $72/month
|
|
272
|
+
|
|
273
|
+
Resources to delete:
|
|
274
|
+
• 2 VPCs
|
|
275
|
+
• 7 Subnets
|
|
276
|
+
• 2 SecurityGroups
|
|
277
|
+
|
|
278
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
279
|
+
|
|
280
|
+
Type 'delete quo-integrations-dev' to confirm: delete quo-integrations-dev
|
|
281
|
+
|
|
282
|
+
✓ Confirmation received. Starting deletion...
|
|
283
|
+
|
|
284
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
285
|
+
|
|
286
|
+
Phase 1: Detaching dependencies...
|
|
287
|
+
(no resources in this phase)
|
|
288
|
+
|
|
289
|
+
Phase 2: Deleting dependent resources...
|
|
290
|
+
[1/9] Deleting subnet-0123456789abcdef0... ✓
|
|
291
|
+
[2/9] Deleting subnet-0123456789abcdef1... ✓
|
|
292
|
+
[3/9] Deleting subnet-0123456789abcdef2... ✓
|
|
293
|
+
[4/9] Deleting subnet-0123456789abcdef3... ✓
|
|
294
|
+
[5/9] Deleting subnet-0123456789abcdef4... ✓
|
|
295
|
+
[6/9] Deleting subnet-0123456789abcdef5... ✓
|
|
296
|
+
[7/9] Deleting subnet-0123456789abcdef6... ✓
|
|
297
|
+
[8/9] Deleting sg-0123456789abcdef0... ✓
|
|
298
|
+
[9/9] Deleting sg-0123456789abcdef1... ✓
|
|
299
|
+
|
|
300
|
+
Phase 3: Deleting core resources...
|
|
301
|
+
[1/2] Deleting vpc-0e2351eac99adcb83... ✓ (20s)
|
|
302
|
+
[2/2] Deleting vpc-020a0365610c05f0b... ✓ (18s)
|
|
303
|
+
|
|
304
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
305
|
+
|
|
306
|
+
✅ CLEANUP COMPLETE
|
|
307
|
+
|
|
308
|
+
Successfully deleted: 11 resources
|
|
309
|
+
• VPCs: 2
|
|
310
|
+
• Subnets: 7
|
|
311
|
+
• SecurityGroups: 2
|
|
312
|
+
|
|
313
|
+
Failed: 0 resources
|
|
314
|
+
|
|
315
|
+
Estimated monthly savings: $72
|
|
316
|
+
|
|
317
|
+
Total time: 45s
|
|
318
|
+
|
|
319
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
320
|
+
|
|
321
|
+
💡 Next steps:
|
|
322
|
+
1. Run 'frigg doctor' to verify health score improved
|
|
323
|
+
2. Run 'frigg repair --import' to import remaining resources
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Execute Mode** (with --yes flag):
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
$ frigg cleanup --orphaned --execute --yes
|
|
330
|
+
|
|
331
|
+
🧹 Frigg Cleanup - Orphaned Resources
|
|
332
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
333
|
+
|
|
334
|
+
⚠️ Auto-confirm enabled (--yes flag)
|
|
335
|
+
|
|
336
|
+
Deleting 11 resources without confirmation...
|
|
337
|
+
|
|
338
|
+
Phase 2: Deleting dependent resources...
|
|
339
|
+
[1/9] Deleting subnet-0123456789abcdef0... ✓
|
|
340
|
+
... (progress shown)
|
|
341
|
+
|
|
342
|
+
✅ CLEANUP COMPLETE
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
#### FR-5: Error Handling
|
|
346
|
+
|
|
347
|
+
**Dependency Errors**:
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
$ frigg cleanup --orphaned --execute
|
|
351
|
+
|
|
352
|
+
Phase 3: Deleting core resources...
|
|
353
|
+
[1/2] Deleting vpc-0e2351eac99adcb83... ✗ FAILED
|
|
354
|
+
|
|
355
|
+
❌ Error: Cannot delete VPC vpc-0e2351eac99adcb83
|
|
356
|
+
Reason: DependencyViolation - The vpc has dependencies and cannot be deleted
|
|
357
|
+
|
|
358
|
+
Remaining dependencies:
|
|
359
|
+
• Internet Gateway: igw-0123456789abcdef0 (still attached)
|
|
360
|
+
• NAT Gateway: nat-0123456789abcdef0 (still exists)
|
|
361
|
+
|
|
362
|
+
💡 Run cleanup again to retry after dependency cleanup
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**Permission Errors**:
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
❌ Error: Access Denied
|
|
369
|
+
Reason: User lacks permission to delete VPCs
|
|
370
|
+
|
|
371
|
+
Required IAM permissions:
|
|
372
|
+
• ec2:DeleteVpc
|
|
373
|
+
• ec2:DeleteSubnet
|
|
374
|
+
• ec2:DeleteSecurityGroup
|
|
375
|
+
|
|
376
|
+
💡 Update your IAM policy and try again
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
**Resource In Use**:
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
⚠️ Cannot delete sg-0123456789abcdef0
|
|
383
|
+
|
|
384
|
+
Reason: Security group is in use by:
|
|
385
|
+
• Lambda function: quo-integrations-dev-handler (12345678)
|
|
386
|
+
• RDS instance: quo-integrations-db (i-12345678)
|
|
387
|
+
|
|
388
|
+
❌ This resource will NOT be deleted
|
|
389
|
+
|
|
390
|
+
💡 Remove references from these resources first
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
#### FR-6: JSON Output
|
|
394
|
+
|
|
395
|
+
For scripting and automation:
|
|
396
|
+
|
|
397
|
+
```json
|
|
398
|
+
{
|
|
399
|
+
"stack": {
|
|
400
|
+
"name": "quo-integrations-dev",
|
|
401
|
+
"region": "us-east-1"
|
|
402
|
+
},
|
|
403
|
+
"mode": "dry-run",
|
|
404
|
+
"resources": {
|
|
405
|
+
"total": 11,
|
|
406
|
+
"canDelete": 11,
|
|
407
|
+
"blocked": 0,
|
|
408
|
+
"byType": {
|
|
409
|
+
"AWS::EC2::VPC": 2,
|
|
410
|
+
"AWS::EC2::Subnet": 7,
|
|
411
|
+
"AWS::EC2::SecurityGroup": 2
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
"duplicateResources": [
|
|
415
|
+
{
|
|
416
|
+
"physicalId": "vpc-0e2351eac99adcb83",
|
|
417
|
+
"resourceType": "AWS::EC2::VPC",
|
|
418
|
+
"logicalId": "FriggVPC",
|
|
419
|
+
"tags": {
|
|
420
|
+
"aws:cloudformation:stack-name": "quo-integrations-dev",
|
|
421
|
+
"aws:cloudformation:logical-id": "FriggVPC",
|
|
422
|
+
"Stage": "dev"
|
|
423
|
+
},
|
|
424
|
+
"dependencies": {
|
|
425
|
+
"subnets": ["subnet-0123456789abcdef0", "subnet-0123456789abcdef1", "subnet-0123456789abcdef2"],
|
|
426
|
+
"securityGroups": ["sg-0123456789abcdef0"],
|
|
427
|
+
"hasBlockingDependencies": false
|
|
428
|
+
},
|
|
429
|
+
"estimatedMonthlyCost": 36.00,
|
|
430
|
+
"deletionPhase": 3,
|
|
431
|
+
"canDelete": true,
|
|
432
|
+
"blockingReason": null
|
|
433
|
+
}
|
|
434
|
+
// ... 10 more resources
|
|
435
|
+
],
|
|
436
|
+
"deletionPlan": {
|
|
437
|
+
"phase1": [],
|
|
438
|
+
"phase2": [
|
|
439
|
+
{
|
|
440
|
+
"physicalId": "subnet-0123456789abcdef0",
|
|
441
|
+
"resourceType": "AWS::EC2::Subnet",
|
|
442
|
+
"order": 1
|
|
443
|
+
}
|
|
444
|
+
// ... 8 more
|
|
445
|
+
],
|
|
446
|
+
"phase3": [
|
|
447
|
+
{
|
|
448
|
+
"physicalId": "vpc-0e2351eac99adcb83",
|
|
449
|
+
"resourceType": "AWS::EC2::VPC",
|
|
450
|
+
"order": 1
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
"physicalId": "vpc-020a0365610c05f0b",
|
|
454
|
+
"resourceType": "AWS::EC2::VPC",
|
|
455
|
+
"order": 2
|
|
456
|
+
}
|
|
457
|
+
]
|
|
458
|
+
},
|
|
459
|
+
"estimatedSavings": {
|
|
460
|
+
"monthly": 72.00,
|
|
461
|
+
"annual": 864.00
|
|
462
|
+
},
|
|
463
|
+
"warnings": [
|
|
464
|
+
"This operation cannot be easily undone",
|
|
465
|
+
"Resources will be permanently deleted from AWS",
|
|
466
|
+
"Verify no applications depend on these resources"
|
|
467
|
+
],
|
|
468
|
+
"timestamp": "2025-10-27T10:30:00Z"
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### Non-Functional Requirements
|
|
473
|
+
|
|
474
|
+
#### NFR-1: Performance
|
|
475
|
+
|
|
476
|
+
- Dependency checking must complete within 30 seconds for 50 resources
|
|
477
|
+
- Deletion progress updates every 2 seconds
|
|
478
|
+
- Support parallel deletion where safe (independent resources)
|
|
479
|
+
|
|
480
|
+
#### NFR-2: Reliability
|
|
481
|
+
|
|
482
|
+
- Idempotent operations (safe to retry after failure)
|
|
483
|
+
- Continue on non-fatal errors
|
|
484
|
+
- Comprehensive error logging
|
|
485
|
+
|
|
486
|
+
#### NFR-3: Security
|
|
487
|
+
|
|
488
|
+
- Require explicit confirmation for destructive operations
|
|
489
|
+
- Log all deletion attempts to CloudWatch/audit log
|
|
490
|
+
- Support AWS CloudTrail integration
|
|
491
|
+
- Never expose credentials in logs
|
|
492
|
+
|
|
493
|
+
#### NFR-4: Usability
|
|
494
|
+
|
|
495
|
+
- Clear, informative error messages
|
|
496
|
+
- Progress indicators for long operations
|
|
497
|
+
- Cost estimates to help decision-making
|
|
498
|
+
- Helpful next-step suggestions
|
|
499
|
+
|
|
500
|
+
## Design
|
|
501
|
+
|
|
502
|
+
### Architecture
|
|
503
|
+
|
|
504
|
+
Following **Hexagonal Architecture** principles:
|
|
505
|
+
|
|
506
|
+
```
|
|
507
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
508
|
+
│ Adapter Layer (CLI Command) │
|
|
509
|
+
│ cleanup-command/index.js │
|
|
510
|
+
│ - Parse command line options │
|
|
511
|
+
│ - Handle confirmation prompts │
|
|
512
|
+
│ - Format output (terminal, JSON) │
|
|
513
|
+
│ - Display progress indicators │
|
|
514
|
+
└────────────────┬────────────────────────────────────────────────┘
|
|
515
|
+
│ calls
|
|
516
|
+
┌────────────────▼────────────────────────────────────────────────┐
|
|
517
|
+
│ Application Layer (Use Case) │
|
|
518
|
+
│ CleanupOrphanedResourcesUseCase (NEW) │
|
|
519
|
+
│ - Orchestrate cleanup workflow │
|
|
520
|
+
│ - Identify duplicate resources │
|
|
521
|
+
│ - Check dependencies │
|
|
522
|
+
│ - Plan deletion order │
|
|
523
|
+
│ - Execute deletions with rollback │
|
|
524
|
+
└────────────────┬────────────────────────────────────────────────┘
|
|
525
|
+
│ calls
|
|
526
|
+
┌────────────────▼────────────────────────────────────────────────┐
|
|
527
|
+
│ Domain Layer (Services) │
|
|
528
|
+
│ OrphanedResourceCategorizerService (REUSE) │
|
|
529
|
+
│ - Identify duplicate orphaned resources │
|
|
530
|
+
│ │
|
|
531
|
+
│ ResourceDependencyAnalyzer (NEW) │
|
|
532
|
+
│ - Analyze resource dependencies │
|
|
533
|
+
│ - Determine deletion order │
|
|
534
|
+
│ - Check for blocking dependencies │
|
|
535
|
+
│ │
|
|
536
|
+
│ ResourceDeletionPlanner (NEW) │
|
|
537
|
+
│ - Create deletion plan with phases │
|
|
538
|
+
│ - Calculate cost savings │
|
|
539
|
+
│ - Generate warnings │
|
|
540
|
+
└────────────────┬────────────────────────────────────────────────┘
|
|
541
|
+
│ uses
|
|
542
|
+
┌────────────────▼────────────────────────────────────────────────┐
|
|
543
|
+
│ Infrastructure Layer (Repositories) │
|
|
544
|
+
│ ResourceDeleterRepository (NEW) │
|
|
545
|
+
│ - Execute AWS API calls for resource deletion │
|
|
546
|
+
│ - Check resource dependencies via AWS APIs │
|
|
547
|
+
│ - Handle AWS-specific errors │
|
|
548
|
+
│ │
|
|
549
|
+
│ AuditLogRepository (NEW) │
|
|
550
|
+
│ - Log deletion attempts and results │
|
|
551
|
+
│ - Store audit trail │
|
|
552
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Component Details
|
|
556
|
+
|
|
557
|
+
#### 1. ResourceDependencyAnalyzer (NEW)
|
|
558
|
+
|
|
559
|
+
**Location**: `packages/devtools/infrastructure/domains/health/domain/services/resource-dependency-analyzer.js`
|
|
560
|
+
|
|
561
|
+
**Responsibilities**:
|
|
562
|
+
- Analyze dependencies between resources
|
|
563
|
+
- Determine safe deletion order
|
|
564
|
+
- Detect blocking dependencies
|
|
565
|
+
|
|
566
|
+
**Public Methods**:
|
|
567
|
+
|
|
568
|
+
```javascript
|
|
569
|
+
class ResourceDependencyAnalyzer {
|
|
570
|
+
constructor({ ec2Client, elbClient, rdsClient, lambdaClient }) {
|
|
571
|
+
this.ec2 = ec2Client;
|
|
572
|
+
this.elb = elbClient;
|
|
573
|
+
this.rds = rdsClient;
|
|
574
|
+
this.lambda = lambdaClient;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Analyze dependencies for a list of resources
|
|
579
|
+
*
|
|
580
|
+
* @param {Array} resources - Resources to analyze
|
|
581
|
+
* @returns {Promise<Object>} Dependency analysis
|
|
582
|
+
*/
|
|
583
|
+
async analyzeDependencies(resources) {
|
|
584
|
+
const analysis = {
|
|
585
|
+
canDeleteAll: true,
|
|
586
|
+
blockedResources: [],
|
|
587
|
+
dependencies: {},
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
for (const resource of resources) {
|
|
591
|
+
const deps = await this._checkResourceDependencies(resource);
|
|
592
|
+
|
|
593
|
+
analysis.dependencies[resource.physicalId] = deps;
|
|
594
|
+
|
|
595
|
+
if (deps.hasBlockingDependencies) {
|
|
596
|
+
analysis.canDeleteAll = false;
|
|
597
|
+
analysis.blockedResources.push({
|
|
598
|
+
resource,
|
|
599
|
+
blockingDependencies: deps.blocking,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return analysis;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Determine deletion order based on dependencies
|
|
609
|
+
*
|
|
610
|
+
* @param {Array} resources - Resources to order
|
|
611
|
+
* @returns {Object} Deletion plan with phases
|
|
612
|
+
*/
|
|
613
|
+
determineDeletionOrder(resources) {
|
|
614
|
+
const phases = {
|
|
615
|
+
phase1: [], // Detach dependencies
|
|
616
|
+
phase2: [], // Delete dependent resources
|
|
617
|
+
phase3: [], // Delete core resources
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
for (const resource of resources) {
|
|
621
|
+
const phase = this._getDeletionPhase(resource.resourceType);
|
|
622
|
+
phases[`phase${phase}`].push(resource);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Sort each phase by priority
|
|
626
|
+
phases.phase1.sort(this._compareDeletionPriority);
|
|
627
|
+
phases.phase2.sort(this._compareDeletionPriority);
|
|
628
|
+
phases.phase3.sort(this._compareDeletionPriority);
|
|
629
|
+
|
|
630
|
+
return phases;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Check dependencies for a specific resource
|
|
635
|
+
* @private
|
|
636
|
+
*/
|
|
637
|
+
async _checkResourceDependencies(resource) {
|
|
638
|
+
switch (resource.resourceType) {
|
|
639
|
+
case 'AWS::EC2::VPC':
|
|
640
|
+
return await this._checkVpcDependencies(resource.physicalId);
|
|
641
|
+
case 'AWS::EC2::Subnet':
|
|
642
|
+
return await this._checkSubnetDependencies(resource.physicalId);
|
|
643
|
+
case 'AWS::EC2::SecurityGroup':
|
|
644
|
+
return await this._checkSecurityGroupDependencies(resource.physicalId);
|
|
645
|
+
default:
|
|
646
|
+
return { hasBlockingDependencies: false, blocking: [], dependent: [] };
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Check VPC dependencies
|
|
652
|
+
* @private
|
|
653
|
+
*/
|
|
654
|
+
async _checkVpcDependencies(vpcId) {
|
|
655
|
+
const [subnets, securityGroups, natGateways, igws, endpoints] = await Promise.all([
|
|
656
|
+
this.ec2.describeSubnets({ Filters: [{ Name: 'vpc-id', Values: [vpcId] }] }),
|
|
657
|
+
this.ec2.describeSecurityGroups({ Filters: [{ Name: 'vpc-id', Values: [vpcId] }] }),
|
|
658
|
+
this.ec2.describeNatGateways({ Filter: [{ Name: 'vpc-id', Values: [vpcId] }] }),
|
|
659
|
+
this.ec2.describeInternetGateways({ Filters: [{ Name: 'attachment.vpc-id', Values: [vpcId] }] }),
|
|
660
|
+
this.ec2.describeVpcEndpoints({ Filters: [{ Name: 'vpc-id', Values: [vpcId] }] }),
|
|
661
|
+
]);
|
|
662
|
+
|
|
663
|
+
const blocking = [];
|
|
664
|
+
|
|
665
|
+
// Filter out default resources that can be deleted with VPC
|
|
666
|
+
const customSubnets = subnets.Subnets.filter(s => !s.DefaultForAz);
|
|
667
|
+
const customSGs = securityGroups.SecurityGroups.filter(sg => sg.GroupName !== 'default');
|
|
668
|
+
|
|
669
|
+
if (customSubnets.length > 0) {
|
|
670
|
+
blocking.push({ type: 'subnets', count: customSubnets.length, ids: customSubnets.map(s => s.SubnetId) });
|
|
671
|
+
}
|
|
672
|
+
if (customSGs.length > 0) {
|
|
673
|
+
blocking.push({ type: 'security_groups', count: customSGs.length, ids: customSGs.map(sg => sg.GroupId) });
|
|
674
|
+
}
|
|
675
|
+
if (natGateways.NatGateways?.length > 0) {
|
|
676
|
+
blocking.push({ type: 'nat_gateways', count: natGateways.NatGateways.length });
|
|
677
|
+
}
|
|
678
|
+
if (igws.InternetGateways?.length > 0) {
|
|
679
|
+
blocking.push({ type: 'internet_gateways', count: igws.InternetGateways.length });
|
|
680
|
+
}
|
|
681
|
+
if (endpoints.VpcEndpoints?.length > 0) {
|
|
682
|
+
blocking.push({ type: 'vpc_endpoints', count: endpoints.VpcEndpoints.length });
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return {
|
|
686
|
+
hasBlockingDependencies: blocking.length > 0,
|
|
687
|
+
blocking,
|
|
688
|
+
dependent: [],
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Check security group dependencies
|
|
694
|
+
* @private
|
|
695
|
+
*/
|
|
696
|
+
async _checkSecurityGroupDependencies(sgId) {
|
|
697
|
+
// Check if security group is in use by any resources
|
|
698
|
+
const [ec2Instances, rdsInstances, lambdaFunctions, loadBalancers] = await Promise.all([
|
|
699
|
+
this.ec2.describeInstances({ Filters: [{ Name: 'instance.group-id', Values: [sgId] }] }),
|
|
700
|
+
this.rds.describeDBInstances(),
|
|
701
|
+
this.lambda.listFunctions(),
|
|
702
|
+
this.elb.describeLoadBalancers(),
|
|
703
|
+
]);
|
|
704
|
+
|
|
705
|
+
const blocking = [];
|
|
706
|
+
|
|
707
|
+
// Check EC2 instances
|
|
708
|
+
const instances = ec2Instances.Reservations.flatMap(r => r.Instances);
|
|
709
|
+
if (instances.length > 0) {
|
|
710
|
+
blocking.push({
|
|
711
|
+
type: 'ec2_instances',
|
|
712
|
+
count: instances.length,
|
|
713
|
+
ids: instances.map(i => i.InstanceId),
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Check RDS instances
|
|
718
|
+
const rdsUsingThisSG = rdsInstances.DBInstances.filter(db =>
|
|
719
|
+
db.VpcSecurityGroups?.some(sg => sg.VpcSecurityGroupId === sgId)
|
|
720
|
+
);
|
|
721
|
+
if (rdsUsingThisSG.length > 0) {
|
|
722
|
+
blocking.push({
|
|
723
|
+
type: 'rds_instances',
|
|
724
|
+
count: rdsUsingThisSG.length,
|
|
725
|
+
ids: rdsUsingThisSG.map(db => db.DBInstanceIdentifier),
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Check Lambda functions
|
|
730
|
+
const lambdaUsingThisSG = lambdaFunctions.Functions.filter(fn =>
|
|
731
|
+
fn.VpcConfig?.SecurityGroupIds?.includes(sgId)
|
|
732
|
+
);
|
|
733
|
+
if (lambdaUsingThisSG.length > 0) {
|
|
734
|
+
blocking.push({
|
|
735
|
+
type: 'lambda_functions',
|
|
736
|
+
count: lambdaUsingThisSG.length,
|
|
737
|
+
ids: lambdaUsingThisSG.map(fn => fn.FunctionName),
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return {
|
|
742
|
+
hasBlockingDependencies: blocking.length > 0,
|
|
743
|
+
blocking,
|
|
744
|
+
dependent: [],
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Get deletion phase for resource type
|
|
750
|
+
* @private
|
|
751
|
+
*/
|
|
752
|
+
_getDeletionPhase(resourceType) {
|
|
753
|
+
const phaseMap = {
|
|
754
|
+
'AWS::EC2::VPCEndpoint': 1,
|
|
755
|
+
'AWS::EC2::NatGateway': 2,
|
|
756
|
+
'AWS::EC2::InternetGateway': 2,
|
|
757
|
+
'AWS::EC2::RouteTable': 2,
|
|
758
|
+
'AWS::EC2::NetworkAcl': 2,
|
|
759
|
+
'AWS::EC2::Subnet': 2,
|
|
760
|
+
'AWS::EC2::SecurityGroup': 2,
|
|
761
|
+
'AWS::EC2::VPC': 3,
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
return phaseMap[resourceType] || 2;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Compare deletion priority
|
|
769
|
+
* @private
|
|
770
|
+
*/
|
|
771
|
+
_compareDeletionPriority(a, b) {
|
|
772
|
+
const priorityMap = {
|
|
773
|
+
'AWS::EC2::VPCEndpoint': 1,
|
|
774
|
+
'AWS::EC2::NatGateway': 2,
|
|
775
|
+
'AWS::EC2::InternetGateway': 3,
|
|
776
|
+
'AWS::EC2::RouteTable': 4,
|
|
777
|
+
'AWS::EC2::Subnet': 5,
|
|
778
|
+
'AWS::EC2::SecurityGroup': 6,
|
|
779
|
+
'AWS::EC2::VPC': 7,
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
return (priorityMap[a.resourceType] || 99) - (priorityMap[b.resourceType] || 99);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
#### 2. ResourceDeletionPlanner (NEW)
|
|
788
|
+
|
|
789
|
+
**Location**: `packages/devtools/infrastructure/domains/health/domain/services/resource-deletion-planner.js`
|
|
790
|
+
|
|
791
|
+
**Responsibilities**:
|
|
792
|
+
- Create detailed deletion plan
|
|
793
|
+
- Calculate cost savings
|
|
794
|
+
- Generate warnings
|
|
795
|
+
|
|
796
|
+
**Public Methods**:
|
|
797
|
+
|
|
798
|
+
```javascript
|
|
799
|
+
class ResourceDeletionPlanner {
|
|
800
|
+
/**
|
|
801
|
+
* Create deletion plan with phases and cost estimates
|
|
802
|
+
*
|
|
803
|
+
* @param {Object} params
|
|
804
|
+
* @param {Array} params.resources - Resources to delete
|
|
805
|
+
* @param {Object} params.dependencyAnalysis - Dependency analysis result
|
|
806
|
+
* @returns {Object} Deletion plan
|
|
807
|
+
*/
|
|
808
|
+
createDeletionPlan({ resources, dependencyAnalysis }) {
|
|
809
|
+
// 1. Filter out blocked resources
|
|
810
|
+
const deletableResources = resources.filter(
|
|
811
|
+
(r) => !dependencyAnalysis.blockedResources.some(
|
|
812
|
+
(b) => b.resource.physicalId === r.physicalId
|
|
813
|
+
)
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
// 2. Determine deletion order
|
|
817
|
+
const deletionPhases = this._organizeDeletionPhases(deletableResources);
|
|
818
|
+
|
|
819
|
+
// 3. Calculate cost savings
|
|
820
|
+
const costSavings = this._calculateCostSavings(deletableResources);
|
|
821
|
+
|
|
822
|
+
// 4. Generate warnings
|
|
823
|
+
const warnings = this._generateWarnings(deletableResources, dependencyAnalysis);
|
|
824
|
+
|
|
825
|
+
return {
|
|
826
|
+
totalResources: resources.length,
|
|
827
|
+
deletableCount: deletableResources.length,
|
|
828
|
+
blockedCount: dependencyAnalysis.blockedResources.length,
|
|
829
|
+
phases: deletionPhases,
|
|
830
|
+
costSavings,
|
|
831
|
+
warnings,
|
|
832
|
+
blockedResources: dependencyAnalysis.blockedResources,
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Calculate estimated monthly cost savings
|
|
838
|
+
* @private
|
|
839
|
+
*/
|
|
840
|
+
_calculateCostSavings(resources) {
|
|
841
|
+
let monthlyCost = 0;
|
|
842
|
+
|
|
843
|
+
for (const resource of resources) {
|
|
844
|
+
switch (resource.resourceType) {
|
|
845
|
+
case 'AWS::EC2::VPC':
|
|
846
|
+
// Assume NAT Gateway cost (most expensive part of VPC)
|
|
847
|
+
monthlyCost += 36; // $32.40 for NAT + $0.045/GB
|
|
848
|
+
break;
|
|
849
|
+
case 'AWS::EC2::NatGateway':
|
|
850
|
+
monthlyCost += 36;
|
|
851
|
+
break;
|
|
852
|
+
case 'AWS::EC2::EIP':
|
|
853
|
+
monthlyCost += 3.65; // $0.005/hour
|
|
854
|
+
break;
|
|
855
|
+
// Other resources have minimal direct costs
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return {
|
|
860
|
+
monthly: monthlyCost,
|
|
861
|
+
annual: monthlyCost * 12,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Generate warnings for deletion
|
|
867
|
+
* @private
|
|
868
|
+
*/
|
|
869
|
+
_generateWarnings(resources, dependencyAnalysis) {
|
|
870
|
+
const warnings = [
|
|
871
|
+
'This operation cannot be easily undone',
|
|
872
|
+
'Resources will be permanently deleted from AWS',
|
|
873
|
+
'Verify no applications depend on these resources',
|
|
874
|
+
];
|
|
875
|
+
|
|
876
|
+
// Add specific warnings based on resource types
|
|
877
|
+
if (resources.some(r => r.resourceType === 'AWS::EC2::VPC')) {
|
|
878
|
+
warnings.push('Deleting VPCs will also delete associated default resources');
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (dependencyAnalysis.blockedResources.length > 0) {
|
|
882
|
+
warnings.push(
|
|
883
|
+
`${dependencyAnalysis.blockedResources.length} resources cannot be deleted due to dependencies`
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
return warnings;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
#### 3. CleanupOrphanedResourcesUseCase (NEW)
|
|
893
|
+
|
|
894
|
+
**Location**: `packages/devtools/infrastructure/domains/health/application/use-cases/cleanup-orphaned-resources-use-case.js`
|
|
895
|
+
|
|
896
|
+
**Responsibilities**:
|
|
897
|
+
- Orchestrate complete cleanup workflow
|
|
898
|
+
- Coordinate categorizer, analyzer, planner, and deleter
|
|
899
|
+
- Handle errors and rollback
|
|
900
|
+
|
|
901
|
+
**Public Methods**:
|
|
902
|
+
|
|
903
|
+
```javascript
|
|
904
|
+
class CleanupOrphanedResourcesUseCase {
|
|
905
|
+
constructor({
|
|
906
|
+
orphanedResourceCategorizerService,
|
|
907
|
+
resourceDependencyAnalyzer,
|
|
908
|
+
resourceDeletionPlanner,
|
|
909
|
+
resourceDeleterRepository,
|
|
910
|
+
auditLogRepository,
|
|
911
|
+
}) {
|
|
912
|
+
this.categorizerService = orphanedResourceCategorizerService;
|
|
913
|
+
this.dependencyAnalyzer = resourceDependencyAnalyzer;
|
|
914
|
+
this.deletionPlanner = resourceDeletionPlanner;
|
|
915
|
+
this.deleterRepo = resourceDeleterRepository;
|
|
916
|
+
this.auditRepo = auditLogRepository;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Execute cleanup workflow
|
|
921
|
+
*
|
|
922
|
+
* @param {Object} params
|
|
923
|
+
* @param {StackIdentifier} params.stackIdentifier
|
|
924
|
+
* @param {string} params.buildTemplatePath
|
|
925
|
+
* @param {boolean} params.dryRun - If true, only plan, don't execute
|
|
926
|
+
* @param {string} params.resourceTypeFilter - Optional resource type filter
|
|
927
|
+
* @param {string} params.logicalIdPattern - Optional logical ID pattern
|
|
928
|
+
* @returns {Promise<Object>} Cleanup result
|
|
929
|
+
*/
|
|
930
|
+
async execute({
|
|
931
|
+
stackIdentifier,
|
|
932
|
+
buildTemplatePath,
|
|
933
|
+
dryRun = true,
|
|
934
|
+
resourceTypeFilter = null,
|
|
935
|
+
logicalIdPattern = null,
|
|
936
|
+
}) {
|
|
937
|
+
// 1. Get duplicate orphaned resources
|
|
938
|
+
const categorization = await this.categorizerService.categorize({
|
|
939
|
+
orphanedResources: [], // Will be fetched internally
|
|
940
|
+
stackIdentifier,
|
|
941
|
+
buildTemplatePath,
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
let duplicates = categorization.duplicates || [];
|
|
945
|
+
|
|
946
|
+
// 2. Apply filters
|
|
947
|
+
if (resourceTypeFilter) {
|
|
948
|
+
duplicates = duplicates.filter((r) => r.resourceType === resourceTypeFilter);
|
|
949
|
+
}
|
|
950
|
+
if (logicalIdPattern) {
|
|
951
|
+
const regex = new RegExp(logicalIdPattern.replace('*', '.*'));
|
|
952
|
+
duplicates = duplicates.filter((r) => regex.test(r.logicalId));
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (duplicates.length === 0) {
|
|
956
|
+
return {
|
|
957
|
+
success: true,
|
|
958
|
+
message: 'No duplicate orphaned resources found',
|
|
959
|
+
deletedCount: 0,
|
|
960
|
+
skippedCount: 0,
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// 3. Analyze dependencies
|
|
965
|
+
const dependencyAnalysis = await this.dependencyAnalyzer.analyzeDependencies(duplicates);
|
|
966
|
+
|
|
967
|
+
// 4. Create deletion plan
|
|
968
|
+
const deletionPlan = this.deletionPlanner.createDeletionPlan({
|
|
969
|
+
resources: duplicates,
|
|
970
|
+
dependencyAnalysis,
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
// 5. If dry-run, return plan without executing
|
|
974
|
+
if (dryRun) {
|
|
975
|
+
return {
|
|
976
|
+
dryRun: true,
|
|
977
|
+
deletionPlan,
|
|
978
|
+
message: 'Dry-run complete. No resources were deleted.',
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// 6. Execute deletions
|
|
983
|
+
const deletionResult = await this._executeDeletionPlan(
|
|
984
|
+
deletionPlan,
|
|
985
|
+
stackIdentifier
|
|
986
|
+
);
|
|
987
|
+
|
|
988
|
+
// 7. Log to audit trail
|
|
989
|
+
await this.auditRepo.logCleanupOperation({
|
|
990
|
+
stackIdentifier,
|
|
991
|
+
deletionPlan,
|
|
992
|
+
result: deletionResult,
|
|
993
|
+
timestamp: new Date().toISOString(),
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
return {
|
|
997
|
+
success: true,
|
|
998
|
+
dryRun: false,
|
|
999
|
+
deletedCount: deletionResult.successCount,
|
|
1000
|
+
failedCount: deletionResult.failedCount,
|
|
1001
|
+
skippedCount: deletionPlan.blockedCount,
|
|
1002
|
+
deletionResult,
|
|
1003
|
+
costSavings: deletionPlan.costSavings,
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Execute deletion plan phase by phase
|
|
1009
|
+
* @private
|
|
1010
|
+
*/
|
|
1011
|
+
async _executeDeletionPlan(deletionPlan, stackIdentifier) {
|
|
1012
|
+
const results = {
|
|
1013
|
+
successCount: 0,
|
|
1014
|
+
failedCount: 0,
|
|
1015
|
+
deleted: [],
|
|
1016
|
+
failed: [],
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
// Execute in order: phase1, phase2, phase3
|
|
1020
|
+
for (const phase of ['phase1', 'phase2', 'phase3']) {
|
|
1021
|
+
const resources = deletionPlan.phases[phase];
|
|
1022
|
+
|
|
1023
|
+
for (const resource of resources) {
|
|
1024
|
+
try {
|
|
1025
|
+
await this.deleterRepo.deleteResource({
|
|
1026
|
+
resourceType: resource.resourceType,
|
|
1027
|
+
physicalId: resource.physicalId,
|
|
1028
|
+
region: stackIdentifier.region,
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
results.successCount++;
|
|
1032
|
+
results.deleted.push({
|
|
1033
|
+
physicalId: resource.physicalId,
|
|
1034
|
+
resourceType: resource.resourceType,
|
|
1035
|
+
});
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
results.failedCount++;
|
|
1038
|
+
results.failed.push({
|
|
1039
|
+
physicalId: resource.physicalId,
|
|
1040
|
+
resourceType: resource.resourceType,
|
|
1041
|
+
error: error.message,
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Small delay between deletions to avoid throttling
|
|
1046
|
+
await this._delay(500);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Delay between phases to allow AWS to clean up
|
|
1050
|
+
if (resources.length > 0) {
|
|
1051
|
+
await this._delay(5000);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
return results;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
_delay(ms) {
|
|
1059
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
#### 4. ResourceDeleterRepository (NEW)
|
|
1065
|
+
|
|
1066
|
+
**Location**: `packages/devtools/infrastructure/domains/health/infrastructure/repositories/resource-deleter-repository.js`
|
|
1067
|
+
|
|
1068
|
+
**Responsibilities**:
|
|
1069
|
+
- Execute AWS API calls for resource deletion
|
|
1070
|
+
- Handle AWS-specific errors
|
|
1071
|
+
|
|
1072
|
+
**Public Methods**:
|
|
1073
|
+
|
|
1074
|
+
```javascript
|
|
1075
|
+
class ResourceDeleterRepository {
|
|
1076
|
+
constructor({ ec2Client, region }) {
|
|
1077
|
+
this.ec2 = ec2Client;
|
|
1078
|
+
this.region = region;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Delete a resource from AWS
|
|
1083
|
+
*
|
|
1084
|
+
* @param {Object} params
|
|
1085
|
+
* @param {string} params.resourceType - CloudFormation resource type
|
|
1086
|
+
* @param {string} params.physicalId - Physical resource ID
|
|
1087
|
+
* @param {string} params.region - AWS region
|
|
1088
|
+
* @returns {Promise<Object>} Deletion result
|
|
1089
|
+
*/
|
|
1090
|
+
async deleteResource({ resourceType, physicalId, region }) {
|
|
1091
|
+
try {
|
|
1092
|
+
switch (resourceType) {
|
|
1093
|
+
case 'AWS::EC2::VPC':
|
|
1094
|
+
return await this._deleteVpc(physicalId);
|
|
1095
|
+
case 'AWS::EC2::Subnet':
|
|
1096
|
+
return await this._deleteSubnet(physicalId);
|
|
1097
|
+
case 'AWS::EC2::SecurityGroup':
|
|
1098
|
+
return await this._deleteSecurityGroup(physicalId);
|
|
1099
|
+
// Add more resource types as needed
|
|
1100
|
+
default:
|
|
1101
|
+
throw new Error(`Unsupported resource type: ${resourceType}`);
|
|
1102
|
+
}
|
|
1103
|
+
} catch (error) {
|
|
1104
|
+
throw new Error(
|
|
1105
|
+
`Failed to delete ${resourceType} ${physicalId}: ${error.message}`
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* Delete VPC
|
|
1112
|
+
* @private
|
|
1113
|
+
*/
|
|
1114
|
+
async _deleteVpc(vpcId) {
|
|
1115
|
+
await this.ec2.deleteVpc({ VpcId: vpcId });
|
|
1116
|
+
return { success: true, physicalId: vpcId };
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Delete Subnet
|
|
1121
|
+
* @private
|
|
1122
|
+
*/
|
|
1123
|
+
async _deleteSubnet(subnetId) {
|
|
1124
|
+
await this.ec2.deleteSubnet({ SubnetId: subnetId });
|
|
1125
|
+
return { success: true, physicalId: subnetId };
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Delete Security Group
|
|
1130
|
+
* @private
|
|
1131
|
+
*/
|
|
1132
|
+
async _deleteSecurityGroup(sgId) {
|
|
1133
|
+
await this.ec2.deleteSecurityGroup({ GroupId: sgId });
|
|
1134
|
+
return { success: true, physicalId: sgId };
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
### Data Flow
|
|
1140
|
+
|
|
1141
|
+
```
|
|
1142
|
+
User runs: frigg cleanup --orphaned --execute
|
|
1143
|
+
↓
|
|
1144
|
+
cleanup-command/index.js
|
|
1145
|
+
↓ calls
|
|
1146
|
+
CleanupOrphanedResourcesUseCase.execute()
|
|
1147
|
+
↓ calls
|
|
1148
|
+
OrphanedResourceCategorizerService.categorize()
|
|
1149
|
+
↓ returns
|
|
1150
|
+
{ duplicates: [...] }
|
|
1151
|
+
↓
|
|
1152
|
+
ResourceDependencyAnalyzer.analyzeDependencies()
|
|
1153
|
+
↓ returns
|
|
1154
|
+
{ blockedResources: [...], dependencies: {...} }
|
|
1155
|
+
↓
|
|
1156
|
+
ResourceDeletionPlanner.createDeletionPlan()
|
|
1157
|
+
↓ returns
|
|
1158
|
+
{ phases: {...}, costSavings: {...}, warnings: [...] }
|
|
1159
|
+
↓
|
|
1160
|
+
Display confirmation prompt (unless --yes)
|
|
1161
|
+
↓ if confirmed
|
|
1162
|
+
_executeDeletionPlan()
|
|
1163
|
+
↓ phase by phase
|
|
1164
|
+
ResourceDeleterRepository.deleteResource()
|
|
1165
|
+
↓ for each resource
|
|
1166
|
+
AWS API calls (deleteVpc, deleteSubnet, etc.)
|
|
1167
|
+
↓
|
|
1168
|
+
AuditLogRepository.logCleanupOperation()
|
|
1169
|
+
↓
|
|
1170
|
+
Display result summary
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1173
|
+
## Implementation Plan
|
|
1174
|
+
|
|
1175
|
+
### Phase 1: Core Dependency Analysis (TDD)
|
|
1176
|
+
|
|
1177
|
+
1. **Write tests** for `ResourceDependencyAnalyzer`
|
|
1178
|
+
- Test VPC dependency checking
|
|
1179
|
+
- Test security group dependency checking
|
|
1180
|
+
- Test deletion order determination
|
|
1181
|
+
- Test blocking dependency detection
|
|
1182
|
+
|
|
1183
|
+
2. **Implement** `ResourceDependencyAnalyzer`
|
|
1184
|
+
- Implement dependency checking methods
|
|
1185
|
+
- Implement deletion order logic
|
|
1186
|
+
- Handle AWS API calls
|
|
1187
|
+
|
|
1188
|
+
3. **Verify** tests pass
|
|
1189
|
+
|
|
1190
|
+
### Phase 2: Deletion Planning (TDD)
|
|
1191
|
+
|
|
1192
|
+
1. **Write tests** for `ResourceDeletionPlanner`
|
|
1193
|
+
- Test deletion plan creation
|
|
1194
|
+
- Test cost calculation
|
|
1195
|
+
- Test warning generation
|
|
1196
|
+
- Test phase organization
|
|
1197
|
+
|
|
1198
|
+
2. **Implement** `ResourceDeletionPlanner`
|
|
1199
|
+
- Implement plan creation
|
|
1200
|
+
- Implement cost calculator
|
|
1201
|
+
- Implement warning generator
|
|
1202
|
+
|
|
1203
|
+
3. **Verify** tests pass
|
|
1204
|
+
|
|
1205
|
+
### Phase 3: Use Case Orchestration (TDD)
|
|
1206
|
+
|
|
1207
|
+
1. **Write tests** for `CleanupOrphanedResourcesUseCase`
|
|
1208
|
+
- Test dry-run mode
|
|
1209
|
+
- Test execute mode
|
|
1210
|
+
- Test filtering (resource type, logical ID)
|
|
1211
|
+
- Test error handling
|
|
1212
|
+
- Test rollback behavior
|
|
1213
|
+
|
|
1214
|
+
2. **Implement** use case
|
|
1215
|
+
- Orchestrate all services
|
|
1216
|
+
- Implement deletion execution
|
|
1217
|
+
- Handle errors gracefully
|
|
1218
|
+
|
|
1219
|
+
3. **Verify** tests pass
|
|
1220
|
+
|
|
1221
|
+
### Phase 4: Infrastructure Repository (TDD)
|
|
1222
|
+
|
|
1223
|
+
1. **Write tests** for `ResourceDeleterRepository`
|
|
1224
|
+
- Test VPC deletion
|
|
1225
|
+
- Test subnet deletion
|
|
1226
|
+
- Test security group deletion
|
|
1227
|
+
- Test error handling
|
|
1228
|
+
|
|
1229
|
+
2. **Implement** repository
|
|
1230
|
+
- AWS SDK integration
|
|
1231
|
+
- Error handling and retries
|
|
1232
|
+
|
|
1233
|
+
3. **Verify** tests pass
|
|
1234
|
+
|
|
1235
|
+
### Phase 5: CLI Integration
|
|
1236
|
+
|
|
1237
|
+
1. **Create** `cleanup-command/index.js`
|
|
1238
|
+
- Command line parsing
|
|
1239
|
+
- Confirmation prompts
|
|
1240
|
+
- Output formatting
|
|
1241
|
+
- Progress indicators
|
|
1242
|
+
|
|
1243
|
+
2. **Test** with real data
|
|
1244
|
+
- Test dry-run mode
|
|
1245
|
+
- Test execute mode
|
|
1246
|
+
- Test error scenarios
|
|
1247
|
+
- Test with `quo-integrations-dev`
|
|
1248
|
+
|
|
1249
|
+
3. **Update documentation**
|
|
1250
|
+
|
|
1251
|
+
## Testing Strategy
|
|
1252
|
+
|
|
1253
|
+
### Unit Tests
|
|
1254
|
+
|
|
1255
|
+
**ResourceDependencyAnalyzer**:
|
|
1256
|
+
- ✅ Analyze VPC with dependencies
|
|
1257
|
+
- ✅ Analyze VPC without dependencies
|
|
1258
|
+
- ✅ Analyze security group in use
|
|
1259
|
+
- ✅ Analyze security group not in use
|
|
1260
|
+
- ✅ Determine deletion order correctly
|
|
1261
|
+
- ✅ Handle API errors gracefully
|
|
1262
|
+
|
|
1263
|
+
**ResourceDeletionPlanner**:
|
|
1264
|
+
- ✅ Create plan with multiple phases
|
|
1265
|
+
- ✅ Calculate cost savings accurately
|
|
1266
|
+
- ✅ Generate appropriate warnings
|
|
1267
|
+
- ✅ Handle blocked resources
|
|
1268
|
+
- ✅ Filter resources correctly
|
|
1269
|
+
|
|
1270
|
+
**CleanupOrphanedResourcesUseCase**:
|
|
1271
|
+
- ✅ Dry-run returns plan without deleting
|
|
1272
|
+
- ✅ Execute mode deletes resources
|
|
1273
|
+
- ✅ Stop on first error (optional behavior)
|
|
1274
|
+
- ✅ Continue on non-fatal errors
|
|
1275
|
+
- ✅ Filter by resource type
|
|
1276
|
+
- ✅ Filter by logical ID pattern
|
|
1277
|
+
- ✅ Log to audit trail
|
|
1278
|
+
|
|
1279
|
+
**ResourceDeleterRepository**:
|
|
1280
|
+
- ✅ Delete VPC successfully
|
|
1281
|
+
- ✅ Delete subnet successfully
|
|
1282
|
+
- ✅ Delete security group successfully
|
|
1283
|
+
- ✅ Handle dependency errors
|
|
1284
|
+
- ✅ Handle permission errors
|
|
1285
|
+
- ✅ Retry on throttling
|
|
1286
|
+
|
|
1287
|
+
### Integration Tests
|
|
1288
|
+
|
|
1289
|
+
- ✅ End-to-end cleanup with mock AWS data
|
|
1290
|
+
- ✅ Verify deletion order is correct
|
|
1291
|
+
- ✅ Verify blocked resources are not deleted
|
|
1292
|
+
- ✅ Verify audit log is written
|
|
1293
|
+
- ✅ Test with `quo-integrations-dev` stack (11 duplicate resources)
|
|
1294
|
+
|
|
1295
|
+
### Real-World Validation
|
|
1296
|
+
|
|
1297
|
+
Use `quo-integrations-dev` stack as reference:
|
|
1298
|
+
- 11 duplicate orphaned resources
|
|
1299
|
+
- 2 VPCs with dependencies (9 total)
|
|
1300
|
+
- Verify all dependencies are detected
|
|
1301
|
+
- Verify deletion plan is safe
|
|
1302
|
+
- Execute in isolated test environment
|
|
1303
|
+
|
|
1304
|
+
## Security Considerations
|
|
1305
|
+
|
|
1306
|
+
### 1. Permission Requirements
|
|
1307
|
+
|
|
1308
|
+
**Minimum IAM Policy**:
|
|
1309
|
+
|
|
1310
|
+
```json
|
|
1311
|
+
{
|
|
1312
|
+
"Version": "2012-10-17",
|
|
1313
|
+
"Statement": [
|
|
1314
|
+
{
|
|
1315
|
+
"Effect": "Allow",
|
|
1316
|
+
"Action": [
|
|
1317
|
+
"ec2:DeleteVpc",
|
|
1318
|
+
"ec2:DeleteSubnet",
|
|
1319
|
+
"ec2:DeleteSecurityGroup",
|
|
1320
|
+
"ec2:DeleteInternetGateway",
|
|
1321
|
+
"ec2:DeleteNatGateway",
|
|
1322
|
+
"ec2:DeleteRouteTable",
|
|
1323
|
+
"ec2:DeleteVpcEndpoint",
|
|
1324
|
+
"ec2:DescribeVpcs",
|
|
1325
|
+
"ec2:DescribeSubnets",
|
|
1326
|
+
"ec2:DescribeSecurityGroups",
|
|
1327
|
+
"ec2:DescribeInstances",
|
|
1328
|
+
"ec2:DescribeNatGateways",
|
|
1329
|
+
"ec2:DescribeInternetGateways",
|
|
1330
|
+
"ec2:DescribeVpcEndpoints"
|
|
1331
|
+
],
|
|
1332
|
+
"Resource": "*",
|
|
1333
|
+
"Condition": {
|
|
1334
|
+
"StringEquals": {
|
|
1335
|
+
"ec2:ResourceTag/ManagedBy": "Frigg"
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
]
|
|
1340
|
+
}
|
|
1341
|
+
```
|
|
1342
|
+
|
|
1343
|
+
### 2. Audit Logging
|
|
1344
|
+
|
|
1345
|
+
All cleanup operations must be logged:
|
|
1346
|
+
|
|
1347
|
+
```javascript
|
|
1348
|
+
{
|
|
1349
|
+
"operation": "cleanup_orphaned_resources",
|
|
1350
|
+
"timestamp": "2025-10-27T10:30:00Z",
|
|
1351
|
+
"user": "user@example.com",
|
|
1352
|
+
"stackName": "quo-integrations-dev",
|
|
1353
|
+
"region": "us-east-1",
|
|
1354
|
+
"mode": "execute",
|
|
1355
|
+
"resources": {
|
|
1356
|
+
"total": 11,
|
|
1357
|
+
"deleted": 11,
|
|
1358
|
+
"failed": 0
|
|
1359
|
+
},
|
|
1360
|
+
"deletedResources": [
|
|
1361
|
+
{
|
|
1362
|
+
"physicalId": "vpc-0e2351eac99adcb83",
|
|
1363
|
+
"resourceType": "AWS::EC2::VPC",
|
|
1364
|
+
"logicalId": "FriggVPC",
|
|
1365
|
+
"deletedAt": "2025-10-27T10:30:15Z"
|
|
1366
|
+
}
|
|
1367
|
+
// ... 10 more
|
|
1368
|
+
],
|
|
1369
|
+
"costSavings": {
|
|
1370
|
+
"monthly": 72.00,
|
|
1371
|
+
"annual": 864.00
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
```
|
|
1375
|
+
|
|
1376
|
+
### 3. Safety Mechanisms
|
|
1377
|
+
|
|
1378
|
+
- ✅ Dry-run by default
|
|
1379
|
+
- ✅ Explicit confirmation required
|
|
1380
|
+
- ✅ Dependency checking before deletion
|
|
1381
|
+
- ✅ Deletion order enforcement
|
|
1382
|
+
- ✅ Audit trail for all operations
|
|
1383
|
+
- ✅ Support for --yes flag (automation)
|
|
1384
|
+
- ✅ Permission checks before deletion
|
|
1385
|
+
- ✅ Resource tagging verification (only delete Frigg-managed resources)
|
|
1386
|
+
|
|
1387
|
+
## Success Criteria
|
|
1388
|
+
|
|
1389
|
+
1. ✅ Command correctly identifies duplicate orphaned resources
|
|
1390
|
+
2. ✅ Dependency analysis detects blocking dependencies
|
|
1391
|
+
3. ✅ Deletion plan orders resources correctly
|
|
1392
|
+
4. ✅ Dry-run mode shows plan without deleting
|
|
1393
|
+
5. ✅ Execute mode deletes resources safely
|
|
1394
|
+
6. ✅ Error handling is robust (permissions, dependencies, API errors)
|
|
1395
|
+
7. ✅ All deletions are logged to audit trail
|
|
1396
|
+
8. ✅ Cost savings are calculated accurately
|
|
1397
|
+
9. ✅ All tests pass (unit, integration, real-world validation)
|
|
1398
|
+
10. ✅ Documentation is complete
|
|
1399
|
+
|
|
1400
|
+
## Open Questions
|
|
1401
|
+
|
|
1402
|
+
1. **Rollback strategy**: Should we support rollback if deletion fails partway through? (Complex: resources can't be "undeleted")
|
|
1403
|
+
2. **Backup before delete**: Should we export resource configurations before deletion for recovery purposes?
|
|
1404
|
+
3. **Concurrent deletion**: Should we delete independent resources in parallel for speed?
|
|
1405
|
+
4. **Retry policy**: How many retries for throttling errors? What backoff strategy?
|
|
1406
|
+
5. **Confirmation format**: Is `delete <stack-name>` sufficient, or should we require typing the exact resource count?
|
|
1407
|
+
6. **Cost estimation**: Should we call AWS Pricing API for accurate costs, or use hardcoded estimates?
|
|
1408
|
+
|
|
1409
|
+
## Related Specifications
|
|
1410
|
+
|
|
1411
|
+
- [SPEC-ENHANCED-HEALTH-REPORT.md](./SPEC-ENHANCED-HEALTH-REPORT.md) - Enhanced health report with resource categorization
|
|
1412
|
+
- [FIX-SUMMARY.md](../debug/FIX-SUMMARY.md) - Root cause analysis and fixes for logical ID mapping
|
|
1413
|
+
|
|
1414
|
+
## References
|
|
1415
|
+
|
|
1416
|
+
- User testing with `quo-integrations-dev` stack (11 duplicate resources)
|
|
1417
|
+
- Real-world data documented in `ACTUAL-DATA-SHAPE.md`
|
|
1418
|
+
- AWS EC2 API documentation for resource deletion
|
|
1419
|
+
- CloudFormation resource import documentation
|