@friggframework/devtools 2.0.0--canary.474.b510a6e.0 → 2.0.0--canary.474.2ad4ebc.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 → README.md} +99 -0
- package/infrastructure/HEALTH.md +468 -0
- package/package.json +6 -6
- package/FRIGG_DOCTOR_USAGE.md +0 -217
- package/TDD_PROOF.md +0 -254
- package/frigg-cli/CLI_PACKAGE_SEPARATION.md +0 -416
- package/infrastructure/MULTI_CLOUD_ARCHITECTURE.md +0 -703
|
@@ -1,703 +0,0 @@
|
|
|
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
|
-
Port interfaces define the contracts that provider-specific adapters must implement. These are the boundaries between the provider-agnostic domain layer and provider-specific infrastructure.
|
|
201
|
-
|
|
202
|
-
**Source files**: `packages/devtools/infrastructure/domains/health/application/ports/`
|
|
203
|
-
|
|
204
|
-
### Key Ports
|
|
205
|
-
|
|
206
|
-
**IStackRepository** - Stack management operations (CloudFormation, Deployment Manager, ARM)
|
|
207
|
-
```javascript
|
|
208
|
-
// Example methods:
|
|
209
|
-
async getStack(identifier)
|
|
210
|
-
async listResources(identifier)
|
|
211
|
-
async getOutputs(identifier)
|
|
212
|
-
```
|
|
213
|
-
📄 See: `application/ports/IStackRepository.js`
|
|
214
|
-
|
|
215
|
-
**IResourceDetector** - Cloud resource discovery (AWS APIs, GCP APIs, Azure APIs)
|
|
216
|
-
```javascript
|
|
217
|
-
// Example methods:
|
|
218
|
-
async detectNetworks(region)
|
|
219
|
-
async detectDatabases(region)
|
|
220
|
-
async detectKeys(region)
|
|
221
|
-
async detectResourceById(physicalId, resourceType)
|
|
222
|
-
```
|
|
223
|
-
📄 See: `application/ports/IResourceDetector.js`
|
|
224
|
-
|
|
225
|
-
**IDriftDetector** - Compare desired state vs actual state
|
|
226
|
-
```javascript
|
|
227
|
-
// Example methods:
|
|
228
|
-
async detectDrift(resource, desiredProperties)
|
|
229
|
-
async detectStackDrift(identifier)
|
|
230
|
-
```
|
|
231
|
-
📄 See: `application/ports/IDriftDetector.js`
|
|
232
|
-
|
|
233
|
-
**IResourceImporter** - Import existing resources into stack
|
|
234
|
-
```javascript
|
|
235
|
-
// Example methods:
|
|
236
|
-
isImportable(resourceType)
|
|
237
|
-
async createImportChangeSet(stackId, resources)
|
|
238
|
-
async executeImport(changeSet)
|
|
239
|
-
```
|
|
240
|
-
📄 See: `application/ports/IResourceImporter.js`
|
|
241
|
-
|
|
242
|
-
**IPropertyReconciler** - Fix property mismatches
|
|
243
|
-
```javascript
|
|
244
|
-
// Example methods:
|
|
245
|
-
async reconcile(mismatch, resource)
|
|
246
|
-
async planReconciliation(mismatches)
|
|
247
|
-
```
|
|
248
|
-
📄 See: `application/ports/IPropertyReconciler.js`
|
|
249
|
-
|
|
250
|
-
> **Note**: Full interface definitions are maintained in source files. See the actual TypeScript/JSDoc definitions for complete method signatures and documentation.
|
|
251
|
-
|
|
252
|
-
---
|
|
253
|
-
|
|
254
|
-
## AWS Adapter Implementations
|
|
255
|
-
|
|
256
|
-
AWS-specific implementations of the port interfaces using AWS SDK v3.
|
|
257
|
-
|
|
258
|
-
**Source files**: `packages/devtools/infrastructure/domains/health/infrastructure/adapters/aws/`
|
|
259
|
-
|
|
260
|
-
### AWSStackRepository
|
|
261
|
-
|
|
262
|
-
Implements `IStackRepository` using CloudFormation API.
|
|
263
|
-
|
|
264
|
-
```javascript
|
|
265
|
-
class AWSStackRepository extends IStackRepository {
|
|
266
|
-
constructor({ region }) {
|
|
267
|
-
this.client = new CloudFormationClient({ region });
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
async getStack(identifier) {
|
|
271
|
-
// Uses DescribeStacksCommand
|
|
272
|
-
// Returns Stack or null if not found
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
async listResources(identifier) {
|
|
276
|
-
// Uses ListStackResourcesCommand
|
|
277
|
-
// Maps to standard resource format
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
📄 See full implementation: `adapters/aws/AWSStackRepository.js`
|
|
283
|
-
|
|
284
|
-
### AWSResourceDetector
|
|
285
|
-
|
|
286
|
-
Implements `IResourceDetector` using AWS service APIs (EC2, RDS, KMS, etc.).
|
|
287
|
-
|
|
288
|
-
```javascript
|
|
289
|
-
class AWSResourceDetector extends IResourceDetector {
|
|
290
|
-
constructor({ region }) {
|
|
291
|
-
this.ec2 = new EC2Client({ region });
|
|
292
|
-
this.rds = new RDSClient({ region });
|
|
293
|
-
this.kms = new KMSClient({ region });
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
async detectNetworks(region) {
|
|
297
|
-
// Uses DescribeVpcsCommand
|
|
298
|
-
// Returns standardized network resources
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
async detectDatabases(region) {
|
|
302
|
-
// Uses DescribeDBClustersCommand
|
|
303
|
-
// Returns standardized database resources
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
async detectKeys(region) {
|
|
307
|
-
// Uses ListKeysCommand + DescribeKeyCommand
|
|
308
|
-
// Returns standardized key resources
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
📄 See full implementation: `adapters/aws/AWSResourceDetector.js`
|
|
314
|
-
|
|
315
|
-
### Other AWS Adapters
|
|
316
|
-
|
|
317
|
-
- **AWSDriftDetector** - Uses CloudFormation drift detection API
|
|
318
|
-
- **AWSResourceImporter** - Uses CloudFormation import change sets
|
|
319
|
-
- **AWSPropertyReconciler** - Uses CloudFormation update stacks
|
|
320
|
-
|
|
321
|
-
📄 See: `adapters/aws/` directory for all implementations
|
|
322
|
-
|
|
323
|
-
---
|
|
324
|
-
|
|
325
|
-
## Provider Registry
|
|
326
|
-
|
|
327
|
-
### registry.js
|
|
328
|
-
|
|
329
|
-
```javascript
|
|
330
|
-
/**
|
|
331
|
-
* Provider Registry
|
|
332
|
-
*
|
|
333
|
-
* Maps provider names to their implementations
|
|
334
|
-
*/
|
|
335
|
-
|
|
336
|
-
class ProviderRegistry {
|
|
337
|
-
constructor() {
|
|
338
|
-
this.providers = new Map();
|
|
339
|
-
this.registerBuiltInProviders();
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
registerBuiltInProviders() {
|
|
343
|
-
// Register AWS (available today)
|
|
344
|
-
this.register('aws', require('./aws-provider'));
|
|
345
|
-
|
|
346
|
-
// Register GCP (future - throws helpful error)
|
|
347
|
-
this.register('gcp', {
|
|
348
|
-
name: 'GCP',
|
|
349
|
-
available: false,
|
|
350
|
-
message: 'GCP support is planned but not yet implemented',
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
// Register Azure (future - throws helpful error)
|
|
354
|
-
this.register('azure', {
|
|
355
|
-
name: 'Azure',
|
|
356
|
-
available: false,
|
|
357
|
-
message: 'Azure support is planned but not yet implemented',
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
register(name, provider) {
|
|
362
|
-
this.providers.set(name, provider);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
get(name) {
|
|
366
|
-
const provider = this.providers.get(name);
|
|
367
|
-
|
|
368
|
-
if (!provider) {
|
|
369
|
-
throw new Error(`Unknown provider: ${name}. Supported providers: ${Array.from(this.providers.keys()).join(', ')}`);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (provider.available === false) {
|
|
373
|
-
throw new Error(`${provider.name} provider is not yet available. ${provider.message}`);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
return provider;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
isAvailable(name) {
|
|
380
|
-
const provider = this.providers.get(name);
|
|
381
|
-
return provider && provider.available !== false;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
getSupportedProviders() {
|
|
385
|
-
return Array.from(this.providers.keys()).filter(name => this.isAvailable(name));
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
module.exports = new ProviderRegistry();
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
### aws-provider.js
|
|
393
|
-
|
|
394
|
-
```javascript
|
|
395
|
-
/**
|
|
396
|
-
* AWS Provider Definition
|
|
397
|
-
*
|
|
398
|
-
* Factory for AWS-specific implementations
|
|
399
|
-
*/
|
|
400
|
-
|
|
401
|
-
const AWSStackRepository = require('../domains/health/infrastructure/adapters/aws/AWSStackRepository');
|
|
402
|
-
const AWSResourceDetector = require('../domains/health/infrastructure/adapters/aws/AWSResourceDetector');
|
|
403
|
-
const AWSDriftDetector = require('../domains/health/infrastructure/adapters/aws/AWSDriftDetector');
|
|
404
|
-
const AWSResourceImporter = require('../domains/health/infrastructure/adapters/aws/AWSResourceImporter');
|
|
405
|
-
const AWSPropertyReconciler = require('../domains/health/infrastructure/adapters/aws/AWSPropertyReconciler');
|
|
406
|
-
|
|
407
|
-
// Domain builders
|
|
408
|
-
const VpcBuilder = require('../domains/networking/aws/vpc-builder');
|
|
409
|
-
const KmsBuilder = require('../domains/security/aws/kms-builder');
|
|
410
|
-
const AuroraBuilder = require('../domains/database/aws/aurora-builder');
|
|
411
|
-
const MigrationBuilder = require('../domains/database/aws/migration-builder');
|
|
412
|
-
|
|
413
|
-
module.exports = {
|
|
414
|
-
name: 'AWS',
|
|
415
|
-
available: true,
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Create health check adapters for AWS
|
|
419
|
-
*/
|
|
420
|
-
createHealthAdapters({ region }) {
|
|
421
|
-
return {
|
|
422
|
-
stackRepository: new AWSStackRepository({ region }),
|
|
423
|
-
resourceDetector: new AWSResourceDetector({ region }),
|
|
424
|
-
driftDetector: new AWSDriftDetector({ region }),
|
|
425
|
-
resourceImporter: new AWSResourceImporter({ region }),
|
|
426
|
-
propertyReconciler: new AWSPropertyReconciler({ region }),
|
|
427
|
-
};
|
|
428
|
-
},
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* Get infrastructure builders for AWS
|
|
432
|
-
*/
|
|
433
|
-
getBuilders() {
|
|
434
|
-
return [
|
|
435
|
-
new VpcBuilder(),
|
|
436
|
-
new KmsBuilder(),
|
|
437
|
-
new AuroraBuilder(),
|
|
438
|
-
new MigrationBuilder(),
|
|
439
|
-
];
|
|
440
|
-
},
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* Get supported resource types
|
|
444
|
-
*/
|
|
445
|
-
getSupportedResourceTypes() {
|
|
446
|
-
return [
|
|
447
|
-
'AWS::EC2::VPC',
|
|
448
|
-
'AWS::EC2::Subnet',
|
|
449
|
-
'AWS::EC2::SecurityGroup',
|
|
450
|
-
'AWS::RDS::DBCluster',
|
|
451
|
-
'AWS::RDS::DBInstance',
|
|
452
|
-
'AWS::KMS::Key',
|
|
453
|
-
'AWS::Lambda::Function',
|
|
454
|
-
'AWS::SQS::Queue',
|
|
455
|
-
'AWS::S3::Bucket',
|
|
456
|
-
// ... more
|
|
457
|
-
];
|
|
458
|
-
},
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* Get resource property metadata
|
|
462
|
-
*/
|
|
463
|
-
getResourceMetadata(resourceType) {
|
|
464
|
-
return require(`./metadata/${resourceType.replace(/::/g, '_')}.json`);
|
|
465
|
-
},
|
|
466
|
-
};
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
---
|
|
470
|
-
|
|
471
|
-
## Use Case Integration with Providers
|
|
472
|
-
|
|
473
|
-
### RunHealthCheckUseCase
|
|
474
|
-
|
|
475
|
-
```javascript
|
|
476
|
-
const ProviderRegistry = require('../../../providers/registry');
|
|
477
|
-
|
|
478
|
-
class RunHealthCheckUseCase {
|
|
479
|
-
/**
|
|
480
|
-
* @param {Object} dependencies - Injected dependencies (optional)
|
|
481
|
-
*/
|
|
482
|
-
constructor(dependencies = {}) {
|
|
483
|
-
this.dependencies = dependencies;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* Execute health check
|
|
488
|
-
*
|
|
489
|
-
* @param {Object} params
|
|
490
|
-
* @param {string} params.stackName - CloudFormation stack name
|
|
491
|
-
* @param {string} params.region - Cloud provider region
|
|
492
|
-
* @param {string} params.provider - Provider name ('aws', 'gcp', 'azure')
|
|
493
|
-
* @param {Object} params.appDefinition - App definition (desired state)
|
|
494
|
-
*/
|
|
495
|
-
async execute({ stackName, region, provider, appDefinition }) {
|
|
496
|
-
// Get provider-specific adapters
|
|
497
|
-
const providerImpl = ProviderRegistry.get(provider);
|
|
498
|
-
const adapters = this.dependencies.adapters || providerImpl.createHealthAdapters({ region });
|
|
499
|
-
|
|
500
|
-
// Step 1: Get stack state
|
|
501
|
-
const stackIdentifier = new StackIdentifier({ stackName, region });
|
|
502
|
-
const stack = await adapters.stackRepository.getStack(stackIdentifier);
|
|
503
|
-
|
|
504
|
-
if (!stack) {
|
|
505
|
-
return StackHealthReport.createForMissingStack(stackIdentifier);
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Step 2: Discover resources
|
|
509
|
-
const stackResources = await adapters.stackRepository.listResources(stackIdentifier);
|
|
510
|
-
const cloudResources = await this.discoverCloudResources(adapters.resourceDetector, region);
|
|
511
|
-
|
|
512
|
-
// Step 3: Detect issues
|
|
513
|
-
const orphanedResources = this.detectOrphaned(stackResources, cloudResources);
|
|
514
|
-
const missingResources = this.detectMissing(stackResources, cloudResources);
|
|
515
|
-
const driftedResources = await this.detectDrift(stackResources, cloudResources, adapters.driftDetector);
|
|
516
|
-
|
|
517
|
-
// Step 4: Calculate health score
|
|
518
|
-
const healthScore = HealthScoreCalculator.calculate({
|
|
519
|
-
orphaned: orphanedResources.length,
|
|
520
|
-
missing: missingResources.length,
|
|
521
|
-
drifted: driftedResources.length,
|
|
522
|
-
total: stackResources.length,
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
// Step 5: Build health report
|
|
526
|
-
return new StackHealthReport({
|
|
527
|
-
stackIdentifier,
|
|
528
|
-
healthScore,
|
|
529
|
-
orphanedResources,
|
|
530
|
-
missingResources,
|
|
531
|
-
driftedResources,
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
async discoverCloudResources(detector, region) {
|
|
536
|
-
const [networks, databases, keys] = await Promise.all([
|
|
537
|
-
detector.detectNetworks(region),
|
|
538
|
-
detector.detectDatabases(region),
|
|
539
|
-
detector.detectKeys(region),
|
|
540
|
-
]);
|
|
541
|
-
|
|
542
|
-
return [...networks, ...databases, ...keys];
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
detectOrphaned(stackResources, cloudResources) {
|
|
546
|
-
// Resources in cloud but not in stack
|
|
547
|
-
const stackPhysicalIds = new Set(stackResources.map(r => r.physicalId));
|
|
548
|
-
return cloudResources.filter(r => !stackPhysicalIds.has(r.physicalId));
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
detectMissing(stackResources, cloudResources) {
|
|
552
|
-
// Resources in stack but not in cloud
|
|
553
|
-
const cloudPhysicalIds = new Set(cloudResources.map(r => r.physicalId));
|
|
554
|
-
return stackResources.filter(r => !cloudPhysicalIds.has(r.physicalId));
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
async detectDrift(stackResources, cloudResources, driftDetector) {
|
|
558
|
-
const drifted = [];
|
|
559
|
-
|
|
560
|
-
for (const stackResource of stackResources) {
|
|
561
|
-
const cloudResource = cloudResources.find(r => r.physicalId === stackResource.physicalId);
|
|
562
|
-
|
|
563
|
-
if (cloudResource) {
|
|
564
|
-
const mismatches = await driftDetector.detectDrift(stackResource, cloudResource.properties);
|
|
565
|
-
|
|
566
|
-
if (mismatches.length > 0) {
|
|
567
|
-
drifted.push({
|
|
568
|
-
resource: stackResource,
|
|
569
|
-
mismatches,
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
return drifted;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
module.exports = RunHealthCheckUseCase;
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
---
|
|
583
|
-
|
|
584
|
-
## CLI Command Integration
|
|
585
|
-
|
|
586
|
-
### doctor-command.js
|
|
587
|
-
|
|
588
|
-
```javascript
|
|
589
|
-
const ProviderRegistry = require('../../providers/registry');
|
|
590
|
-
const RunHealthCheckUseCase = require('../../domains/health/application/use-cases/run-health-check-use-case');
|
|
591
|
-
const HealthReportPresenter = require('./presenters/health-report-presenter');
|
|
592
|
-
|
|
593
|
-
async function doctorCommand(options) {
|
|
594
|
-
console.log('🩺 Running infrastructure health check...\n');
|
|
595
|
-
|
|
596
|
-
// Load app definition
|
|
597
|
-
const appDefinition = loadAppDefinition();
|
|
598
|
-
const provider = options.provider || appDefinition.provider || 'aws';
|
|
599
|
-
const region = options.region || appDefinition.region;
|
|
600
|
-
const stackName = options.stack || `${appDefinition.name}-${options.stage || 'dev'}`;
|
|
601
|
-
|
|
602
|
-
// Verify provider is supported
|
|
603
|
-
if (!ProviderRegistry.isAvailable(provider)) {
|
|
604
|
-
console.error(`❌ Provider '${provider}' is not supported`);
|
|
605
|
-
console.log(`Supported providers: ${ProviderRegistry.getSupportedProviders().join(', ')}`);
|
|
606
|
-
process.exit(1);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// Create use case (adapters created automatically based on provider)
|
|
610
|
-
const useCase = new RunHealthCheckUseCase();
|
|
611
|
-
|
|
612
|
-
// Execute health check
|
|
613
|
-
const report = await useCase.execute({
|
|
614
|
-
stackName,
|
|
615
|
-
region,
|
|
616
|
-
provider,
|
|
617
|
-
appDefinition,
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
// Present results
|
|
621
|
-
const presenter = new HealthReportPresenter({ format: options.format || 'table' });
|
|
622
|
-
presenter.present(report);
|
|
623
|
-
|
|
624
|
-
// Exit with code based on health score (if requested)
|
|
625
|
-
if (options.exitCode) {
|
|
626
|
-
if (report.healthScore.isHealthy()) {
|
|
627
|
-
process.exit(0);
|
|
628
|
-
} else if (report.healthScore.isDegraded()) {
|
|
629
|
-
process.exit(1);
|
|
630
|
-
} else {
|
|
631
|
-
process.exit(2);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
module.exports = doctorCommand;
|
|
637
|
-
```
|
|
638
|
-
|
|
639
|
-
---
|
|
640
|
-
|
|
641
|
-
## Extension Points for Future Providers
|
|
642
|
-
|
|
643
|
-
### Adding GCP Support
|
|
644
|
-
|
|
645
|
-
To add GCP support in the future:
|
|
646
|
-
|
|
647
|
-
1. **Create GCP adapters**:
|
|
648
|
-
```
|
|
649
|
-
infrastructure/domains/health/infrastructure/adapters/gcp/
|
|
650
|
-
├── GCPStackRepository.js # Deployment Manager
|
|
651
|
-
├── GCPResourceDetector.js # GCP APIs
|
|
652
|
-
├── GCPDriftDetector.js
|
|
653
|
-
├── GCPResourceImporter.js
|
|
654
|
-
└── GCPPropertyReconciler.js
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
2. **Create GCP builders**:
|
|
658
|
-
```
|
|
659
|
-
infrastructure/domains/networking/gcp/network-builder.js
|
|
660
|
-
infrastructure/domains/database/gcp/cloud-sql-builder.js
|
|
661
|
-
infrastructure/domains/security/gcp/kms-builder.js
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
3. **Register GCP provider**:
|
|
665
|
-
```javascript
|
|
666
|
-
// providers/gcp-provider.js
|
|
667
|
-
module.exports = {
|
|
668
|
-
name: 'GCP',
|
|
669
|
-
available: true,
|
|
670
|
-
createHealthAdapters({ region }) { ... },
|
|
671
|
-
getBuilders() { ... },
|
|
672
|
-
getSupportedResourceTypes() { ... },
|
|
673
|
-
};
|
|
674
|
-
```
|
|
675
|
-
|
|
676
|
-
4. **Update registry**:
|
|
677
|
-
```javascript
|
|
678
|
-
// providers/registry.js
|
|
679
|
-
this.register('gcp', require('./gcp-provider'));
|
|
680
|
-
```
|
|
681
|
-
|
|
682
|
-
**No changes required to**:
|
|
683
|
-
- Domain entities (Resource, Issue, StackHealthReport)
|
|
684
|
-
- Value objects (HealthScore, ResourceState)
|
|
685
|
-
- Use cases (RunHealthCheckUseCase, RepairStackViaImportUseCase)
|
|
686
|
-
- CLI commands (doctor-command.js, repair-command.js)
|
|
687
|
-
|
|
688
|
-
The hexagonal architecture ensures new providers only require implementing the port interfaces!
|
|
689
|
-
|
|
690
|
-
---
|
|
691
|
-
|
|
692
|
-
## Summary
|
|
693
|
-
|
|
694
|
-
This architecture achieves:
|
|
695
|
-
|
|
696
|
-
✅ **Multi-cloud ready** - Ports & Adapters make provider swapping trivial
|
|
697
|
-
✅ **Provider-specific domains clear** - Obvious where AWS/GCP/Azure diverge
|
|
698
|
-
✅ **Testable** - Mock port interfaces for unit tests
|
|
699
|
-
✅ **Extensible** - Add new providers without touching domain logic
|
|
700
|
-
✅ **Explicit** - Provider selection obvious in app definition
|
|
701
|
-
✅ **Future-proof** - Non-serverless (Docker) can be added as another provider
|
|
702
|
-
|
|
703
|
-
**AWS works today**, and the path to GCP/Azure/Cloudflare is clear and isolated to adapter implementations.
|