@friggframework/devtools 2.0.0--canary.474.97bfcf0.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.
@@ -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.