@friggframework/devtools 2.0.0--canary.474.27d9425.0 → 2.0.0--canary.474.da7b114.0
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/CLI.md +1136 -0
- package/infrastructure/MULTI_CLOUD_ARCHITECTURE.md +1019 -0
- package/infrastructure/domains/health/domain/entities/property-mismatch.js +105 -0
- package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +251 -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 +179 -0
- package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +233 -0
- package/package.json +6 -6
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
# Multi-Cloud Architecture - Discovery, Doctor & Repair
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This document describes the architecture for multi-cloud support in Frigg's infrastructure tooling, with a focus on the discovery, health checking (doctor), and repair capabilities.
|
|
6
|
+
|
|
7
|
+
**Key Principle**: Use Domain-Driven Design (DDD) and Hexagonal Architecture (Ports & Adapters) to support AWS today while making it obvious where to extend for GCP, Azure, Cloudflare, and non-serverless (Docker) deployments.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Architecture Layers
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
15
|
+
│ CLI LAYER │
|
|
16
|
+
│ frigg doctor | frigg repair | frigg deploy │
|
|
17
|
+
└────────────────────────┬─────────────────────────────────────┘
|
|
18
|
+
│
|
|
19
|
+
┌────────────────────────▼─────────────────────────────────────┐
|
|
20
|
+
│ APPLICATION LAYER (Use Cases) │
|
|
21
|
+
│ Orchestrates business logic - provider agnostic │
|
|
22
|
+
│ │
|
|
23
|
+
│ • RunHealthCheckUseCase │
|
|
24
|
+
│ • RepairStackViaImportUseCase │
|
|
25
|
+
│ • ReconcilePropertyMismatchesUseCase │
|
|
26
|
+
│ • DiscoverInfrastructureUseCase │
|
|
27
|
+
└────────────────────────┬─────────────────────────────────────┘
|
|
28
|
+
│
|
|
29
|
+
│ Uses Ports (Interfaces)
|
|
30
|
+
│
|
|
31
|
+
┌────────────────────────▼─────────────────────────────────────┐
|
|
32
|
+
│ PORT INTERFACES (Boundaries) │
|
|
33
|
+
│ Define contracts - implemented by adapters │
|
|
34
|
+
│ │
|
|
35
|
+
│ • IStackRepository - Stack CRUD operations │
|
|
36
|
+
│ • IResourceDetector - Cloud resource queries │
|
|
37
|
+
│ • IDriftDetector - Compare desired vs actual │
|
|
38
|
+
│ • IResourceImporter - Import existing resources │
|
|
39
|
+
│ • IPropertyReconciler - Fix property mismatches │
|
|
40
|
+
└────────────────────────┬─────────────────────────────────────┘
|
|
41
|
+
│
|
|
42
|
+
│ Implemented by
|
|
43
|
+
│
|
|
44
|
+
┌────────────────────────▼─────────────────────────────────────┐
|
|
45
|
+
│ ADAPTER LAYER (Provider-Specific) │
|
|
46
|
+
│ │
|
|
47
|
+
│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐│
|
|
48
|
+
│ │ AWS Adapters │ │ GCP Adapters │ │ Azure ││
|
|
49
|
+
│ │ (Today) │ │ (Future) │ │ Adapters ││
|
|
50
|
+
│ │ │ │ │ │ (Future) ││
|
|
51
|
+
│ │ • CloudFormation│ │ • Deployment │ │ • ARM ││
|
|
52
|
+
│ │ • AWS SDK APIs │ │ Manager │ │ Templates ││
|
|
53
|
+
│ │ • Resource │ │ • GCP APIs │ │ • Azure ││
|
|
54
|
+
│ │ Importers │ │ │ │ APIs ││
|
|
55
|
+
│ └─────────────────┘ └─────────────────┘ └──────────────┘│
|
|
56
|
+
└──────────────────────────────────────────────────────────────┘
|
|
57
|
+
│
|
|
58
|
+
┌────────────────────────▼─────────────────────────────────────┐
|
|
59
|
+
│ CLOUD PROVIDERS │
|
|
60
|
+
│ AWS | GCP | Azure | Cloudflare │
|
|
61
|
+
└──────────────────────────────────────────────────────────────┘
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Domain Structure
|
|
67
|
+
|
|
68
|
+
### Directory Organization
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
packages/devtools/infrastructure/
|
|
72
|
+
├── domains/
|
|
73
|
+
│ ├── health/ # NEW - Health checking domain
|
|
74
|
+
│ │ ├── domain/ # Domain layer (provider-agnostic)
|
|
75
|
+
│ │ │ ├── entities/
|
|
76
|
+
│ │ │ │ ├── Resource.js
|
|
77
|
+
│ │ │ │ ├── Issue.js
|
|
78
|
+
│ │ │ │ ├── PropertyMismatch.js
|
|
79
|
+
│ │ │ │ └── StackHealthReport.js
|
|
80
|
+
│ │ │ ├── value-objects/
|
|
81
|
+
│ │ │ │ ├── StackIdentifier.js
|
|
82
|
+
│ │ │ │ ├── HealthScore.js
|
|
83
|
+
│ │ │ │ ├── ResourceState.js # IN_STACK, ORPHANED, MISSING, DRIFTED
|
|
84
|
+
│ │ │ │ └── PropertyMutability.js
|
|
85
|
+
│ │ │ ├── services/
|
|
86
|
+
│ │ │ │ ├── HealthScoreCalculator.js
|
|
87
|
+
│ │ │ │ └── MismatchAnalyzer.js
|
|
88
|
+
│ │ │ └── collections/
|
|
89
|
+
│ │ │ ├── ResourceCollection.js
|
|
90
|
+
│ │ │ └── IssueCollection.js
|
|
91
|
+
│ │ ├── application/ # Application layer (use cases)
|
|
92
|
+
│ │ │ ├── use-cases/
|
|
93
|
+
│ │ │ │ ├── run-health-check-use-case.js
|
|
94
|
+
│ │ │ │ ├── repair-via-import-use-case.js
|
|
95
|
+
│ │ │ │ └── reconcile-properties-use-case.js
|
|
96
|
+
│ │ │ └── ports/ # Port interfaces
|
|
97
|
+
│ │ │ ├── IStackRepository.js
|
|
98
|
+
│ │ │ ├── IResourceDetector.js
|
|
99
|
+
│ │ │ ├── IDriftDetector.js
|
|
100
|
+
│ │ │ ├── IResourceImporter.js
|
|
101
|
+
│ │ │ └── IPropertyReconciler.js
|
|
102
|
+
│ │ └── infrastructure/ # Infrastructure layer (adapters)
|
|
103
|
+
│ │ ├── adapters/
|
|
104
|
+
│ │ │ ├── aws/ # AWS implementations (TODAY)
|
|
105
|
+
│ │ │ │ ├── AWSStackRepository.js
|
|
106
|
+
│ │ │ │ ├── AWSResourceDetector.js
|
|
107
|
+
│ │ │ │ ├── AWSDriftDetector.js
|
|
108
|
+
│ │ │ │ ├── AWSResourceImporter.js
|
|
109
|
+
│ │ │ │ └── AWSPropertyReconciler.js
|
|
110
|
+
│ │ │ ├── gcp/ # GCP implementations (FUTURE)
|
|
111
|
+
│ │ │ │ ├── GCPStackRepository.js
|
|
112
|
+
│ │ │ │ └── ...
|
|
113
|
+
│ │ │ └── azure/ # Azure implementations (FUTURE)
|
|
114
|
+
│ │ │ ├── AzureStackRepository.js
|
|
115
|
+
│ │ │ └── ...
|
|
116
|
+
│ │ └── cli/
|
|
117
|
+
│ │ ├── doctor-command.js
|
|
118
|
+
│ │ ├── repair-command.js
|
|
119
|
+
│ │ └── presenters/
|
|
120
|
+
│ │ ├── health-report-presenter.js
|
|
121
|
+
│ │ └── repair-plan-presenter.js
|
|
122
|
+
│ │
|
|
123
|
+
│ ├── discovery/ # REFACTORED - Cloud discovery domain
|
|
124
|
+
│ │ ├── domain/
|
|
125
|
+
│ │ │ ├── entities/
|
|
126
|
+
│ │ │ │ ├── DiscoveryResult.js
|
|
127
|
+
│ │ │ │ └── CloudResource.js
|
|
128
|
+
│ │ │ └── value-objects/
|
|
129
|
+
│ │ │ └── ResourceIdentifier.js
|
|
130
|
+
│ │ ├── application/
|
|
131
|
+
│ │ │ ├── use-cases/
|
|
132
|
+
│ │ │ │ └── discover-infrastructure-use-case.js
|
|
133
|
+
│ │ │ └── ports/
|
|
134
|
+
│ │ │ ├── ICloudProvider.js # Port for cloud providers
|
|
135
|
+
│ │ │ └── IStackProvider.js # Port for stack systems
|
|
136
|
+
│ │ └── infrastructure/
|
|
137
|
+
│ │ └── adapters/
|
|
138
|
+
│ │ ├── aws/
|
|
139
|
+
│ │ │ ├── AWSCloudProvider.js
|
|
140
|
+
│ │ │ ├── CloudFormationStackProvider.js
|
|
141
|
+
│ │ │ ├── EC2Discoverer.js
|
|
142
|
+
│ │ │ ├── RDSDiscoverer.js
|
|
143
|
+
│ │ │ └── KMSDiscoverer.js
|
|
144
|
+
│ │ ├── gcp/
|
|
145
|
+
│ │ │ ├── GCPCloudProvider.js
|
|
146
|
+
│ │ │ └── DeploymentManagerStackProvider.js
|
|
147
|
+
│ │ └── azure/
|
|
148
|
+
│ │ ├── AzureCloudProvider.js
|
|
149
|
+
│ │ └── ARMTemplateStackProvider.js
|
|
150
|
+
│ │
|
|
151
|
+
│ ├── networking/ # PROVIDER-SPECIFIC builders
|
|
152
|
+
│ │ ├── aws/
|
|
153
|
+
│ │ │ ├── vpc-builder.js
|
|
154
|
+
│ │ │ └── vpc-resolver.js
|
|
155
|
+
│ │ ├── gcp/
|
|
156
|
+
│ │ │ ├── network-builder.js # (FUTURE)
|
|
157
|
+
│ │ │ └── network-resolver.js
|
|
158
|
+
│ │ └── azure/
|
|
159
|
+
│ │ ├── vnet-builder.js # (FUTURE)
|
|
160
|
+
│ │ └── vnet-resolver.js
|
|
161
|
+
│ │
|
|
162
|
+
│ ├── database/ # PROVIDER-SPECIFIC builders
|
|
163
|
+
│ │ ├── aws/
|
|
164
|
+
│ │ │ ├── aurora-builder.js
|
|
165
|
+
│ │ │ ├── aurora-resolver.js
|
|
166
|
+
│ │ │ ├── migration-builder.js
|
|
167
|
+
│ │ │ └── migration-resolver.js
|
|
168
|
+
│ │ ├── gcp/
|
|
169
|
+
│ │ │ ├── cloud-sql-builder.js # (FUTURE)
|
|
170
|
+
│ │ │ └── cloud-sql-resolver.js
|
|
171
|
+
│ │ └── azure/
|
|
172
|
+
│ │ ├── cosmos-db-builder.js # (FUTURE)
|
|
173
|
+
│ │ └── cosmos-db-resolver.js
|
|
174
|
+
│ │
|
|
175
|
+
│ ├── security/ # PROVIDER-SPECIFIC builders
|
|
176
|
+
│ │ ├── aws/
|
|
177
|
+
│ │ │ ├── kms-builder.js
|
|
178
|
+
│ │ │ └── kms-resolver.js
|
|
179
|
+
│ │ ├── gcp/
|
|
180
|
+
│ │ │ └── kms-builder.js # (FUTURE)
|
|
181
|
+
│ │ └── azure/
|
|
182
|
+
│ │ └── key-vault-builder.js # (FUTURE)
|
|
183
|
+
│ │
|
|
184
|
+
│ └── shared/ # Shared utilities
|
|
185
|
+
│ ├── base-resource-resolver.js
|
|
186
|
+
│ ├── builder-orchestrator.js
|
|
187
|
+
│ └── resource-ownership.js
|
|
188
|
+
│
|
|
189
|
+
└── providers/ # PROVIDER REGISTRY
|
|
190
|
+
├── registry.js # Maps provider name to implementations
|
|
191
|
+
├── aws-provider.js # AWS provider definition
|
|
192
|
+
├── gcp-provider.js # GCP provider definition (FUTURE)
|
|
193
|
+
└── azure-provider.js # Azure provider definition (FUTURE)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Port Interfaces (Contracts)
|
|
199
|
+
|
|
200
|
+
### IStackRepository
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
/**
|
|
204
|
+
* Port: Stack Repository Interface
|
|
205
|
+
*
|
|
206
|
+
* Abstracts stack management operations (CloudFormation, Deployment Manager, ARM)
|
|
207
|
+
*/
|
|
208
|
+
class IStackRepository {
|
|
209
|
+
/**
|
|
210
|
+
* Get stack by identifier
|
|
211
|
+
* @param {StackIdentifier} identifier
|
|
212
|
+
* @returns {Promise<Stack|null>}
|
|
213
|
+
*/
|
|
214
|
+
async getStack(identifier) {
|
|
215
|
+
throw new Error('Not implemented');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* List resources in stack
|
|
220
|
+
* @param {StackIdentifier} identifier
|
|
221
|
+
* @returns {Promise<Resource[]>}
|
|
222
|
+
*/
|
|
223
|
+
async listResources(identifier) {
|
|
224
|
+
throw new Error('Not implemented');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get stack outputs
|
|
229
|
+
* @param {StackIdentifier} identifier
|
|
230
|
+
* @returns {Promise<Object>}
|
|
231
|
+
*/
|
|
232
|
+
async getOutputs(identifier) {
|
|
233
|
+
throw new Error('Not implemented');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get stack parameters
|
|
238
|
+
* @param {StackIdentifier} identifier
|
|
239
|
+
* @returns {Promise<Object>}
|
|
240
|
+
*/
|
|
241
|
+
async getParameters(identifier) {
|
|
242
|
+
throw new Error('Not implemented');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check if stack exists
|
|
247
|
+
* @param {StackIdentifier} identifier
|
|
248
|
+
* @returns {Promise<boolean>}
|
|
249
|
+
*/
|
|
250
|
+
async exists(identifier) {
|
|
251
|
+
throw new Error('Not implemented');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### IResourceDetector
|
|
257
|
+
|
|
258
|
+
```javascript
|
|
259
|
+
/**
|
|
260
|
+
* Port: Resource Detector Interface
|
|
261
|
+
*
|
|
262
|
+
* Abstracts cloud resource discovery (AWS APIs, GCP APIs, Azure APIs)
|
|
263
|
+
*/
|
|
264
|
+
class IResourceDetector {
|
|
265
|
+
/**
|
|
266
|
+
* Detect VPCs/Networks
|
|
267
|
+
* @param {string} region
|
|
268
|
+
* @returns {Promise<NetworkResource[]>}
|
|
269
|
+
*/
|
|
270
|
+
async detectNetworks(region) {
|
|
271
|
+
throw new Error('Not implemented');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Detect database instances
|
|
276
|
+
* @param {string} region
|
|
277
|
+
* @returns {Promise<DatabaseResource[]>}
|
|
278
|
+
*/
|
|
279
|
+
async detectDatabases(region) {
|
|
280
|
+
throw new Error('Not implemented');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Detect encryption keys
|
|
285
|
+
* @param {string} region
|
|
286
|
+
* @returns {Promise<KeyResource[]>}
|
|
287
|
+
*/
|
|
288
|
+
async detectKeys(region) {
|
|
289
|
+
throw new Error('Not implemented');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Detect resource by physical ID
|
|
294
|
+
* @param {string} physicalId
|
|
295
|
+
* @param {string} resourceType
|
|
296
|
+
* @returns {Promise<Resource|null>}
|
|
297
|
+
*/
|
|
298
|
+
async detectResourceById(physicalId, resourceType) {
|
|
299
|
+
throw new Error('Not implemented');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get resource properties
|
|
304
|
+
* @param {string} physicalId
|
|
305
|
+
* @param {string} resourceType
|
|
306
|
+
* @returns {Promise<Object>}
|
|
307
|
+
*/
|
|
308
|
+
async getResourceProperties(physicalId, resourceType) {
|
|
309
|
+
throw new Error('Not implemented');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### IDriftDetector
|
|
315
|
+
|
|
316
|
+
```javascript
|
|
317
|
+
/**
|
|
318
|
+
* Port: Drift Detector Interface
|
|
319
|
+
*
|
|
320
|
+
* Abstracts drift detection logic
|
|
321
|
+
*/
|
|
322
|
+
class IDriftDetector {
|
|
323
|
+
/**
|
|
324
|
+
* Detect drift for a resource
|
|
325
|
+
* @param {Resource} resource - Resource from stack
|
|
326
|
+
* @param {Object} desiredProperties - Desired properties
|
|
327
|
+
* @returns {Promise<PropertyMismatch[]>}
|
|
328
|
+
*/
|
|
329
|
+
async detectDrift(resource, desiredProperties) {
|
|
330
|
+
throw new Error('Not implemented');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Detect drift for entire stack
|
|
335
|
+
* @param {StackIdentifier} identifier
|
|
336
|
+
* @returns {Promise<DriftDetectionResult>}
|
|
337
|
+
*/
|
|
338
|
+
async detectStackDrift(identifier) {
|
|
339
|
+
throw new Error('Not implemented');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### IResourceImporter
|
|
345
|
+
|
|
346
|
+
```javascript
|
|
347
|
+
/**
|
|
348
|
+
* Port: Resource Importer Interface
|
|
349
|
+
*
|
|
350
|
+
* Abstracts resource import operations
|
|
351
|
+
*/
|
|
352
|
+
class IResourceImporter {
|
|
353
|
+
/**
|
|
354
|
+
* Check if resource type is importable
|
|
355
|
+
* @param {string} resourceType
|
|
356
|
+
* @returns {boolean}
|
|
357
|
+
*/
|
|
358
|
+
isImportable(resourceType) {
|
|
359
|
+
throw new Error('Not implemented');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Create import change set
|
|
364
|
+
* @param {StackIdentifier} stackId
|
|
365
|
+
* @param {Resource[]} resources
|
|
366
|
+
* @returns {Promise<ImportChangeSet>}
|
|
367
|
+
*/
|
|
368
|
+
async createImportChangeSet(stackId, resources) {
|
|
369
|
+
throw new Error('Not implemented');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Execute import operation
|
|
374
|
+
* @param {ImportChangeSet} changeSet
|
|
375
|
+
* @returns {Promise<ImportResult>}
|
|
376
|
+
*/
|
|
377
|
+
async executeImport(changeSet) {
|
|
378
|
+
throw new Error('Not implemented');
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### IPropertyReconciler
|
|
384
|
+
|
|
385
|
+
```javascript
|
|
386
|
+
/**
|
|
387
|
+
* Port: Property Reconciler Interface
|
|
388
|
+
*
|
|
389
|
+
* Abstracts property reconciliation logic
|
|
390
|
+
*/
|
|
391
|
+
class IPropertyReconciler {
|
|
392
|
+
/**
|
|
393
|
+
* Reconcile property mismatch
|
|
394
|
+
* @param {PropertyMismatch} mismatch
|
|
395
|
+
* @param {Resource} resource
|
|
396
|
+
* @returns {Promise<ReconciliationResult>}
|
|
397
|
+
*/
|
|
398
|
+
async reconcile(mismatch, resource) {
|
|
399
|
+
throw new Error('Not implemented');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Plan reconciliation (dry run)
|
|
404
|
+
* @param {PropertyMismatch[]} mismatches
|
|
405
|
+
* @returns {Promise<ReconciliationPlan>}
|
|
406
|
+
*/
|
|
407
|
+
async planReconciliation(mismatches) {
|
|
408
|
+
throw new Error('Not implemented');
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## AWS Adapter Implementations
|
|
416
|
+
|
|
417
|
+
### AWSStackRepository
|
|
418
|
+
|
|
419
|
+
```javascript
|
|
420
|
+
const { CloudFormationClient, DescribeStacksCommand, ListStackResourcesCommand } = require('@aws-sdk/client-cloudformation');
|
|
421
|
+
const IStackRepository = require('../../application/ports/IStackRepository');
|
|
422
|
+
|
|
423
|
+
class AWSStackRepository extends IStackRepository {
|
|
424
|
+
constructor({ region }) {
|
|
425
|
+
super();
|
|
426
|
+
this.client = new CloudFormationClient({ region });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async getStack(identifier) {
|
|
430
|
+
const command = new DescribeStacksCommand({
|
|
431
|
+
StackName: identifier.stackName,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
const response = await this.client.send(command);
|
|
436
|
+
return response.Stacks[0] || null;
|
|
437
|
+
} catch (error) {
|
|
438
|
+
if (error.name === 'ValidationError') {
|
|
439
|
+
return null; // Stack doesn't exist
|
|
440
|
+
}
|
|
441
|
+
throw error;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async listResources(identifier) {
|
|
446
|
+
const command = new ListStackResourcesCommand({
|
|
447
|
+
StackName: identifier.stackName,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const response = await this.client.send(command);
|
|
451
|
+
|
|
452
|
+
return response.StackResourceSummaries.map(resource => ({
|
|
453
|
+
logicalId: resource.LogicalResourceId,
|
|
454
|
+
physicalId: resource.PhysicalResourceId,
|
|
455
|
+
type: resource.ResourceType,
|
|
456
|
+
status: resource.ResourceStatus,
|
|
457
|
+
timestamp: resource.LastUpdatedTimestamp,
|
|
458
|
+
}));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async getOutputs(identifier) {
|
|
462
|
+
const stack = await this.getStack(identifier);
|
|
463
|
+
if (!stack) return {};
|
|
464
|
+
|
|
465
|
+
return (stack.Outputs || []).reduce((acc, output) => {
|
|
466
|
+
acc[output.OutputKey] = output.OutputValue;
|
|
467
|
+
return acc;
|
|
468
|
+
}, {});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async exists(identifier) {
|
|
472
|
+
return (await this.getStack(identifier)) !== null;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
module.exports = AWSStackRepository;
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### AWSResourceDetector
|
|
480
|
+
|
|
481
|
+
```javascript
|
|
482
|
+
const { EC2Client, DescribeVpcsCommand, DescribeSubnetsCommand } = require('@aws-sdk/client-ec2');
|
|
483
|
+
const { RDSClient, DescribeDBClustersCommand } = require('@aws-sdk/client-rds');
|
|
484
|
+
const { KMSClient, ListKeysCommand, DescribeKeyCommand } = require('@aws-sdk/client-kms');
|
|
485
|
+
const IResourceDetector = require('../../application/ports/IResourceDetector');
|
|
486
|
+
|
|
487
|
+
class AWSResourceDetector extends IResourceDetector {
|
|
488
|
+
constructor({ region }) {
|
|
489
|
+
super();
|
|
490
|
+
this.region = region;
|
|
491
|
+
this.ec2 = new EC2Client({ region });
|
|
492
|
+
this.rds = new RDSClient({ region });
|
|
493
|
+
this.kms = new KMSClient({ region });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async detectNetworks(region) {
|
|
497
|
+
const command = new DescribeVpcsCommand({});
|
|
498
|
+
const response = await this.ec2.send(command);
|
|
499
|
+
|
|
500
|
+
return response.Vpcs.map(vpc => ({
|
|
501
|
+
type: 'AWS::EC2::VPC',
|
|
502
|
+
physicalId: vpc.VpcId,
|
|
503
|
+
properties: {
|
|
504
|
+
CidrBlock: vpc.CidrBlock,
|
|
505
|
+
Tags: vpc.Tags,
|
|
506
|
+
IsDefault: vpc.IsDefault,
|
|
507
|
+
},
|
|
508
|
+
}));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async detectDatabases(region) {
|
|
512
|
+
const command = new DescribeDBClustersCommand({});
|
|
513
|
+
const response = await this.rds.send(command);
|
|
514
|
+
|
|
515
|
+
return response.DBClusters.map(cluster => ({
|
|
516
|
+
type: 'AWS::RDS::DBCluster',
|
|
517
|
+
physicalId: cluster.DBClusterIdentifier,
|
|
518
|
+
properties: {
|
|
519
|
+
Engine: cluster.Engine,
|
|
520
|
+
EngineVersion: cluster.EngineVersion,
|
|
521
|
+
DatabaseName: cluster.DatabaseName,
|
|
522
|
+
MasterUsername: cluster.MasterUsername,
|
|
523
|
+
Port: cluster.Port,
|
|
524
|
+
},
|
|
525
|
+
}));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async detectKeys(region) {
|
|
529
|
+
const listCommand = new ListKeysCommand({});
|
|
530
|
+
const response = await this.kms.send(listCommand);
|
|
531
|
+
|
|
532
|
+
const keys = [];
|
|
533
|
+
for (const key of response.Keys) {
|
|
534
|
+
const describeCommand = new DescribeKeyCommand({ KeyId: key.KeyId });
|
|
535
|
+
const keyDetails = await this.kms.send(describeCommand);
|
|
536
|
+
|
|
537
|
+
keys.push({
|
|
538
|
+
type: 'AWS::KMS::Key',
|
|
539
|
+
physicalId: keyDetails.KeyMetadata.KeyId,
|
|
540
|
+
properties: {
|
|
541
|
+
Description: keyDetails.KeyMetadata.Description,
|
|
542
|
+
Enabled: keyDetails.KeyMetadata.Enabled,
|
|
543
|
+
KeyUsage: keyDetails.KeyMetadata.KeyUsage,
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return keys;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async detectResourceById(physicalId, resourceType) {
|
|
552
|
+
// Route to appropriate detector based on resource type
|
|
553
|
+
switch (resourceType) {
|
|
554
|
+
case 'AWS::EC2::VPC':
|
|
555
|
+
return this.detectVpcById(physicalId);
|
|
556
|
+
case 'AWS::RDS::DBCluster':
|
|
557
|
+
return this.detectClusterById(physicalId);
|
|
558
|
+
case 'AWS::KMS::Key':
|
|
559
|
+
return this.detectKeyById(physicalId);
|
|
560
|
+
default:
|
|
561
|
+
throw new Error(`Unsupported resource type: ${resourceType}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async getResourceProperties(physicalId, resourceType) {
|
|
566
|
+
const resource = await this.detectResourceById(physicalId, resourceType);
|
|
567
|
+
return resource ? resource.properties : null;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Private helper methods
|
|
571
|
+
async detectVpcById(vpcId) {
|
|
572
|
+
const command = new DescribeVpcsCommand({ VpcIds: [vpcId] });
|
|
573
|
+
const response = await this.ec2.send(command);
|
|
574
|
+
const vpc = response.Vpcs[0];
|
|
575
|
+
|
|
576
|
+
if (!vpc) return null;
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
type: 'AWS::EC2::VPC',
|
|
580
|
+
physicalId: vpc.VpcId,
|
|
581
|
+
properties: {
|
|
582
|
+
CidrBlock: vpc.CidrBlock,
|
|
583
|
+
Tags: vpc.Tags,
|
|
584
|
+
IsDefault: vpc.IsDefault,
|
|
585
|
+
},
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async detectClusterById(clusterId) {
|
|
590
|
+
const command = new DescribeDBClustersCommand({ DBClusterIdentifier: clusterId });
|
|
591
|
+
try {
|
|
592
|
+
const response = await this.rds.send(command);
|
|
593
|
+
const cluster = response.DBClusters[0];
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
type: 'AWS::RDS::DBCluster',
|
|
597
|
+
physicalId: cluster.DBClusterIdentifier,
|
|
598
|
+
properties: {
|
|
599
|
+
Engine: cluster.Engine,
|
|
600
|
+
EngineVersion: cluster.EngineVersion,
|
|
601
|
+
DatabaseName: cluster.DatabaseName,
|
|
602
|
+
MasterUsername: cluster.MasterUsername,
|
|
603
|
+
Port: cluster.Port,
|
|
604
|
+
},
|
|
605
|
+
};
|
|
606
|
+
} catch (error) {
|
|
607
|
+
if (error.name === 'DBClusterNotFoundFault') {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
throw error;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async detectKeyById(keyId) {
|
|
615
|
+
const command = new DescribeKeyCommand({ KeyId: keyId });
|
|
616
|
+
try {
|
|
617
|
+
const response = await this.kms.send(command);
|
|
618
|
+
return {
|
|
619
|
+
type: 'AWS::KMS::Key',
|
|
620
|
+
physicalId: response.KeyMetadata.KeyId,
|
|
621
|
+
properties: {
|
|
622
|
+
Description: response.KeyMetadata.Description,
|
|
623
|
+
Enabled: response.KeyMetadata.Enabled,
|
|
624
|
+
KeyUsage: response.KeyMetadata.KeyUsage,
|
|
625
|
+
},
|
|
626
|
+
};
|
|
627
|
+
} catch (error) {
|
|
628
|
+
if (error.name === 'NotFoundException') {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
throw error;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
module.exports = AWSResourceDetector;
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## Provider Registry
|
|
642
|
+
|
|
643
|
+
### registry.js
|
|
644
|
+
|
|
645
|
+
```javascript
|
|
646
|
+
/**
|
|
647
|
+
* Provider Registry
|
|
648
|
+
*
|
|
649
|
+
* Maps provider names to their implementations
|
|
650
|
+
*/
|
|
651
|
+
|
|
652
|
+
class ProviderRegistry {
|
|
653
|
+
constructor() {
|
|
654
|
+
this.providers = new Map();
|
|
655
|
+
this.registerBuiltInProviders();
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
registerBuiltInProviders() {
|
|
659
|
+
// Register AWS (available today)
|
|
660
|
+
this.register('aws', require('./aws-provider'));
|
|
661
|
+
|
|
662
|
+
// Register GCP (future - throws helpful error)
|
|
663
|
+
this.register('gcp', {
|
|
664
|
+
name: 'GCP',
|
|
665
|
+
available: false,
|
|
666
|
+
message: 'GCP support is planned but not yet implemented',
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// Register Azure (future - throws helpful error)
|
|
670
|
+
this.register('azure', {
|
|
671
|
+
name: 'Azure',
|
|
672
|
+
available: false,
|
|
673
|
+
message: 'Azure support is planned but not yet implemented',
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
register(name, provider) {
|
|
678
|
+
this.providers.set(name, provider);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
get(name) {
|
|
682
|
+
const provider = this.providers.get(name);
|
|
683
|
+
|
|
684
|
+
if (!provider) {
|
|
685
|
+
throw new Error(`Unknown provider: ${name}. Supported providers: ${Array.from(this.providers.keys()).join(', ')}`);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (provider.available === false) {
|
|
689
|
+
throw new Error(`${provider.name} provider is not yet available. ${provider.message}`);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return provider;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
isAvailable(name) {
|
|
696
|
+
const provider = this.providers.get(name);
|
|
697
|
+
return provider && provider.available !== false;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
getSupportedProviders() {
|
|
701
|
+
return Array.from(this.providers.keys()).filter(name => this.isAvailable(name));
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
module.exports = new ProviderRegistry();
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
### aws-provider.js
|
|
709
|
+
|
|
710
|
+
```javascript
|
|
711
|
+
/**
|
|
712
|
+
* AWS Provider Definition
|
|
713
|
+
*
|
|
714
|
+
* Factory for AWS-specific implementations
|
|
715
|
+
*/
|
|
716
|
+
|
|
717
|
+
const AWSStackRepository = require('../domains/health/infrastructure/adapters/aws/AWSStackRepository');
|
|
718
|
+
const AWSResourceDetector = require('../domains/health/infrastructure/adapters/aws/AWSResourceDetector');
|
|
719
|
+
const AWSDriftDetector = require('../domains/health/infrastructure/adapters/aws/AWSDriftDetector');
|
|
720
|
+
const AWSResourceImporter = require('../domains/health/infrastructure/adapters/aws/AWSResourceImporter');
|
|
721
|
+
const AWSPropertyReconciler = require('../domains/health/infrastructure/adapters/aws/AWSPropertyReconciler');
|
|
722
|
+
|
|
723
|
+
// Domain builders
|
|
724
|
+
const VpcBuilder = require('../domains/networking/aws/vpc-builder');
|
|
725
|
+
const KmsBuilder = require('../domains/security/aws/kms-builder');
|
|
726
|
+
const AuroraBuilder = require('../domains/database/aws/aurora-builder');
|
|
727
|
+
const MigrationBuilder = require('../domains/database/aws/migration-builder');
|
|
728
|
+
|
|
729
|
+
module.exports = {
|
|
730
|
+
name: 'AWS',
|
|
731
|
+
available: true,
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Create health check adapters for AWS
|
|
735
|
+
*/
|
|
736
|
+
createHealthAdapters({ region }) {
|
|
737
|
+
return {
|
|
738
|
+
stackRepository: new AWSStackRepository({ region }),
|
|
739
|
+
resourceDetector: new AWSResourceDetector({ region }),
|
|
740
|
+
driftDetector: new AWSDriftDetector({ region }),
|
|
741
|
+
resourceImporter: new AWSResourceImporter({ region }),
|
|
742
|
+
propertyReconciler: new AWSPropertyReconciler({ region }),
|
|
743
|
+
};
|
|
744
|
+
},
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Get infrastructure builders for AWS
|
|
748
|
+
*/
|
|
749
|
+
getBuilders() {
|
|
750
|
+
return [
|
|
751
|
+
new VpcBuilder(),
|
|
752
|
+
new KmsBuilder(),
|
|
753
|
+
new AuroraBuilder(),
|
|
754
|
+
new MigrationBuilder(),
|
|
755
|
+
];
|
|
756
|
+
},
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Get supported resource types
|
|
760
|
+
*/
|
|
761
|
+
getSupportedResourceTypes() {
|
|
762
|
+
return [
|
|
763
|
+
'AWS::EC2::VPC',
|
|
764
|
+
'AWS::EC2::Subnet',
|
|
765
|
+
'AWS::EC2::SecurityGroup',
|
|
766
|
+
'AWS::RDS::DBCluster',
|
|
767
|
+
'AWS::RDS::DBInstance',
|
|
768
|
+
'AWS::KMS::Key',
|
|
769
|
+
'AWS::Lambda::Function',
|
|
770
|
+
'AWS::SQS::Queue',
|
|
771
|
+
'AWS::S3::Bucket',
|
|
772
|
+
// ... more
|
|
773
|
+
];
|
|
774
|
+
},
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Get resource property metadata
|
|
778
|
+
*/
|
|
779
|
+
getResourceMetadata(resourceType) {
|
|
780
|
+
return require(`./metadata/${resourceType.replace(/::/g, '_')}.json`);
|
|
781
|
+
},
|
|
782
|
+
};
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
---
|
|
786
|
+
|
|
787
|
+
## Use Case Integration with Providers
|
|
788
|
+
|
|
789
|
+
### RunHealthCheckUseCase
|
|
790
|
+
|
|
791
|
+
```javascript
|
|
792
|
+
const ProviderRegistry = require('../../../providers/registry');
|
|
793
|
+
|
|
794
|
+
class RunHealthCheckUseCase {
|
|
795
|
+
/**
|
|
796
|
+
* @param {Object} dependencies - Injected dependencies (optional)
|
|
797
|
+
*/
|
|
798
|
+
constructor(dependencies = {}) {
|
|
799
|
+
this.dependencies = dependencies;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Execute health check
|
|
804
|
+
*
|
|
805
|
+
* @param {Object} params
|
|
806
|
+
* @param {string} params.stackName - CloudFormation stack name
|
|
807
|
+
* @param {string} params.region - Cloud provider region
|
|
808
|
+
* @param {string} params.provider - Provider name ('aws', 'gcp', 'azure')
|
|
809
|
+
* @param {Object} params.appDefinition - App definition (desired state)
|
|
810
|
+
*/
|
|
811
|
+
async execute({ stackName, region, provider, appDefinition }) {
|
|
812
|
+
// Get provider-specific adapters
|
|
813
|
+
const providerImpl = ProviderRegistry.get(provider);
|
|
814
|
+
const adapters = this.dependencies.adapters || providerImpl.createHealthAdapters({ region });
|
|
815
|
+
|
|
816
|
+
// Step 1: Get stack state
|
|
817
|
+
const stackIdentifier = new StackIdentifier({ stackName, region });
|
|
818
|
+
const stack = await adapters.stackRepository.getStack(stackIdentifier);
|
|
819
|
+
|
|
820
|
+
if (!stack) {
|
|
821
|
+
return StackHealthReport.createForMissingStack(stackIdentifier);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Step 2: Discover resources
|
|
825
|
+
const stackResources = await adapters.stackRepository.listResources(stackIdentifier);
|
|
826
|
+
const cloudResources = await this.discoverCloudResources(adapters.resourceDetector, region);
|
|
827
|
+
|
|
828
|
+
// Step 3: Detect issues
|
|
829
|
+
const orphanedResources = this.detectOrphaned(stackResources, cloudResources);
|
|
830
|
+
const missingResources = this.detectMissing(stackResources, cloudResources);
|
|
831
|
+
const driftedResources = await this.detectDrift(stackResources, cloudResources, adapters.driftDetector);
|
|
832
|
+
|
|
833
|
+
// Step 4: Calculate health score
|
|
834
|
+
const healthScore = HealthScoreCalculator.calculate({
|
|
835
|
+
orphaned: orphanedResources.length,
|
|
836
|
+
missing: missingResources.length,
|
|
837
|
+
drifted: driftedResources.length,
|
|
838
|
+
total: stackResources.length,
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
// Step 5: Build health report
|
|
842
|
+
return new StackHealthReport({
|
|
843
|
+
stackIdentifier,
|
|
844
|
+
healthScore,
|
|
845
|
+
orphanedResources,
|
|
846
|
+
missingResources,
|
|
847
|
+
driftedResources,
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async discoverCloudResources(detector, region) {
|
|
852
|
+
const [networks, databases, keys] = await Promise.all([
|
|
853
|
+
detector.detectNetworks(region),
|
|
854
|
+
detector.detectDatabases(region),
|
|
855
|
+
detector.detectKeys(region),
|
|
856
|
+
]);
|
|
857
|
+
|
|
858
|
+
return [...networks, ...databases, ...keys];
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
detectOrphaned(stackResources, cloudResources) {
|
|
862
|
+
// Resources in cloud but not in stack
|
|
863
|
+
const stackPhysicalIds = new Set(stackResources.map(r => r.physicalId));
|
|
864
|
+
return cloudResources.filter(r => !stackPhysicalIds.has(r.physicalId));
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
detectMissing(stackResources, cloudResources) {
|
|
868
|
+
// Resources in stack but not in cloud
|
|
869
|
+
const cloudPhysicalIds = new Set(cloudResources.map(r => r.physicalId));
|
|
870
|
+
return stackResources.filter(r => !cloudPhysicalIds.has(r.physicalId));
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
async detectDrift(stackResources, cloudResources, driftDetector) {
|
|
874
|
+
const drifted = [];
|
|
875
|
+
|
|
876
|
+
for (const stackResource of stackResources) {
|
|
877
|
+
const cloudResource = cloudResources.find(r => r.physicalId === stackResource.physicalId);
|
|
878
|
+
|
|
879
|
+
if (cloudResource) {
|
|
880
|
+
const mismatches = await driftDetector.detectDrift(stackResource, cloudResource.properties);
|
|
881
|
+
|
|
882
|
+
if (mismatches.length > 0) {
|
|
883
|
+
drifted.push({
|
|
884
|
+
resource: stackResource,
|
|
885
|
+
mismatches,
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
return drifted;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
module.exports = RunHealthCheckUseCase;
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
---
|
|
899
|
+
|
|
900
|
+
## CLI Command Integration
|
|
901
|
+
|
|
902
|
+
### doctor-command.js
|
|
903
|
+
|
|
904
|
+
```javascript
|
|
905
|
+
const ProviderRegistry = require('../../providers/registry');
|
|
906
|
+
const RunHealthCheckUseCase = require('../../domains/health/application/use-cases/run-health-check-use-case');
|
|
907
|
+
const HealthReportPresenter = require('./presenters/health-report-presenter');
|
|
908
|
+
|
|
909
|
+
async function doctorCommand(options) {
|
|
910
|
+
console.log('🩺 Running infrastructure health check...\n');
|
|
911
|
+
|
|
912
|
+
// Load app definition
|
|
913
|
+
const appDefinition = loadAppDefinition();
|
|
914
|
+
const provider = options.provider || appDefinition.provider || 'aws';
|
|
915
|
+
const region = options.region || appDefinition.region;
|
|
916
|
+
const stackName = options.stack || `${appDefinition.name}-${options.stage || 'dev'}`;
|
|
917
|
+
|
|
918
|
+
// Verify provider is supported
|
|
919
|
+
if (!ProviderRegistry.isAvailable(provider)) {
|
|
920
|
+
console.error(`❌ Provider '${provider}' is not supported`);
|
|
921
|
+
console.log(`Supported providers: ${ProviderRegistry.getSupportedProviders().join(', ')}`);
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Create use case (adapters created automatically based on provider)
|
|
926
|
+
const useCase = new RunHealthCheckUseCase();
|
|
927
|
+
|
|
928
|
+
// Execute health check
|
|
929
|
+
const report = await useCase.execute({
|
|
930
|
+
stackName,
|
|
931
|
+
region,
|
|
932
|
+
provider,
|
|
933
|
+
appDefinition,
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
// Present results
|
|
937
|
+
const presenter = new HealthReportPresenter({ format: options.format || 'table' });
|
|
938
|
+
presenter.present(report);
|
|
939
|
+
|
|
940
|
+
// Exit with code based on health score (if requested)
|
|
941
|
+
if (options.exitCode) {
|
|
942
|
+
if (report.healthScore.isHealthy()) {
|
|
943
|
+
process.exit(0);
|
|
944
|
+
} else if (report.healthScore.isDegraded()) {
|
|
945
|
+
process.exit(1);
|
|
946
|
+
} else {
|
|
947
|
+
process.exit(2);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
module.exports = doctorCommand;
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
---
|
|
956
|
+
|
|
957
|
+
## Extension Points for Future Providers
|
|
958
|
+
|
|
959
|
+
### Adding GCP Support
|
|
960
|
+
|
|
961
|
+
To add GCP support in the future:
|
|
962
|
+
|
|
963
|
+
1. **Create GCP adapters**:
|
|
964
|
+
```
|
|
965
|
+
infrastructure/domains/health/infrastructure/adapters/gcp/
|
|
966
|
+
├── GCPStackRepository.js # Deployment Manager
|
|
967
|
+
├── GCPResourceDetector.js # GCP APIs
|
|
968
|
+
├── GCPDriftDetector.js
|
|
969
|
+
├── GCPResourceImporter.js
|
|
970
|
+
└── GCPPropertyReconciler.js
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
2. **Create GCP builders**:
|
|
974
|
+
```
|
|
975
|
+
infrastructure/domains/networking/gcp/network-builder.js
|
|
976
|
+
infrastructure/domains/database/gcp/cloud-sql-builder.js
|
|
977
|
+
infrastructure/domains/security/gcp/kms-builder.js
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
3. **Register GCP provider**:
|
|
981
|
+
```javascript
|
|
982
|
+
// providers/gcp-provider.js
|
|
983
|
+
module.exports = {
|
|
984
|
+
name: 'GCP',
|
|
985
|
+
available: true,
|
|
986
|
+
createHealthAdapters({ region }) { ... },
|
|
987
|
+
getBuilders() { ... },
|
|
988
|
+
getSupportedResourceTypes() { ... },
|
|
989
|
+
};
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
4. **Update registry**:
|
|
993
|
+
```javascript
|
|
994
|
+
// providers/registry.js
|
|
995
|
+
this.register('gcp', require('./gcp-provider'));
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
**No changes required to**:
|
|
999
|
+
- Domain entities (Resource, Issue, StackHealthReport)
|
|
1000
|
+
- Value objects (HealthScore, ResourceState)
|
|
1001
|
+
- Use cases (RunHealthCheckUseCase, RepairStackViaImportUseCase)
|
|
1002
|
+
- CLI commands (doctor-command.js, repair-command.js)
|
|
1003
|
+
|
|
1004
|
+
The hexagonal architecture ensures new providers only require implementing the port interfaces!
|
|
1005
|
+
|
|
1006
|
+
---
|
|
1007
|
+
|
|
1008
|
+
## Summary
|
|
1009
|
+
|
|
1010
|
+
This architecture achieves:
|
|
1011
|
+
|
|
1012
|
+
✅ **Multi-cloud ready** - Ports & Adapters make provider swapping trivial
|
|
1013
|
+
✅ **Provider-specific domains clear** - Obvious where AWS/GCP/Azure diverge
|
|
1014
|
+
✅ **Testable** - Mock port interfaces for unit tests
|
|
1015
|
+
✅ **Extensible** - Add new providers without touching domain logic
|
|
1016
|
+
✅ **Explicit** - Provider selection obvious in app definition
|
|
1017
|
+
✅ **Future-proof** - Non-serverless (Docker) can be added as another provider
|
|
1018
|
+
|
|
1019
|
+
**AWS works today**, and the path to GCP/Azure/Cloudflare is clear and isolated to adapter implementations.
|