@friggframework/devtools 2.0.0-next.47 → 2.0.0-next.48
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/frigg-cli/README.md +1290 -0
- package/frigg-cli/__tests__/unit/commands/build.test.js +279 -0
- package/frigg-cli/__tests__/unit/commands/db-setup.test.js +548 -0
- package/frigg-cli/__tests__/unit/commands/deploy.test.js +320 -0
- package/frigg-cli/__tests__/unit/commands/doctor.test.js +309 -0
- package/frigg-cli/__tests__/unit/commands/install.test.js +400 -0
- package/frigg-cli/__tests__/unit/commands/ui.test.js +346 -0
- package/frigg-cli/__tests__/unit/dependencies.test.js +74 -0
- package/frigg-cli/__tests__/unit/utils/database-validator.test.js +366 -0
- package/frigg-cli/__tests__/unit/utils/error-messages.test.js +304 -0
- package/frigg-cli/__tests__/unit/version-detection.test.js +171 -0
- package/frigg-cli/__tests__/utils/mock-factory.js +270 -0
- package/frigg-cli/__tests__/utils/prisma-mock.js +194 -0
- package/frigg-cli/__tests__/utils/test-fixtures.js +463 -0
- package/frigg-cli/__tests__/utils/test-setup.js +287 -0
- package/frigg-cli/build-command/index.js +66 -0
- package/frigg-cli/db-setup-command/index.js +193 -0
- package/frigg-cli/deploy-command/SPEC-DEPLOY-DRY-RUN.md +981 -0
- package/frigg-cli/deploy-command/index.js +302 -0
- package/frigg-cli/doctor-command/index.js +335 -0
- package/frigg-cli/generate-command/__tests__/generate-command.test.js +301 -0
- package/frigg-cli/generate-command/azure-generator.js +43 -0
- package/frigg-cli/generate-command/gcp-generator.js +47 -0
- package/frigg-cli/generate-command/index.js +332 -0
- package/frigg-cli/generate-command/terraform-generator.js +555 -0
- package/frigg-cli/generate-iam-command.js +118 -0
- package/frigg-cli/index.js +173 -0
- package/frigg-cli/index.test.js +158 -0
- package/frigg-cli/init-command/backend-first-handler.js +756 -0
- package/frigg-cli/init-command/index.js +93 -0
- package/frigg-cli/init-command/template-handler.js +143 -0
- package/frigg-cli/install-command/backend-js.js +33 -0
- package/frigg-cli/install-command/commit-changes.js +16 -0
- package/frigg-cli/install-command/environment-variables.js +127 -0
- package/frigg-cli/install-command/environment-variables.test.js +136 -0
- package/frigg-cli/install-command/index.js +54 -0
- package/frigg-cli/install-command/install-package.js +13 -0
- package/frigg-cli/install-command/integration-file.js +30 -0
- package/frigg-cli/install-command/logger.js +12 -0
- package/frigg-cli/install-command/template.js +90 -0
- package/frigg-cli/install-command/validate-package.js +75 -0
- package/frigg-cli/jest.config.js +124 -0
- package/frigg-cli/package.json +63 -0
- package/frigg-cli/repair-command/index.js +564 -0
- package/frigg-cli/start-command/index.js +149 -0
- package/frigg-cli/start-command/start-command.test.js +297 -0
- package/frigg-cli/test/init-command.test.js +180 -0
- package/frigg-cli/test/npm-registry.test.js +319 -0
- package/frigg-cli/ui-command/index.js +154 -0
- package/frigg-cli/utils/app-resolver.js +319 -0
- package/frigg-cli/utils/backend-path.js +25 -0
- package/frigg-cli/utils/database-validator.js +154 -0
- package/frigg-cli/utils/error-messages.js +257 -0
- package/frigg-cli/utils/npm-registry.js +167 -0
- package/frigg-cli/utils/process-manager.js +199 -0
- package/frigg-cli/utils/repo-detection.js +405 -0
- package/infrastructure/create-frigg-infrastructure.js +125 -12
- package/infrastructure/docs/PRE-DEPLOYMENT-HEALTH-CHECK-SPEC.md +1317 -0
- package/infrastructure/domains/shared/resource-discovery.enhanced.test.js +306 -0
- package/infrastructure/domains/shared/resource-discovery.js +31 -2
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +1 -1
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +109 -5
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +310 -4
- package/infrastructure/domains/shared/validation/plugin-validator.js +187 -0
- package/infrastructure/domains/shared/validation/plugin-validator.test.js +323 -0
- package/infrastructure/infrastructure-composer.js +22 -0
- package/layers/prisma/.build-complete +3 -0
- package/package.json +18 -7
- package/management-ui/package-lock.json +0 -16517
|
@@ -0,0 +1,1317 @@
|
|
|
1
|
+
# Pre-Deployment Health Check Specification
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This specification defines a **pre-deployment health check system** that prevents deployment failures by detecting blocking issues before invoking `serverless deploy`. The system integrates with the existing `frigg doctor` and `frigg repair` infrastructure health check domain, following **Test-Driven Development (TDD)**, **Domain-Driven Design (DDD)**, and **Hexagonal Architecture** patterns.
|
|
6
|
+
|
|
7
|
+
**Problem Statement:**
|
|
8
|
+
Deployments frequently fail due to **orphaned resources** (KMS aliases, VPCs, security groups) that cause CloudFormation `AlreadyExistsException` errors. These failures waste time, create broken stacks in `ROLLBACK_COMPLETE` or `UPDATE_ROLLBACK_COMPLETE` states, and require manual remediation.
|
|
9
|
+
|
|
10
|
+
**Solution:**
|
|
11
|
+
Run a comprehensive pre-deployment health check that:
|
|
12
|
+
1. **Blocks deployment** for issues that will cause CloudFormation to fail
|
|
13
|
+
2. **Shows warnings** for non-blocking issues (drift, property mismatches)
|
|
14
|
+
3. **Suggests remediation** via `frigg repair` commands
|
|
15
|
+
4. **Provides clear exit paths** for users (fix and retry vs. skip warnings)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Architecture
|
|
20
|
+
|
|
21
|
+
### Hexagonal Architecture (Ports & Adapters)
|
|
22
|
+
|
|
23
|
+
Following the existing `packages/devtools/infrastructure/domains/health/` architecture:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
27
|
+
│ CLI LAYER │
|
|
28
|
+
│ frigg deploy --stage prod │
|
|
29
|
+
│ ↓ (before serverless deploy) │
|
|
30
|
+
│ RunPreDeploymentHealthCheck │
|
|
31
|
+
└────────────────────────┬─────────────────────────────────────┘
|
|
32
|
+
│
|
|
33
|
+
┌────────────────────────▼─────────────────────────────────────┐
|
|
34
|
+
│ APPLICATION LAYER (Use Cases) │
|
|
35
|
+
│ │
|
|
36
|
+
│ • RunPreDeploymentHealthCheckUseCase (NEW) │
|
|
37
|
+
│ - Orchestrates pre-deployment checks │
|
|
38
|
+
│ - Categorizes issues as BLOCKING vs WARNING │
|
|
39
|
+
│ - Returns actionable recommendations │
|
|
40
|
+
│ │
|
|
41
|
+
│ • RunHealthCheckUseCase (EXISTING - Post-Deploy) │
|
|
42
|
+
│ - Comprehensive drift/orphan detection │
|
|
43
|
+
│ - Used by both doctor and deploy commands │
|
|
44
|
+
└────────────────────────┬─────────────────────────────────────┘
|
|
45
|
+
│ Uses Ports (Interfaces)
|
|
46
|
+
│
|
|
47
|
+
┌────────────────────────▼─────────────────────────────────────┐
|
|
48
|
+
│ PORT INTERFACES (Boundaries) │
|
|
49
|
+
│ │
|
|
50
|
+
│ • IStackRepository - Stack CRUD, drift detection │
|
|
51
|
+
│ • IResourceDetector - Orphan detection │
|
|
52
|
+
│ • IMismatchAnalyzer - Property drift analysis │
|
|
53
|
+
│ • IHealthScoreCalculator - Health scoring │
|
|
54
|
+
└────────────────────────┬─────────────────────────────────────┘
|
|
55
|
+
│ Implemented by
|
|
56
|
+
│
|
|
57
|
+
┌────────────────────────▼─────────────────────────────────────┐
|
|
58
|
+
│ ADAPTER LAYER (AWS-Specific) │
|
|
59
|
+
│ │
|
|
60
|
+
│ • AWSStackRepository - CloudFormation SDK │
|
|
61
|
+
│ • AWSResourceDetector - AWS SDK (KMS, VPC, etc.) │
|
|
62
|
+
│ • MismatchAnalyzer (Domain) - Property comparison │
|
|
63
|
+
│ • HealthScoreCalculator - Scoring algorithm │
|
|
64
|
+
└──────────────────────────────────────────────────────────────┘
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Domain Model
|
|
68
|
+
|
|
69
|
+
Reuses existing domain entities from `domains/health/domain/entities/`:
|
|
70
|
+
|
|
71
|
+
- **`Issue`** - Represents detected problems (ORPHANED_RESOURCE, MISSING_RESOURCE, PROPERTY_MISMATCH)
|
|
72
|
+
- **`Resource`** - CloudFormation resource with state (IN_STACK, ORPHANED, MISSING, DRIFTED)
|
|
73
|
+
- **`HealthScore`** - Score 0-100 with status (HEALTHY, DEGRADED, CRITICAL)
|
|
74
|
+
- **`StackIdentifier`** - Stack name + region
|
|
75
|
+
- **`PropertyMismatch`** - Property drift details with mutability info
|
|
76
|
+
|
|
77
|
+
**New Value Object:**
|
|
78
|
+
|
|
79
|
+
- **`BlockingCategory`** - Classification of issue severity for pre-deployment
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Blocking vs. Warning Categories
|
|
84
|
+
|
|
85
|
+
### BLOCKING Issues (Prevent Deployment)
|
|
86
|
+
|
|
87
|
+
These issues will cause CloudFormation to fail with errors like `AlreadyExistsException`, `LimitExceededException`, or invalid stack states.
|
|
88
|
+
|
|
89
|
+
#### 1. Stack in Invalid State
|
|
90
|
+
|
|
91
|
+
**CloudFormation Behavior:**
|
|
92
|
+
- Cannot update stacks in certain states
|
|
93
|
+
- Attempting to update will immediately fail with `ValidationError`
|
|
94
|
+
|
|
95
|
+
**Blocking Stack States:**
|
|
96
|
+
```javascript
|
|
97
|
+
const BLOCKING_STACK_STATES = [
|
|
98
|
+
'CREATE_FAILED', // Stack failed to create - must delete first
|
|
99
|
+
'ROLLBACK_COMPLETE', // Orphaned from failed create - must delete first
|
|
100
|
+
'ROLLBACK_FAILED', // Rollback itself failed - needs manual intervention
|
|
101
|
+
'UPDATE_ROLLBACK_FAILED', // Update rollback failed - needs ContinueUpdateRollback
|
|
102
|
+
'DELETE_IN_PROGRESS', // Stack being deleted - wait or cancel
|
|
103
|
+
'DELETE_FAILED', // Delete failed - manual cleanup needed
|
|
104
|
+
];
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Non-Blocking States (OK to deploy):**
|
|
108
|
+
```javascript
|
|
109
|
+
const DEPLOYABLE_STACK_STATES = [
|
|
110
|
+
'CREATE_COMPLETE',
|
|
111
|
+
'UPDATE_COMPLETE',
|
|
112
|
+
'UPDATE_ROLLBACK_COMPLETE', // Can update after rollback cleanup completes
|
|
113
|
+
'IMPORT_COMPLETE',
|
|
114
|
+
'IMPORT_ROLLBACK_COMPLETE',
|
|
115
|
+
];
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Issue Example:**
|
|
119
|
+
```javascript
|
|
120
|
+
Issue.stackInInvalidState({
|
|
121
|
+
resourceType: 'AWS::CloudFormation::Stack',
|
|
122
|
+
resourceId: 'my-app-prod',
|
|
123
|
+
stackStatus: 'ROLLBACK_COMPLETE',
|
|
124
|
+
description: 'Stack is in ROLLBACK_COMPLETE state and cannot be updated. Must be deleted first.',
|
|
125
|
+
resolution: 'Delete stack with: aws cloudformation delete-stack --stack-name my-app-prod',
|
|
126
|
+
canAutoFix: false,
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### 2. Orphaned Resources (AlreadyExistsException)
|
|
131
|
+
|
|
132
|
+
**CloudFormation Behavior:**
|
|
133
|
+
- Resource exists in AWS but not tracked by stack
|
|
134
|
+
- CloudFormation attempts to create → gets `AlreadyExistsException`
|
|
135
|
+
- Deployment fails immediately
|
|
136
|
+
|
|
137
|
+
**Detection Strategy:**
|
|
138
|
+
```javascript
|
|
139
|
+
// Check if resource exists in AWS AND not in current stack resources
|
|
140
|
+
const orphanedResources = await resourceDetector.findOrphanedResources({
|
|
141
|
+
stackIdentifier,
|
|
142
|
+
expectedResources: templateParser.getExpectedResourcesFromTemplate(),
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Blocking Resource Types:**
|
|
147
|
+
```javascript
|
|
148
|
+
const BLOCKING_ORPHAN_TYPES = [
|
|
149
|
+
'AWS::KMS::Alias', // Our recurring issue
|
|
150
|
+
'AWS::KMS::Key', // With Retain policy
|
|
151
|
+
'AWS::EC2::VPC', // Named resources
|
|
152
|
+
'AWS::S3::Bucket', // Named buckets
|
|
153
|
+
'AWS::Lambda::Function', // Named functions
|
|
154
|
+
'AWS::RDS::DBInstance', // Named databases
|
|
155
|
+
'AWS::DynamoDB::Table', // Named tables
|
|
156
|
+
// Any resource with explicit name will block
|
|
157
|
+
];
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Issue Example:**
|
|
161
|
+
```javascript
|
|
162
|
+
Issue.orphanedResource({
|
|
163
|
+
resourceType: 'AWS::KMS::Alias',
|
|
164
|
+
resourceId: 'alias/quo-integrations-dev-frigg-kms',
|
|
165
|
+
description: 'KMS alias exists in AWS but not tracked by CloudFormation stack. Deployment will fail with AlreadyExistsException.',
|
|
166
|
+
resolution: 'Import resource: frigg repair --import AWS::KMS::Alias alias/quo-integrations-dev-frigg-kms',
|
|
167
|
+
canAutoFix: true, // Can use CloudFormation import
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### 3. Resource Quota Exceeded
|
|
172
|
+
|
|
173
|
+
**CloudFormation Behavior:**
|
|
174
|
+
- Attempts to create resource → hits AWS service quota
|
|
175
|
+
- Fails with `LimitExceededException` or service-specific error
|
|
176
|
+
|
|
177
|
+
**Detection Strategy:**
|
|
178
|
+
```javascript
|
|
179
|
+
// Check current usage vs. quota for resources in template
|
|
180
|
+
const quotaChecks = await resourceDetector.checkServiceQuotas({
|
|
181
|
+
stackIdentifier,
|
|
182
|
+
templateResources: templateParser.getResourceCounts(),
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Blocking Quota Issues:**
|
|
187
|
+
```javascript
|
|
188
|
+
const QUOTA_CHECKS = [
|
|
189
|
+
{ resource: 'AWS::EC2::EIP', service: 'ec2', quota: 'L-0263D0A3' }, // 5 EIPs per region
|
|
190
|
+
{ resource: 'AWS::EC2::VPC', service: 'ec2', quota: 'L-F678F1CE' }, // 5 VPCs per region
|
|
191
|
+
{ resource: 'AWS::Lambda::Function', service: 'lambda', quota: 'L-9FEE3D26' }, // 1000 functions
|
|
192
|
+
{ resource: 'AWS::S3::Bucket', service: 's3', quota: 'L-DC2B2D3D' }, // 100 buckets per account
|
|
193
|
+
];
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Issue Example:**
|
|
197
|
+
```javascript
|
|
198
|
+
Issue.quotaExceeded({
|
|
199
|
+
resourceType: 'AWS::EC2::EIP',
|
|
200
|
+
resourceId: 'FriggNATGatewayEIP',
|
|
201
|
+
quotaCode: 'L-0263D0A3',
|
|
202
|
+
currentUsage: 5,
|
|
203
|
+
quota: 5,
|
|
204
|
+
description: 'Account has 5 Elastic IPs allocated (limit: 5). Cannot create FriggNATGatewayEIP.',
|
|
205
|
+
resolution: 'Release unused EIPs or request quota increase: https://console.aws.amazon.com/servicequotas/',
|
|
206
|
+
canAutoFix: false,
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
#### 4. Missing Required Resources (Dependencies)
|
|
211
|
+
|
|
212
|
+
**CloudFormation Behavior:**
|
|
213
|
+
- Template references resource that doesn't exist (e.g., VPC ID, Security Group)
|
|
214
|
+
- Deployment fails with `InvalidParameterValue` or similar
|
|
215
|
+
|
|
216
|
+
**Detection Strategy:**
|
|
217
|
+
```javascript
|
|
218
|
+
// Verify all referenced resources exist
|
|
219
|
+
const missingDependencies = await resourceDetector.checkDependencies({
|
|
220
|
+
templateReferences: templateParser.getResourceReferences(),
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Issue Example:**
|
|
225
|
+
```javascript
|
|
226
|
+
Issue.missingDependency({
|
|
227
|
+
resourceType: 'AWS::EC2::VPC',
|
|
228
|
+
resourceId: 'vpc-nonexistent',
|
|
229
|
+
referencedBy: 'FriggSecurityGroup',
|
|
230
|
+
description: 'Template references VPC vpc-nonexistent which does not exist in AWS.',
|
|
231
|
+
resolution: 'Update template to use correct VPC ID or create VPC first',
|
|
232
|
+
canAutoFix: false,
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
### WARNING Issues (Non-Blocking)
|
|
239
|
+
|
|
240
|
+
These issues indicate drift or suboptimal configuration but won't prevent deployment.
|
|
241
|
+
|
|
242
|
+
#### 1. Property Drift (Mutable Properties)
|
|
243
|
+
|
|
244
|
+
**CloudFormation Behavior:**
|
|
245
|
+
- Resource exists but properties differ from template
|
|
246
|
+
- Deployment succeeds but may update resources
|
|
247
|
+
- **Mutable properties** can be updated without replacement
|
|
248
|
+
|
|
249
|
+
**Issue Example:**
|
|
250
|
+
```javascript
|
|
251
|
+
Issue.propertyMismatch({
|
|
252
|
+
resourceType: 'AWS::Lambda::Function',
|
|
253
|
+
resourceId: 'my-function',
|
|
254
|
+
mismatch: new PropertyMismatch({
|
|
255
|
+
propertyPath: 'Timeout',
|
|
256
|
+
expectedValue: 30,
|
|
257
|
+
actualValue: 60,
|
|
258
|
+
mutability: PropertyMutability.MUTABLE,
|
|
259
|
+
requiresReplacement: false,
|
|
260
|
+
}),
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
#### 2. Health Score < 80 (Degraded)
|
|
265
|
+
|
|
266
|
+
**Issue Example:**
|
|
267
|
+
```javascript
|
|
268
|
+
Issue.degradedHealth({
|
|
269
|
+
resourceType: 'AWS::CloudFormation::Stack',
|
|
270
|
+
resourceId: 'my-app-prod',
|
|
271
|
+
healthScore: 65,
|
|
272
|
+
description: 'Stack health score is 65 (DEGRADED). 3 resources have property drift.',
|
|
273
|
+
resolution: 'Review drift with: frigg doctor my-app-prod --verbose',
|
|
274
|
+
canAutoFix: false,
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
#### 3. Missing Optional Tags
|
|
279
|
+
|
|
280
|
+
**Issue Example:**
|
|
281
|
+
```javascript
|
|
282
|
+
Issue.missingTag({
|
|
283
|
+
resourceType: 'AWS::KMS::Key',
|
|
284
|
+
resourceId: 'key-12345',
|
|
285
|
+
missingTags: ['Environment', 'Owner'],
|
|
286
|
+
description: 'KMS key missing recommended tags: Environment, Owner',
|
|
287
|
+
resolution: 'Add tags via: frigg repair --reconcile',
|
|
288
|
+
canAutoFix: true,
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## CloudFormation Stack Status Reference
|
|
295
|
+
|
|
296
|
+
Complete list of all 23 possible CloudFormation stack statuses:
|
|
297
|
+
|
|
298
|
+
### CREATE Operations
|
|
299
|
+
- `CREATE_IN_PROGRESS` - Stack creation in progress (⏳ wait)
|
|
300
|
+
- `CREATE_FAILED` - Stack creation failed (🚫 BLOCKING - must delete)
|
|
301
|
+
- `CREATE_COMPLETE` - Stack created successfully (✅ deployable)
|
|
302
|
+
|
|
303
|
+
### ROLLBACK Operations
|
|
304
|
+
- `ROLLBACK_IN_PROGRESS` - Rolling back failed create (⏳ wait)
|
|
305
|
+
- `ROLLBACK_FAILED` - Rollback failed (🚫 BLOCKING - manual intervention)
|
|
306
|
+
- `ROLLBACK_COMPLETE` - Rollback completed (🚫 BLOCKING - orphaned stack, must delete)
|
|
307
|
+
|
|
308
|
+
### DELETE Operations
|
|
309
|
+
- `DELETE_IN_PROGRESS` - Stack deletion in progress (⏳ wait)
|
|
310
|
+
- `DELETE_FAILED` - Stack deletion failed (🚫 BLOCKING - manual cleanup)
|
|
311
|
+
- `DELETE_COMPLETE` - Stack deleted (✅ no stack, proceed with create)
|
|
312
|
+
|
|
313
|
+
### UPDATE Operations
|
|
314
|
+
- `UPDATE_IN_PROGRESS` - Stack update in progress (⏳ wait)
|
|
315
|
+
- `UPDATE_COMPLETE_CLEANUP_IN_PROGRESS` - Cleaning up old resources (⏳ wait)
|
|
316
|
+
- `UPDATE_COMPLETE` - Stack updated successfully (✅ deployable)
|
|
317
|
+
- `UPDATE_FAILED` - Stack update failed (⚠️ WARNING - will attempt rollback)
|
|
318
|
+
|
|
319
|
+
### UPDATE_ROLLBACK Operations
|
|
320
|
+
- `UPDATE_ROLLBACK_IN_PROGRESS` - Rolling back failed update (⏳ wait)
|
|
321
|
+
- `UPDATE_ROLLBACK_FAILED` - Update rollback failed (🚫 BLOCKING - needs ContinueUpdateRollback)
|
|
322
|
+
- `UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS` - Cleaning up after rollback (⏳ wait)
|
|
323
|
+
- `UPDATE_ROLLBACK_COMPLETE` - Update rolled back (✅ deployable after cleanup)
|
|
324
|
+
|
|
325
|
+
### IMPORT Operations
|
|
326
|
+
- `IMPORT_IN_PROGRESS` - Resource import in progress (⏳ wait)
|
|
327
|
+
- `IMPORT_COMPLETE` - Resources imported successfully (✅ deployable)
|
|
328
|
+
- `IMPORT_ROLLBACK_IN_PROGRESS` - Rolling back failed import (⏳ wait)
|
|
329
|
+
- `IMPORT_ROLLBACK_FAILED` - Import rollback failed (🚫 BLOCKING)
|
|
330
|
+
- `IMPORT_ROLLBACK_COMPLETE` - Import rolled back (✅ deployable)
|
|
331
|
+
|
|
332
|
+
### REVIEW Operations
|
|
333
|
+
- `REVIEW_IN_PROGRESS` - Change set review (⏳ not a real stack yet)
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Implementation Plan (TDD Approach)
|
|
338
|
+
|
|
339
|
+
### Phase 1: Domain Layer (Test-First)
|
|
340
|
+
|
|
341
|
+
**File:** `domains/health/domain/value-objects/blocking-category.js`
|
|
342
|
+
|
|
343
|
+
```javascript
|
|
344
|
+
/**
|
|
345
|
+
* BlockingCategory Value Object
|
|
346
|
+
*
|
|
347
|
+
* Categorizes issues for pre-deployment health checks
|
|
348
|
+
*/
|
|
349
|
+
class BlockingCategory {
|
|
350
|
+
static CATEGORIES = {
|
|
351
|
+
BLOCKING: 'BLOCKING', // Prevents deployment
|
|
352
|
+
WARNING: 'WARNING', // Non-blocking
|
|
353
|
+
INFO: 'INFO', // Informational
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
static BLOCKING_REASONS = {
|
|
357
|
+
INVALID_STACK_STATE: 'INVALID_STACK_STATE',
|
|
358
|
+
ORPHANED_RESOURCE: 'ORPHANED_RESOURCE',
|
|
359
|
+
QUOTA_EXCEEDED: 'QUOTA_EXCEEDED',
|
|
360
|
+
MISSING_DEPENDENCY: 'MISSING_DEPENDENCY',
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
constructor({ category, reason, description }) {
|
|
364
|
+
this.category = category;
|
|
365
|
+
this.reason = reason;
|
|
366
|
+
this.description = description;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
isBlocking() {
|
|
370
|
+
return this.category === BlockingCategory.CATEGORIES.BLOCKING;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
isWarning() {
|
|
374
|
+
return this.category === BlockingCategory.CATEGORIES.WARNING;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
**Tests:** `__tests__/blocking-category.test.js`
|
|
380
|
+
|
|
381
|
+
```javascript
|
|
382
|
+
describe('BlockingCategory', () => {
|
|
383
|
+
it('should create blocking category for invalid stack state', () => {
|
|
384
|
+
const category = new BlockingCategory({
|
|
385
|
+
category: BlockingCategory.CATEGORIES.BLOCKING,
|
|
386
|
+
reason: BlockingCategory.BLOCKING_REASONS.INVALID_STACK_STATE,
|
|
387
|
+
description: 'Stack in ROLLBACK_COMPLETE',
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
expect(category.isBlocking()).toBe(true);
|
|
391
|
+
expect(category.isWarning()).toBe(false);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should create warning category for property drift', () => {
|
|
395
|
+
const category = new BlockingCategory({
|
|
396
|
+
category: BlockingCategory.CATEGORIES.WARNING,
|
|
397
|
+
reason: 'PROPERTY_DRIFT',
|
|
398
|
+
description: 'Mutable property drift detected',
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
expect(category.isBlocking()).toBe(false);
|
|
402
|
+
expect(category.isWarning()).toBe(true);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Phase 2: Domain Service (Test-First)
|
|
408
|
+
|
|
409
|
+
**File:** `domains/health/domain/services/pre-deployment-categorizer.js`
|
|
410
|
+
|
|
411
|
+
```javascript
|
|
412
|
+
/**
|
|
413
|
+
* PreDeploymentCategorizer Domain Service
|
|
414
|
+
*
|
|
415
|
+
* Categorizes issues for pre-deployment health checks
|
|
416
|
+
*/
|
|
417
|
+
class PreDeploymentCategorizer {
|
|
418
|
+
static BLOCKING_STACK_STATES = [
|
|
419
|
+
'CREATE_FAILED',
|
|
420
|
+
'ROLLBACK_COMPLETE',
|
|
421
|
+
'ROLLBACK_FAILED',
|
|
422
|
+
'UPDATE_ROLLBACK_FAILED',
|
|
423
|
+
'DELETE_IN_PROGRESS',
|
|
424
|
+
'DELETE_FAILED',
|
|
425
|
+
];
|
|
426
|
+
|
|
427
|
+
static BLOCKING_ORPHAN_TYPES = [
|
|
428
|
+
'AWS::KMS::Alias',
|
|
429
|
+
'AWS::KMS::Key',
|
|
430
|
+
'AWS::EC2::VPC',
|
|
431
|
+
'AWS::S3::Bucket',
|
|
432
|
+
'AWS::Lambda::Function',
|
|
433
|
+
'AWS::RDS::DBInstance',
|
|
434
|
+
'AWS::DynamoDB::Table',
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Categorize issue for pre-deployment
|
|
439
|
+
*/
|
|
440
|
+
categorize(issue) {
|
|
441
|
+
// Stack in invalid state - BLOCKING
|
|
442
|
+
if (this._isInvalidStackState(issue)) {
|
|
443
|
+
return new BlockingCategory({
|
|
444
|
+
category: BlockingCategory.CATEGORIES.BLOCKING,
|
|
445
|
+
reason: BlockingCategory.BLOCKING_REASONS.INVALID_STACK_STATE,
|
|
446
|
+
description: `Stack state ${issue.stackStatus} prevents deployment`,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Orphaned resource that will cause AlreadyExistsException - BLOCKING
|
|
451
|
+
if (this._isBlockingOrphan(issue)) {
|
|
452
|
+
return new BlockingCategory({
|
|
453
|
+
category: BlockingCategory.CATEGORIES.BLOCKING,
|
|
454
|
+
reason: BlockingCategory.BLOCKING_REASONS.ORPHANED_RESOURCE,
|
|
455
|
+
description: `Orphaned ${issue.resourceType} will cause AlreadyExistsException`,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Resource quota exceeded - BLOCKING
|
|
460
|
+
if (this._isQuotaExceeded(issue)) {
|
|
461
|
+
return new BlockingCategory({
|
|
462
|
+
category: BlockingCategory.CATEGORIES.BLOCKING,
|
|
463
|
+
reason: BlockingCategory.BLOCKING_REASONS.QUOTA_EXCEEDED,
|
|
464
|
+
description: `${issue.resourceType} quota exceeded`,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Property drift (mutable) - WARNING
|
|
469
|
+
if (issue.isPropertyMismatch() && !issue.propertyMismatch.requiresReplacement()) {
|
|
470
|
+
return new BlockingCategory({
|
|
471
|
+
category: BlockingCategory.CATEGORIES.WARNING,
|
|
472
|
+
reason: 'PROPERTY_DRIFT',
|
|
473
|
+
description: 'Mutable property drift detected',
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Default to INFO
|
|
478
|
+
return new BlockingCategory({
|
|
479
|
+
category: BlockingCategory.CATEGORIES.INFO,
|
|
480
|
+
reason: 'OTHER',
|
|
481
|
+
description: issue.description,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
_isInvalidStackState(issue) {
|
|
486
|
+
return issue.stackStatus &&
|
|
487
|
+
PreDeploymentCategorizer.BLOCKING_STACK_STATES.includes(issue.stackStatus);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
_isBlockingOrphan(issue) {
|
|
491
|
+
return issue.isOrphanedResource() &&
|
|
492
|
+
PreDeploymentCategorizer.BLOCKING_ORPHAN_TYPES.includes(issue.resourceType);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
_isQuotaExceeded(issue) {
|
|
496
|
+
return issue.type === Issue.TYPES.QUOTA_EXCEEDED;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
**Tests:** `__tests__/pre-deployment-categorizer.test.js` (100% coverage)
|
|
502
|
+
|
|
503
|
+
```javascript
|
|
504
|
+
describe('PreDeploymentCategorizer', () => {
|
|
505
|
+
let categorizer;
|
|
506
|
+
|
|
507
|
+
beforeEach(() => {
|
|
508
|
+
categorizer = new PreDeploymentCategorizer();
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
describe('categorize', () => {
|
|
512
|
+
it('should categorize invalid stack state as BLOCKING', () => {
|
|
513
|
+
const issue = {
|
|
514
|
+
type: Issue.TYPES.INVALID_STACK_STATE,
|
|
515
|
+
stackStatus: 'ROLLBACK_COMPLETE',
|
|
516
|
+
description: 'Stack in rollback complete',
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const category = categorizer.categorize(issue);
|
|
520
|
+
|
|
521
|
+
expect(category.isBlocking()).toBe(true);
|
|
522
|
+
expect(category.reason).toBe(BlockingCategory.BLOCKING_REASONS.INVALID_STACK_STATE);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('should categorize orphaned KMS alias as BLOCKING', () => {
|
|
526
|
+
const issue = Issue.orphanedResource({
|
|
527
|
+
resourceType: 'AWS::KMS::Alias',
|
|
528
|
+
resourceId: 'alias/my-app-dev-kms',
|
|
529
|
+
description: 'Orphaned KMS alias',
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const category = categorizer.categorize(issue);
|
|
533
|
+
|
|
534
|
+
expect(category.isBlocking()).toBe(true);
|
|
535
|
+
expect(category.reason).toBe(BlockingCategory.BLOCKING_REASONS.ORPHANED_RESOURCE);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('should categorize property drift as WARNING', () => {
|
|
539
|
+
const mismatch = new PropertyMismatch({
|
|
540
|
+
propertyPath: 'Timeout',
|
|
541
|
+
expectedValue: 30,
|
|
542
|
+
actualValue: 60,
|
|
543
|
+
mutability: PropertyMutability.MUTABLE,
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const issue = Issue.propertyMismatch({
|
|
547
|
+
resourceType: 'AWS::Lambda::Function',
|
|
548
|
+
resourceId: 'my-function',
|
|
549
|
+
mismatch,
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
const category = categorizer.categorize(issue);
|
|
553
|
+
|
|
554
|
+
expect(category.isWarning()).toBe(true);
|
|
555
|
+
expect(category.reason).toBe('PROPERTY_DRIFT');
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('should categorize quota exceeded as BLOCKING', () => {
|
|
559
|
+
const issue = {
|
|
560
|
+
type: Issue.TYPES.QUOTA_EXCEEDED,
|
|
561
|
+
resourceType: 'AWS::EC2::EIP',
|
|
562
|
+
description: 'EIP quota exceeded',
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
const category = categorizer.categorize(issue);
|
|
566
|
+
|
|
567
|
+
expect(category.isBlocking()).toBe(true);
|
|
568
|
+
expect(category.reason).toBe(BlockingCategory.BLOCKING_REASONS.QUOTA_EXCEEDED);
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
### Phase 3: Use Case (Test-First)
|
|
575
|
+
|
|
576
|
+
**File:** `domains/health/application/use-cases/run-pre-deployment-health-check-use-case.js`
|
|
577
|
+
|
|
578
|
+
```javascript
|
|
579
|
+
/**
|
|
580
|
+
* RunPreDeploymentHealthCheckUseCase
|
|
581
|
+
*
|
|
582
|
+
* Application Layer - Use Case
|
|
583
|
+
*
|
|
584
|
+
* Orchestrates pre-deployment health checks to prevent deployment failures
|
|
585
|
+
*/
|
|
586
|
+
class RunPreDeploymentHealthCheckUseCase {
|
|
587
|
+
constructor({
|
|
588
|
+
stackRepository,
|
|
589
|
+
resourceDetector,
|
|
590
|
+
mismatchAnalyzer,
|
|
591
|
+
healthScoreCalculator,
|
|
592
|
+
preDeploymentCategorizer,
|
|
593
|
+
}) {
|
|
594
|
+
this.stackRepository = stackRepository;
|
|
595
|
+
this.resourceDetector = resourceDetector;
|
|
596
|
+
this.mismatchAnalyzer = mismatchAnalyzer;
|
|
597
|
+
this.healthScoreCalculator = healthScoreCalculator;
|
|
598
|
+
this.categorizer = preDeploymentCategorizer;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Execute pre-deployment health check
|
|
603
|
+
*
|
|
604
|
+
* @param {Object} params
|
|
605
|
+
* @param {StackIdentifier} params.stackIdentifier - Stack to check
|
|
606
|
+
* @param {string} params.templatePath - Path to generated serverless.yml
|
|
607
|
+
* @param {Function} params.onProgress - Progress callback
|
|
608
|
+
* @returns {Promise<PreDeploymentHealthReport>}
|
|
609
|
+
*/
|
|
610
|
+
async execute({ stackIdentifier, templatePath, onProgress }) {
|
|
611
|
+
const progress = (step, message) => onProgress?.(step, message);
|
|
612
|
+
|
|
613
|
+
// 1. Check if stack exists
|
|
614
|
+
progress('📋 Step 1/6:', 'Checking stack status...');
|
|
615
|
+
let stackExists = true;
|
|
616
|
+
let stack = null;
|
|
617
|
+
|
|
618
|
+
try {
|
|
619
|
+
stack = await this.stackRepository.getStack(stackIdentifier);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
if (error.code === 'ValidationError') {
|
|
622
|
+
stackExists = false;
|
|
623
|
+
progress(' Stack does not exist (first deployment)');
|
|
624
|
+
} else {
|
|
625
|
+
throw error;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const issues = [];
|
|
630
|
+
|
|
631
|
+
// 2. If stack exists, check if it's in deployable state
|
|
632
|
+
if (stackExists) {
|
|
633
|
+
progress('🔍 Step 2/6:', 'Validating stack state...');
|
|
634
|
+
|
|
635
|
+
if (!this._isDeployableState(stack.stackStatus)) {
|
|
636
|
+
issues.push({
|
|
637
|
+
type: Issue.TYPES.INVALID_STACK_STATE,
|
|
638
|
+
severity: Issue.SEVERITIES.CRITICAL,
|
|
639
|
+
resourceType: 'AWS::CloudFormation::Stack',
|
|
640
|
+
resourceId: stackIdentifier.stackName,
|
|
641
|
+
stackStatus: stack.stackStatus,
|
|
642
|
+
description: `Stack is in ${stack.stackStatus} state and cannot be updated`,
|
|
643
|
+
resolution: this._getStackStateResolution(stack.stackStatus),
|
|
644
|
+
canAutoFix: false,
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
} else {
|
|
648
|
+
progress('⏭️ Step 2/6:', 'Skipping state validation (new stack)');
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// 3. Parse expected resources from template
|
|
652
|
+
progress('📄 Step 3/6:', 'Parsing deployment template...');
|
|
653
|
+
const templateParser = new TemplateParser();
|
|
654
|
+
const expectedResources = await templateParser.parseTemplate(templatePath);
|
|
655
|
+
|
|
656
|
+
// 4. Check for orphaned resources
|
|
657
|
+
progress('🔎 Step 4/6:', 'Checking for orphaned resources...');
|
|
658
|
+
const orphanedResources = await this.resourceDetector.findOrphanedResources({
|
|
659
|
+
stackIdentifier,
|
|
660
|
+
expectedResources,
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
orphanedResources.forEach(orphan => {
|
|
664
|
+
issues.push(Issue.orphanedResource({
|
|
665
|
+
resourceType: orphan.resourceType,
|
|
666
|
+
resourceId: orphan.physicalId,
|
|
667
|
+
description: `Resource exists in AWS but not tracked by stack: ${orphan.physicalId}`,
|
|
668
|
+
}));
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// 5. Check resource quotas
|
|
672
|
+
progress('📊 Step 5/6:', 'Checking service quotas...');
|
|
673
|
+
const quotaIssues = await this.resourceDetector.checkServiceQuotas({
|
|
674
|
+
stackIdentifier,
|
|
675
|
+
expectedResources,
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
issues.push(...quotaIssues);
|
|
679
|
+
|
|
680
|
+
// 6. Categorize all issues
|
|
681
|
+
progress('🏷️ Step 6/6:', 'Categorizing issues...');
|
|
682
|
+
const categorizedIssues = issues.map(issue => ({
|
|
683
|
+
issue,
|
|
684
|
+
category: this.categorizer.categorize(issue),
|
|
685
|
+
}));
|
|
686
|
+
|
|
687
|
+
// Separate blocking and warning issues
|
|
688
|
+
const blockingIssues = categorizedIssues.filter(i => i.category.isBlocking());
|
|
689
|
+
const warningIssues = categorizedIssues.filter(i => i.category.isWarning());
|
|
690
|
+
|
|
691
|
+
return {
|
|
692
|
+
canDeploy: blockingIssues.length === 0,
|
|
693
|
+
blockingIssues,
|
|
694
|
+
warningIssues,
|
|
695
|
+
stackExists,
|
|
696
|
+
stackStatus: stack?.stackStatus,
|
|
697
|
+
summary: {
|
|
698
|
+
total: issues.length,
|
|
699
|
+
blocking: blockingIssues.length,
|
|
700
|
+
warnings: warningIssues.length,
|
|
701
|
+
},
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
_isDeployableState(stackStatus) {
|
|
706
|
+
const DEPLOYABLE_STATES = [
|
|
707
|
+
'CREATE_COMPLETE',
|
|
708
|
+
'UPDATE_COMPLETE',
|
|
709
|
+
'UPDATE_ROLLBACK_COMPLETE',
|
|
710
|
+
'IMPORT_COMPLETE',
|
|
711
|
+
'IMPORT_ROLLBACK_COMPLETE',
|
|
712
|
+
];
|
|
713
|
+
|
|
714
|
+
return DEPLOYABLE_STATES.includes(stackStatus);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
_getStackStateResolution(stackStatus) {
|
|
718
|
+
const resolutions = {
|
|
719
|
+
'ROLLBACK_COMPLETE': 'Delete stack with: aws cloudformation delete-stack --stack-name ${stackName}',
|
|
720
|
+
'CREATE_FAILED': 'Delete stack with: aws cloudformation delete-stack --stack-name ${stackName}',
|
|
721
|
+
'UPDATE_ROLLBACK_FAILED': 'Continue rollback with: aws cloudformation continue-update-rollback --stack-name ${stackName}',
|
|
722
|
+
'DELETE_FAILED': 'Force delete with: aws cloudformation delete-stack --stack-name ${stackName} --force',
|
|
723
|
+
'DELETE_IN_PROGRESS': 'Wait for deletion to complete',
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
return resolutions[stackStatus] || 'Manual intervention required';
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
**Tests:** `__tests__/run-pre-deployment-health-check-use-case.test.js` (100% coverage)
|
|
732
|
+
|
|
733
|
+
```javascript
|
|
734
|
+
describe('RunPreDeploymentHealthCheckUseCase', () => {
|
|
735
|
+
let useCase;
|
|
736
|
+
let mockStackRepository;
|
|
737
|
+
let mockResourceDetector;
|
|
738
|
+
let mockCategorizer;
|
|
739
|
+
|
|
740
|
+
beforeEach(() => {
|
|
741
|
+
mockStackRepository = {
|
|
742
|
+
getStack: jest.fn(),
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
mockResourceDetector = {
|
|
746
|
+
findOrphanedResources: jest.fn(),
|
|
747
|
+
checkServiceQuotas: jest.fn(),
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
mockCategorizer = {
|
|
751
|
+
categorize: jest.fn(),
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
useCase = new RunPreDeploymentHealthCheckUseCase({
|
|
755
|
+
stackRepository: mockStackRepository,
|
|
756
|
+
resourceDetector: mockResourceDetector,
|
|
757
|
+
preDeploymentCategorizer: mockCategorizer,
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
describe('execute', () => {
|
|
762
|
+
it('should allow deployment when no blocking issues found', async () => {
|
|
763
|
+
mockStackRepository.getStack.mockResolvedValue({
|
|
764
|
+
stackName: 'my-app-prod',
|
|
765
|
+
stackStatus: 'UPDATE_COMPLETE',
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
mockResourceDetector.findOrphanedResources.mockResolvedValue([]);
|
|
769
|
+
mockResourceDetector.checkServiceQuotas.mockResolvedValue([]);
|
|
770
|
+
|
|
771
|
+
const result = await useCase.execute({
|
|
772
|
+
stackIdentifier: new StackIdentifier({ stackName: 'my-app-prod', region: 'us-east-1' }),
|
|
773
|
+
templatePath: '/path/to/infrastructure.yml',
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
expect(result.canDeploy).toBe(true);
|
|
777
|
+
expect(result.blockingIssues).toHaveLength(0);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
it('should block deployment when stack in ROLLBACK_COMPLETE', async () => {
|
|
781
|
+
mockStackRepository.getStack.mockResolvedValue({
|
|
782
|
+
stackName: 'my-app-prod',
|
|
783
|
+
stackStatus: 'ROLLBACK_COMPLETE',
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
mockResourceDetector.findOrphanedResources.mockResolvedValue([]);
|
|
787
|
+
mockResourceDetector.checkServiceQuotas.mockResolvedValue([]);
|
|
788
|
+
|
|
789
|
+
mockCategorizer.categorize.mockReturnValue(
|
|
790
|
+
new BlockingCategory({
|
|
791
|
+
category: BlockingCategory.CATEGORIES.BLOCKING,
|
|
792
|
+
reason: BlockingCategory.BLOCKING_REASONS.INVALID_STACK_STATE,
|
|
793
|
+
description: 'Stack in ROLLBACK_COMPLETE',
|
|
794
|
+
})
|
|
795
|
+
);
|
|
796
|
+
|
|
797
|
+
const result = await useCase.execute({
|
|
798
|
+
stackIdentifier: new StackIdentifier({ stackName: 'my-app-prod', region: 'us-east-1' }),
|
|
799
|
+
templatePath: '/path/to/infrastructure.yml',
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
expect(result.canDeploy).toBe(false);
|
|
803
|
+
expect(result.blockingIssues).toHaveLength(1);
|
|
804
|
+
expect(result.blockingIssues[0].issue.stackStatus).toBe('ROLLBACK_COMPLETE');
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
it('should block deployment when orphaned KMS alias found', async () => {
|
|
808
|
+
mockStackRepository.getStack.mockResolvedValue({
|
|
809
|
+
stackName: 'my-app-prod',
|
|
810
|
+
stackStatus: 'UPDATE_COMPLETE',
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
mockResourceDetector.findOrphanedResources.mockResolvedValue([
|
|
814
|
+
{
|
|
815
|
+
resourceType: 'AWS::KMS::Alias',
|
|
816
|
+
physicalId: 'alias/my-app-prod-kms',
|
|
817
|
+
},
|
|
818
|
+
]);
|
|
819
|
+
|
|
820
|
+
mockResourceDetector.checkServiceQuotas.mockResolvedValue([]);
|
|
821
|
+
|
|
822
|
+
mockCategorizer.categorize.mockReturnValue(
|
|
823
|
+
new BlockingCategory({
|
|
824
|
+
category: BlockingCategory.CATEGORIES.BLOCKING,
|
|
825
|
+
reason: BlockingCategory.BLOCKING_REASONS.ORPHANED_RESOURCE,
|
|
826
|
+
description: 'Orphaned KMS alias',
|
|
827
|
+
})
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
const result = await useCase.execute({
|
|
831
|
+
stackIdentifier: new StackIdentifier({ stackName: 'my-app-prod', region: 'us-east-1' }),
|
|
832
|
+
templatePath: '/path/to/infrastructure.yml',
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
expect(result.canDeploy).toBe(false);
|
|
836
|
+
expect(result.blockingIssues).toHaveLength(1);
|
|
837
|
+
expect(result.blockingIssues[0].issue.resourceType).toBe('AWS::KMS::Alias');
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
it('should allow deployment for first-time stack creation', async () => {
|
|
841
|
+
mockStackRepository.getStack.mockRejectedValue({
|
|
842
|
+
code: 'ValidationError',
|
|
843
|
+
message: 'Stack does not exist',
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
mockResourceDetector.findOrphanedResources.mockResolvedValue([]);
|
|
847
|
+
mockResourceDetector.checkServiceQuotas.mockResolvedValue([]);
|
|
848
|
+
|
|
849
|
+
const result = await useCase.execute({
|
|
850
|
+
stackIdentifier: new StackIdentifier({ stackName: 'my-app-prod', region: 'us-east-1' }),
|
|
851
|
+
templatePath: '/path/to/infrastructure.yml',
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
expect(result.canDeploy).toBe(true);
|
|
855
|
+
expect(result.stackExists).toBe(false);
|
|
856
|
+
});
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
### Phase 4: CLI Integration
|
|
862
|
+
|
|
863
|
+
**File:** `packages/frigg-cli/deploy-command/index.js` (UPDATE EXISTING)
|
|
864
|
+
|
|
865
|
+
```javascript
|
|
866
|
+
/**
|
|
867
|
+
* Pre-deployment health check integration
|
|
868
|
+
* Runs before serverless deploy to catch blocking issues
|
|
869
|
+
*/
|
|
870
|
+
async function runPreDeploymentHealthCheck(stackName, options) {
|
|
871
|
+
console.log('\n' + '═'.repeat(80));
|
|
872
|
+
console.log('Running pre-deployment health check...');
|
|
873
|
+
console.log('═'.repeat(80));
|
|
874
|
+
|
|
875
|
+
try {
|
|
876
|
+
const { RunPreDeploymentHealthCheckUseCase } = require('@friggframework/devtools/infrastructure/domains/health/application/use-cases/run-pre-deployment-health-check-use-case');
|
|
877
|
+
const { AWSStackRepository } = require('@friggframework/devtools/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository');
|
|
878
|
+
const { AWSResourceDetector } = require('@friggframework/devtools/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector');
|
|
879
|
+
const { PreDeploymentCategorizer } = require('@friggframework/devtools/infrastructure/domains/health/domain/services/pre-deployment-categorizer');
|
|
880
|
+
const StackIdentifier = require('@friggframework/devtools/infrastructure/domains/health/domain/value-objects/stack-identifier');
|
|
881
|
+
|
|
882
|
+
const stackRepository = new AWSStackRepository({ region: options.region });
|
|
883
|
+
const resourceDetector = new AWSResourceDetector({ region: options.region });
|
|
884
|
+
const categorizer = new PreDeploymentCategorizer();
|
|
885
|
+
|
|
886
|
+
const useCase = new RunPreDeploymentHealthCheckUseCase({
|
|
887
|
+
stackRepository,
|
|
888
|
+
resourceDetector,
|
|
889
|
+
preDeploymentCategorizer: categorizer,
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
const result = await useCase.execute({
|
|
893
|
+
stackIdentifier: new StackIdentifier({
|
|
894
|
+
stackName,
|
|
895
|
+
region: options.region || 'us-east-1',
|
|
896
|
+
}),
|
|
897
|
+
templatePath: path.join(process.cwd(), PATHS.INFRASTRUCTURE),
|
|
898
|
+
onProgress: (step, message) => console.log(step, message),
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
// Display results
|
|
902
|
+
console.log('\n📊 Pre-Deployment Health Check Results:');
|
|
903
|
+
console.log(` Total issues: ${result.summary.total}`);
|
|
904
|
+
console.log(` 🚫 Blocking: ${result.summary.blocking}`);
|
|
905
|
+
console.log(` ⚠️ Warnings: ${result.summary.warnings}`);
|
|
906
|
+
|
|
907
|
+
// Show blocking issues
|
|
908
|
+
if (result.blockingIssues.length > 0) {
|
|
909
|
+
console.log('\n🚫 BLOCKING ISSUES (deployment will fail):');
|
|
910
|
+
result.blockingIssues.forEach((item, index) => {
|
|
911
|
+
console.log(`\n ${index + 1}. ${item.issue.description}`);
|
|
912
|
+
console.log(` Type: ${item.issue.resourceType}`);
|
|
913
|
+
console.log(` Resolution: ${item.issue.resolution}`);
|
|
914
|
+
if (item.issue.canAutoFix) {
|
|
915
|
+
console.log(` ✓ Can be auto-fixed with: frigg repair --import`);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
console.log('\n✗ Deployment blocked due to critical issues');
|
|
920
|
+
console.log(' Fix these issues and run deploy again');
|
|
921
|
+
return false; // Block deployment
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Show warnings
|
|
925
|
+
if (result.warningIssues.length > 0) {
|
|
926
|
+
console.log('\n⚠️ WARNINGS (non-blocking):');
|
|
927
|
+
result.warningIssues.forEach((item, index) => {
|
|
928
|
+
console.log(`\n ${index + 1}. ${item.issue.description}`);
|
|
929
|
+
console.log(` Type: ${item.issue.resourceType}`);
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
console.log('\n⚠️ Warnings detected but deployment can proceed');
|
|
933
|
+
console.log(' Run "frigg doctor" after deployment to address warnings');
|
|
934
|
+
} else {
|
|
935
|
+
console.log('\n✓ No issues detected - deployment can proceed');
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
return true; // Allow deployment
|
|
939
|
+
|
|
940
|
+
} catch (error) {
|
|
941
|
+
console.log(`\n⚠️ Pre-deployment health check failed: ${error.message}`);
|
|
942
|
+
if (options.verbose) {
|
|
943
|
+
console.error(error.stack);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// On error, allow deployment (fail open)
|
|
947
|
+
console.log(' Proceeding with deployment...');
|
|
948
|
+
return true;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
async function deployCommand(options) {
|
|
953
|
+
console.log('Deploying the serverless application...');
|
|
954
|
+
|
|
955
|
+
const appDefinition = loadAppDefinition();
|
|
956
|
+
const stackName = getStackName(appDefinition, options);
|
|
957
|
+
|
|
958
|
+
// NEW: Run pre-deployment health check (unless --skip-pre-check)
|
|
959
|
+
if (!options.skipPreCheck && stackName) {
|
|
960
|
+
const canDeploy = await runPreDeploymentHealthCheck(stackName, options);
|
|
961
|
+
|
|
962
|
+
if (!canDeploy) {
|
|
963
|
+
console.error('\n✗ Deployment aborted due to blocking issues');
|
|
964
|
+
process.exit(1);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const environment = validateAndBuildEnvironment(appDefinition, options);
|
|
969
|
+
|
|
970
|
+
// Execute deployment
|
|
971
|
+
const exitCode = await executeServerlessDeployment(environment, options);
|
|
972
|
+
|
|
973
|
+
// ... rest of deploy command (post-deployment health check)
|
|
974
|
+
}
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
---
|
|
978
|
+
|
|
979
|
+
## CLI User Experience
|
|
980
|
+
|
|
981
|
+
### Scenario 1: Orphaned KMS Alias (BLOCKING)
|
|
982
|
+
|
|
983
|
+
```bash
|
|
984
|
+
$ frigg deploy --stage dev
|
|
985
|
+
|
|
986
|
+
Deploying the serverless application...
|
|
987
|
+
|
|
988
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
989
|
+
Running pre-deployment health check...
|
|
990
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
991
|
+
📋 Step 1/6: Checking stack status...
|
|
992
|
+
Stack exists: quo-integrations-dev (UPDATE_COMPLETE)
|
|
993
|
+
🔍 Step 2/6: Validating stack state...
|
|
994
|
+
✓ Stack is in deployable state
|
|
995
|
+
📄 Step 3/6: Parsing deployment template...
|
|
996
|
+
Found 47 resources in template
|
|
997
|
+
🔎 Step 4/6: Checking for orphaned resources...
|
|
998
|
+
⚠️ Found 1 orphaned resource
|
|
999
|
+
📊 Step 5/6: Checking service quotas...
|
|
1000
|
+
✓ All quotas within limits
|
|
1001
|
+
🏷️ Step 6/6: Categorizing issues...
|
|
1002
|
+
|
|
1003
|
+
📊 Pre-Deployment Health Check Results:
|
|
1004
|
+
Total issues: 1
|
|
1005
|
+
🚫 Blocking: 1
|
|
1006
|
+
⚠️ Warnings: 0
|
|
1007
|
+
|
|
1008
|
+
🚫 BLOCKING ISSUES (deployment will fail):
|
|
1009
|
+
|
|
1010
|
+
1. KMS alias exists in AWS but not tracked by CloudFormation stack. Deployment will fail with AlreadyExistsException.
|
|
1011
|
+
Type: AWS::KMS::Alias
|
|
1012
|
+
Resource: alias/quo-integrations-dev-frigg-kms
|
|
1013
|
+
Resolution: Import resource with: frigg repair --import AWS::KMS::Alias alias/quo-integrations-dev-frigg-kms
|
|
1014
|
+
✓ Can be auto-fixed with: frigg repair --import
|
|
1015
|
+
|
|
1016
|
+
✗ Deployment blocked due to critical issues
|
|
1017
|
+
Fix these issues and run deploy again
|
|
1018
|
+
|
|
1019
|
+
$ frigg repair --import AWS::KMS::Alias alias/quo-integrations-dev-frigg-kms
|
|
1020
|
+
✓ Resource imported successfully
|
|
1021
|
+
|
|
1022
|
+
$ frigg deploy --stage dev
|
|
1023
|
+
✓ Pre-deployment health check passed
|
|
1024
|
+
🚀 Deploying serverless application...
|
|
1025
|
+
✓ Deployment completed successfully!
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
### Scenario 2: Stack in ROLLBACK_COMPLETE (BLOCKING)
|
|
1029
|
+
|
|
1030
|
+
```bash
|
|
1031
|
+
$ frigg deploy --stage dev
|
|
1032
|
+
|
|
1033
|
+
Deploying the serverless application...
|
|
1034
|
+
|
|
1035
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
1036
|
+
Running pre-deployment health check...
|
|
1037
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
1038
|
+
📋 Step 1/6: Checking stack status...
|
|
1039
|
+
Stack exists: my-app-dev (ROLLBACK_COMPLETE)
|
|
1040
|
+
🔍 Step 2/6: Validating stack state...
|
|
1041
|
+
✗ Stack in invalid state
|
|
1042
|
+
|
|
1043
|
+
📊 Pre-Deployment Health Check Results:
|
|
1044
|
+
Total issues: 1
|
|
1045
|
+
🚫 Blocking: 1
|
|
1046
|
+
⚠️ Warnings: 0
|
|
1047
|
+
|
|
1048
|
+
🚫 BLOCKING ISSUES (deployment will fail):
|
|
1049
|
+
|
|
1050
|
+
1. Stack is in ROLLBACK_COMPLETE state and cannot be updated. This is an orphaned stack from a failed creation.
|
|
1051
|
+
Type: AWS::CloudFormation::Stack
|
|
1052
|
+
Resource: my-app-dev
|
|
1053
|
+
Resolution: Delete stack with: aws cloudformation delete-stack --stack-name my-app-dev
|
|
1054
|
+
|
|
1055
|
+
✗ Deployment blocked due to critical issues
|
|
1056
|
+
Fix these issues and run deploy again
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
### Scenario 3: Property Drift (WARNING)
|
|
1060
|
+
|
|
1061
|
+
```bash
|
|
1062
|
+
$ frigg deploy --stage prod
|
|
1063
|
+
|
|
1064
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
1065
|
+
Running pre-deployment health check...
|
|
1066
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
1067
|
+
📋 Step 1/6: Checking stack status...
|
|
1068
|
+
🔍 Step 2/6: Validating stack state...
|
|
1069
|
+
📄 Step 3/6: Parsing deployment template...
|
|
1070
|
+
🔎 Step 4/6: Checking for orphaned resources...
|
|
1071
|
+
📊 Step 5/6: Checking service quotas...
|
|
1072
|
+
🏷️ Step 6/6: Categorizing issues...
|
|
1073
|
+
|
|
1074
|
+
📊 Pre-Deployment Health Check Results:
|
|
1075
|
+
Total issues: 3
|
|
1076
|
+
🚫 Blocking: 0
|
|
1077
|
+
⚠️ Warnings: 3
|
|
1078
|
+
|
|
1079
|
+
⚠️ WARNINGS (non-blocking):
|
|
1080
|
+
|
|
1081
|
+
1. Property mismatch: Timeout (expected: 30, actual: 60)
|
|
1082
|
+
Type: AWS::Lambda::Function
|
|
1083
|
+
Resource: ProcessOrderFunction
|
|
1084
|
+
|
|
1085
|
+
2. Property mismatch: MemorySize (expected: 1024, actual: 512)
|
|
1086
|
+
Type: AWS::Lambda::Function
|
|
1087
|
+
Resource: SyncDataFunction
|
|
1088
|
+
|
|
1089
|
+
3. Missing recommended tags: Environment, Owner
|
|
1090
|
+
Type: AWS::KMS::Key
|
|
1091
|
+
|
|
1092
|
+
⚠️ Warnings detected but deployment can proceed
|
|
1093
|
+
Run "frigg doctor" after deployment to address warnings
|
|
1094
|
+
|
|
1095
|
+
✓ Pre-deployment health check passed
|
|
1096
|
+
🚀 Deploying serverless application...
|
|
1097
|
+
✓ Deployment completed successfully!
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
---
|
|
1101
|
+
|
|
1102
|
+
## Testing Strategy
|
|
1103
|
+
|
|
1104
|
+
### Unit Tests (100% Coverage)
|
|
1105
|
+
|
|
1106
|
+
**Domain Layer:**
|
|
1107
|
+
- `BlockingCategory` value object
|
|
1108
|
+
- `PreDeploymentCategorizer` domain service
|
|
1109
|
+
|
|
1110
|
+
**Application Layer:**
|
|
1111
|
+
- `RunPreDeploymentHealthCheckUseCase`
|
|
1112
|
+
|
|
1113
|
+
**Test Coverage Requirements:**
|
|
1114
|
+
- All blocking stack states tested
|
|
1115
|
+
- All orphan resource types tested
|
|
1116
|
+
- All quota checks tested
|
|
1117
|
+
- All warning scenarios tested
|
|
1118
|
+
- Error handling and edge cases
|
|
1119
|
+
|
|
1120
|
+
### Integration Tests
|
|
1121
|
+
|
|
1122
|
+
**File:** `__tests__/integration/pre-deployment-health-check.integration.test.js`
|
|
1123
|
+
|
|
1124
|
+
```javascript
|
|
1125
|
+
describe('Pre-Deployment Health Check Integration', () => {
|
|
1126
|
+
it('should detect orphaned KMS alias in AWS', async () => {
|
|
1127
|
+
// Requires real AWS credentials and stack
|
|
1128
|
+
// Mock: createOrphanedKMSAlias()
|
|
1129
|
+
// Run: health check
|
|
1130
|
+
// Assert: blocking issue detected
|
|
1131
|
+
// Cleanup: deleteOrphanedKMSAlias()
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
it('should detect stack in ROLLBACK_COMPLETE', async () => {
|
|
1135
|
+
// Mock: stackInRollbackComplete()
|
|
1136
|
+
// Run: health check
|
|
1137
|
+
// Assert: blocking issue detected with correct resolution
|
|
1138
|
+
});
|
|
1139
|
+
});
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
### End-to-End Tests
|
|
1143
|
+
|
|
1144
|
+
**File:** `__tests__/e2e/deploy-with-health-check.e2e.test.js`
|
|
1145
|
+
|
|
1146
|
+
```javascript
|
|
1147
|
+
describe('Deploy with Pre-Deployment Health Check E2E', () => {
|
|
1148
|
+
it('should block deployment when orphaned resource found', async () => {
|
|
1149
|
+
// Setup: create orphaned KMS alias
|
|
1150
|
+
// Run: frigg deploy
|
|
1151
|
+
// Assert: deployment blocked
|
|
1152
|
+
// Run: frigg repair --import
|
|
1153
|
+
// Run: frigg deploy
|
|
1154
|
+
// Assert: deployment succeeds
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
---
|
|
1160
|
+
|
|
1161
|
+
## Configuration Options
|
|
1162
|
+
|
|
1163
|
+
### CLI Flags
|
|
1164
|
+
|
|
1165
|
+
```bash
|
|
1166
|
+
frigg deploy --stage prod # Run with pre-deployment check
|
|
1167
|
+
frigg deploy --stage prod --skip-pre-check # Skip pre-deployment check
|
|
1168
|
+
frigg deploy --stage prod --skip-doctor # Skip post-deployment check
|
|
1169
|
+
frigg deploy --stage prod --skip-all-checks # Skip both checks
|
|
1170
|
+
frigg deploy --stage prod --verbose # Show detailed progress
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1173
|
+
### Environment Variables
|
|
1174
|
+
|
|
1175
|
+
```bash
|
|
1176
|
+
FRIGG_SKIP_PRE_DEPLOYMENT_CHECK=true # Disable pre-deployment checks
|
|
1177
|
+
FRIGG_PRE_DEPLOYMENT_CHECK_TIMEOUT=60 # Timeout in seconds (default: 30)
|
|
1178
|
+
```
|
|
1179
|
+
|
|
1180
|
+
---
|
|
1181
|
+
|
|
1182
|
+
## Performance Considerations
|
|
1183
|
+
|
|
1184
|
+
### Expected Check Duration
|
|
1185
|
+
|
|
1186
|
+
- **Stack state check**: < 1 second (single API call)
|
|
1187
|
+
- **Orphan detection**: 2-5 seconds (depends on resources in template)
|
|
1188
|
+
- **Quota checks**: 1-2 seconds per resource type
|
|
1189
|
+
- **Total**: 5-10 seconds for typical stack
|
|
1190
|
+
|
|
1191
|
+
### Optimization Strategies
|
|
1192
|
+
|
|
1193
|
+
1. **Parallel API calls** - Check quotas and orphans concurrently
|
|
1194
|
+
2. **Template parsing cache** - Cache parsed template structure
|
|
1195
|
+
3. **Selective checks** - Only check resource types present in template
|
|
1196
|
+
4. **Fail fast** - Return immediately on first blocking issue found
|
|
1197
|
+
|
|
1198
|
+
---
|
|
1199
|
+
|
|
1200
|
+
## Backward Compatibility
|
|
1201
|
+
|
|
1202
|
+
### Existing Behavior Preserved
|
|
1203
|
+
|
|
1204
|
+
- Post-deployment health check (`frigg doctor`) unchanged
|
|
1205
|
+
- `frigg repair` commands unchanged
|
|
1206
|
+
- Existing health check domain entities/services reused
|
|
1207
|
+
|
|
1208
|
+
### Migration Path
|
|
1209
|
+
|
|
1210
|
+
1. **Phase 1**: Pre-deployment check opt-in (`--pre-check` flag)
|
|
1211
|
+
2. **Phase 2**: Pre-deployment check default, opt-out (`--skip-pre-check`)
|
|
1212
|
+
3. **Phase 3**: Remove opt-out after proven stable
|
|
1213
|
+
|
|
1214
|
+
---
|
|
1215
|
+
|
|
1216
|
+
## Success Criteria
|
|
1217
|
+
|
|
1218
|
+
### Functional Requirements
|
|
1219
|
+
|
|
1220
|
+
- ✅ Detects all blocking stack states before deployment
|
|
1221
|
+
- ✅ Detects orphaned resources that cause AlreadyExistsException
|
|
1222
|
+
- ✅ Detects resource quota issues before deployment
|
|
1223
|
+
- ✅ Categorizes issues as BLOCKING vs WARNING correctly
|
|
1224
|
+
- ✅ Provides actionable remediation commands
|
|
1225
|
+
- ✅ Integrates seamlessly with existing deploy command
|
|
1226
|
+
|
|
1227
|
+
### Non-Functional Requirements
|
|
1228
|
+
|
|
1229
|
+
- ✅ 100% test coverage (TDD approach)
|
|
1230
|
+
- ✅ < 10 second check duration for typical stack
|
|
1231
|
+
- ✅ Follows hexagonal architecture patterns
|
|
1232
|
+
- ✅ Reuses existing health check domain
|
|
1233
|
+
- ✅ Clear user experience with progress indicators
|
|
1234
|
+
|
|
1235
|
+
### Success Metrics
|
|
1236
|
+
|
|
1237
|
+
- **Zero false positives** - Never block valid deployments
|
|
1238
|
+
- **Zero missed blocking issues** - Catch all issues that would fail
|
|
1239
|
+
- **< 5% performance overhead** - Minimal added deployment time
|
|
1240
|
+
- **User satisfaction** - Positive feedback on error prevention
|
|
1241
|
+
|
|
1242
|
+
---
|
|
1243
|
+
|
|
1244
|
+
## Future Enhancements
|
|
1245
|
+
|
|
1246
|
+
### Phase 2 Enhancements
|
|
1247
|
+
|
|
1248
|
+
1. **Template validation** - Validate serverless.yml syntax before generation
|
|
1249
|
+
2. **IAM permission check** - Verify deployment user has required permissions
|
|
1250
|
+
3. **Cross-stack dependency check** - Verify referenced stacks exist
|
|
1251
|
+
4. **Regional availability** - Check if services available in target region
|
|
1252
|
+
|
|
1253
|
+
### Phase 3 Enhancements
|
|
1254
|
+
|
|
1255
|
+
1. **Cost estimation** - Estimate deployment cost before execution
|
|
1256
|
+
2. **Security scan** - Check for security misconfigurations
|
|
1257
|
+
3. **Compliance validation** - Verify tags, naming conventions
|
|
1258
|
+
4. **Change preview** - Show what will change before deployment
|
|
1259
|
+
|
|
1260
|
+
---
|
|
1261
|
+
|
|
1262
|
+
## References
|
|
1263
|
+
|
|
1264
|
+
### AWS Documentation
|
|
1265
|
+
|
|
1266
|
+
- [CloudFormation Stack States](https://docs.aws.amazon.com/cli/latest/reference/cloudformation/describe-stacks.html) - Complete list of 23 stack statuses
|
|
1267
|
+
- [CloudFormation Error Codes](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/troubleshooting.html) - AlreadyExistsException, LimitExceededException, etc.
|
|
1268
|
+
- [CloudFormation Import Resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resource-import.html) - Importing orphaned resources
|
|
1269
|
+
|
|
1270
|
+
### Internal Documentation
|
|
1271
|
+
|
|
1272
|
+
- [HEALTH.md](./HEALTH.md) - Existing health check system documentation
|
|
1273
|
+
- [CLAUDE.md](../CLAUDE.md) - Frigg Framework architecture guide
|
|
1274
|
+
- [BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md](./domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md) - Template comparison approach
|
|
1275
|
+
|
|
1276
|
+
### Related Code
|
|
1277
|
+
|
|
1278
|
+
- `domains/health/` - Existing health check domain (DDD/Hexagonal)
|
|
1279
|
+
- `packages/frigg-cli/deploy-command/` - Deploy command implementation
|
|
1280
|
+
- `packages/frigg-cli/doctor-command/` - Doctor command (post-deployment)
|
|
1281
|
+
- `packages/frigg-cli/repair-command/` - Repair command (remediation)
|
|
1282
|
+
|
|
1283
|
+
---
|
|
1284
|
+
|
|
1285
|
+
## Appendix: Complete CloudFormation Status Decision Matrix
|
|
1286
|
+
|
|
1287
|
+
| Stack Status | Can Deploy? | Category | Action |
|
|
1288
|
+
|-------------|-------------|----------|--------|
|
|
1289
|
+
| `CREATE_IN_PROGRESS` | ❌ Wait | N/A | Wait for completion |
|
|
1290
|
+
| `CREATE_FAILED` | ❌ Blocked | BLOCKING | Delete stack first |
|
|
1291
|
+
| `CREATE_COMPLETE` | ✅ Yes | N/A | Proceed with update |
|
|
1292
|
+
| `ROLLBACK_IN_PROGRESS` | ❌ Wait | N/A | Wait for completion |
|
|
1293
|
+
| `ROLLBACK_FAILED` | ❌ Blocked | BLOCKING | Manual intervention |
|
|
1294
|
+
| `ROLLBACK_COMPLETE` | ❌ Blocked | BLOCKING | Delete stack first |
|
|
1295
|
+
| `DELETE_IN_PROGRESS` | ❌ Wait | N/A | Wait or cancel |
|
|
1296
|
+
| `DELETE_FAILED` | ❌ Blocked | BLOCKING | Force delete |
|
|
1297
|
+
| `DELETE_COMPLETE` | ✅ Yes | N/A | Stack gone, create new |
|
|
1298
|
+
| `UPDATE_IN_PROGRESS` | ❌ Wait | N/A | Wait for completion |
|
|
1299
|
+
| `UPDATE_COMPLETE_CLEANUP_IN_PROGRESS` | ❌ Wait | N/A | Wait for cleanup |
|
|
1300
|
+
| `UPDATE_COMPLETE` | ✅ Yes | N/A | Proceed with update |
|
|
1301
|
+
| `UPDATE_FAILED` | ⚠️ Warning | WARNING | Will rollback |
|
|
1302
|
+
| `UPDATE_ROLLBACK_IN_PROGRESS` | ❌ Wait | N/A | Wait for rollback |
|
|
1303
|
+
| `UPDATE_ROLLBACK_FAILED` | ❌ Blocked | BLOCKING | Continue rollback |
|
|
1304
|
+
| `UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS` | ❌ Wait | N/A | Wait for cleanup |
|
|
1305
|
+
| `UPDATE_ROLLBACK_COMPLETE` | ✅ Yes | N/A | Proceed after cleanup |
|
|
1306
|
+
| `REVIEW_IN_PROGRESS` | ❌ Wait | N/A | Change set review |
|
|
1307
|
+
| `IMPORT_IN_PROGRESS` | ❌ Wait | N/A | Wait for import |
|
|
1308
|
+
| `IMPORT_COMPLETE` | ✅ Yes | N/A | Proceed with update |
|
|
1309
|
+
| `IMPORT_ROLLBACK_IN_PROGRESS` | ❌ Wait | N/A | Wait for rollback |
|
|
1310
|
+
| `IMPORT_ROLLBACK_FAILED` | ❌ Blocked | BLOCKING | Manual intervention |
|
|
1311
|
+
| `IMPORT_ROLLBACK_COMPLETE` | ✅ Yes | N/A | Proceed with update |
|
|
1312
|
+
|
|
1313
|
+
---
|
|
1314
|
+
|
|
1315
|
+
**Document Version:** 1.0
|
|
1316
|
+
**Last Updated:** 2025-10-28
|
|
1317
|
+
**Status:** ✅ COMPLETE - Ready for TDD Implementation
|