@friggframework/devtools 2.0.0--canary.474.86c5119.0 → 2.0.0--canary.474.6a0bba7.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/infrastructure/domains/database/migration-builder.js +199 -1
- package/infrastructure/domains/database/migration-builder.test.js +73 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
- package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +6 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +307 -1
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +38 -5
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +3 -3
- package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
- package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
- package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
- package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
- package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
- package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
- package/infrastructure/domains/health/domain/entities/issue.js +50 -1
- package/infrastructure/domains/health/domain/entities/issue.test.js +111 -0
- package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
- package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
- package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
- package/infrastructure/domains/health/domain/services/health-score-calculator.js +174 -91
- package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +332 -228
- package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
- package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +407 -20
- package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +698 -26
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +108 -14
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +69 -12
- package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +392 -1
- package/package.json +6 -6
package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js
CHANGED
|
@@ -67,6 +67,8 @@ class ReconcilePropertiesUseCase {
|
|
|
67
67
|
* @param {Object} params
|
|
68
68
|
* @param {StackIdentifier} params.stackIdentifier - Stack identifier
|
|
69
69
|
* @param {string} params.logicalId - Logical resource ID
|
|
70
|
+
* @param {string} [params.physicalId] - Physical resource ID (required for resource mode)
|
|
71
|
+
* @param {string} [params.resourceType] - Resource type (required for resource mode)
|
|
70
72
|
* @param {PropertyMismatch[]} params.mismatches - Property mismatches to reconcile
|
|
71
73
|
* @param {string} [params.mode='template'] - Reconciliation mode
|
|
72
74
|
* @returns {Promise<Object>} Batch reconciliation result
|
|
@@ -74,6 +76,8 @@ class ReconcilePropertiesUseCase {
|
|
|
74
76
|
async reconcileMultipleProperties({
|
|
75
77
|
stackIdentifier,
|
|
76
78
|
logicalId,
|
|
79
|
+
physicalId,
|
|
80
|
+
resourceType,
|
|
77
81
|
mismatches,
|
|
78
82
|
mode = 'template',
|
|
79
83
|
}) {
|
|
@@ -107,6 +111,8 @@ class ReconcilePropertiesUseCase {
|
|
|
107
111
|
const batchResult = await this.propertyReconciler.reconcileMultipleProperties({
|
|
108
112
|
stackIdentifier,
|
|
109
113
|
logicalId,
|
|
114
|
+
physicalId,
|
|
115
|
+
resourceType,
|
|
110
116
|
mismatches: reconcilableProperties,
|
|
111
117
|
mode,
|
|
112
118
|
});
|
|
@@ -12,8 +12,12 @@
|
|
|
12
12
|
* - Generate CloudFormation template snippets
|
|
13
13
|
* - Execute import operations (single or batch)
|
|
14
14
|
* - Track import operation status
|
|
15
|
+
* - Map orphaned resources to correct logical IDs using template comparison
|
|
15
16
|
*/
|
|
16
17
|
|
|
18
|
+
const { TemplateParser } = require('../../domain/services/template-parser');
|
|
19
|
+
const { LogicalIdMapper } = require('../../domain/services/logical-id-mapper');
|
|
20
|
+
|
|
17
21
|
class RepairViaImportUseCase {
|
|
18
22
|
/**
|
|
19
23
|
* Create use case with required dependencies
|
|
@@ -21,8 +25,11 @@ class RepairViaImportUseCase {
|
|
|
21
25
|
* @param {Object} params
|
|
22
26
|
* @param {IResourceImporter} params.resourceImporter - Resource import operations
|
|
23
27
|
* @param {IResourceDetector} params.resourceDetector - Resource discovery and details
|
|
28
|
+
* @param {IStackRepository} params.stackRepository - CloudFormation stack operations
|
|
29
|
+
* @param {TemplateParser} params.templateParser - CloudFormation template parsing
|
|
30
|
+
* @param {LogicalIdMapper} params.logicalIdMapper - Logical ID mapping service
|
|
24
31
|
*/
|
|
25
|
-
constructor({ resourceImporter, resourceDetector }) {
|
|
32
|
+
constructor({ resourceImporter, resourceDetector, stackRepository, templateParser, logicalIdMapper }) {
|
|
26
33
|
if (!resourceImporter) {
|
|
27
34
|
throw new Error('resourceImporter is required');
|
|
28
35
|
}
|
|
@@ -32,6 +39,9 @@ class RepairViaImportUseCase {
|
|
|
32
39
|
|
|
33
40
|
this.resourceImporter = resourceImporter;
|
|
34
41
|
this.resourceDetector = resourceDetector;
|
|
42
|
+
this.stackRepository = stackRepository;
|
|
43
|
+
this.templateParser = templateParser || new TemplateParser();
|
|
44
|
+
this.logicalIdMapper = logicalIdMapper || new LogicalIdMapper({ region: 'us-east-1' });
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
/**
|
|
@@ -224,6 +234,302 @@ class RepairViaImportUseCase {
|
|
|
224
234
|
properties: resourceDetails.properties,
|
|
225
235
|
};
|
|
226
236
|
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Import orphaned resources with automatic logical ID mapping
|
|
240
|
+
* Uses template comparison to find correct logical IDs
|
|
241
|
+
*
|
|
242
|
+
* @param {Object} params
|
|
243
|
+
* @param {StackIdentifier} params.stackIdentifier - Target stack
|
|
244
|
+
* @param {Array} params.orphanedResources - Orphaned resources to import
|
|
245
|
+
* @param {string} params.buildTemplatePath - Path to .serverless/cloudformation-template-update-stack.json
|
|
246
|
+
* @returns {Promise<Object>} Import result with mappings
|
|
247
|
+
*/
|
|
248
|
+
async importWithLogicalIdMapping({ stackIdentifier, orphanedResources, buildTemplatePath }) {
|
|
249
|
+
// 1. Validate build template exists
|
|
250
|
+
if (!buildTemplatePath) {
|
|
251
|
+
throw new Error('buildTemplatePath is required');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const fs = require('fs');
|
|
255
|
+
if (!fs.existsSync(buildTemplatePath)) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
`Build template not found at: ${buildTemplatePath}\n\n` +
|
|
258
|
+
`Please run one of:\n` +
|
|
259
|
+
` • serverless package\n` +
|
|
260
|
+
` • frigg build\n` +
|
|
261
|
+
` • frigg deploy --stage dev\n\n` +
|
|
262
|
+
`Then try again:\n` +
|
|
263
|
+
` frigg repair --import ${stackIdentifier.stackName}`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 2. Parse build template
|
|
268
|
+
const buildTemplate = this.templateParser.parseTemplate(buildTemplatePath);
|
|
269
|
+
|
|
270
|
+
// 3. Get deployed template from CloudFormation
|
|
271
|
+
if (!this.stackRepository) {
|
|
272
|
+
throw new Error('stackRepository is required for template comparison');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const deployedTemplate = await this.stackRepository.getTemplate(stackIdentifier);
|
|
276
|
+
|
|
277
|
+
// 4. Map orphaned resources to logical IDs
|
|
278
|
+
const mappings = await this.logicalIdMapper.mapOrphanedResourcesToLogicalIds({
|
|
279
|
+
orphanedResources,
|
|
280
|
+
buildTemplate,
|
|
281
|
+
deployedTemplate,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// 5. Filter out unmapped resources
|
|
285
|
+
const mappedResources = mappings.filter((m) => m.logicalId !== null);
|
|
286
|
+
const unmappedResources = mappings.filter((m) => m.logicalId === null);
|
|
287
|
+
|
|
288
|
+
if (mappedResources.length === 0) {
|
|
289
|
+
return {
|
|
290
|
+
success: false,
|
|
291
|
+
message: 'No resources could be mapped to logical IDs',
|
|
292
|
+
unmappedCount: unmappedResources.length,
|
|
293
|
+
unmappedResources,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 6. Deduplicate: Select ONE resource per logical ID based on deployed template
|
|
298
|
+
const { selectedResources, duplicates } = this._deduplicateResourcesByLogicalId(
|
|
299
|
+
mappedResources,
|
|
300
|
+
deployedTemplate
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
// 7. Check for warnings
|
|
304
|
+
const multiResourceWarnings = this._checkForMultipleResources(duplicates);
|
|
305
|
+
|
|
306
|
+
// 8. Generate import-resources.json format using SELECTED resources
|
|
307
|
+
const resourcesToImport = selectedResources.map((mapping) => ({
|
|
308
|
+
ResourceType: mapping.resourceType,
|
|
309
|
+
LogicalResourceId: mapping.logicalId,
|
|
310
|
+
ResourceIdentifier: this._getResourceIdentifier(mapping),
|
|
311
|
+
}));
|
|
312
|
+
|
|
313
|
+
// 9. Return result with deduplication info
|
|
314
|
+
return {
|
|
315
|
+
success: true,
|
|
316
|
+
mappedCount: selectedResources.length,
|
|
317
|
+
unmappedCount: unmappedResources.length,
|
|
318
|
+
duplicatesRemoved: duplicates.length,
|
|
319
|
+
mappings: selectedResources,
|
|
320
|
+
unmappedResources,
|
|
321
|
+
duplicates, // Resources that were filtered out
|
|
322
|
+
resourcesToImport,
|
|
323
|
+
warnings: multiResourceWarnings,
|
|
324
|
+
buildTemplatePath,
|
|
325
|
+
deployedTemplatePath: 'CloudFormation (deployed)',
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Deduplicate resources: Select ONE resource per logical ID
|
|
331
|
+
* When multiple resources have the same logical ID, pick the one that's
|
|
332
|
+
* actually referenced in the deployed template.
|
|
333
|
+
*
|
|
334
|
+
* @param {Array} mappedResources - Resources with logical IDs
|
|
335
|
+
* @param {Object} deployedTemplate - Deployed CloudFormation template
|
|
336
|
+
* @returns {Object} { selectedResources, duplicates }
|
|
337
|
+
* @private
|
|
338
|
+
*/
|
|
339
|
+
_deduplicateResourcesByLogicalId(mappedResources, deployedTemplate) {
|
|
340
|
+
// Group resources by logical ID
|
|
341
|
+
const byLogicalId = {};
|
|
342
|
+
mappedResources.forEach((resource) => {
|
|
343
|
+
if (!byLogicalId[resource.logicalId]) {
|
|
344
|
+
byLogicalId[resource.logicalId] = [];
|
|
345
|
+
}
|
|
346
|
+
byLogicalId[resource.logicalId].push(resource);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Extract all physical IDs referenced in deployed template
|
|
350
|
+
const referencedIds = this._extractReferencedIdsFromTemplate(deployedTemplate);
|
|
351
|
+
|
|
352
|
+
const selectedResources = [];
|
|
353
|
+
const duplicates = [];
|
|
354
|
+
|
|
355
|
+
// For each logical ID, select ONE resource
|
|
356
|
+
Object.entries(byLogicalId).forEach(([logicalId, resources]) => {
|
|
357
|
+
if (resources.length === 1) {
|
|
358
|
+
// Only one resource - select it
|
|
359
|
+
selectedResources.push(resources[0]);
|
|
360
|
+
} else {
|
|
361
|
+
// Multiple resources - pick the one in deployed template
|
|
362
|
+
let selected = null;
|
|
363
|
+
|
|
364
|
+
// Try to find resource that's actually referenced
|
|
365
|
+
for (const resource of resources) {
|
|
366
|
+
if (this._isResourceReferenced(resource, referencedIds)) {
|
|
367
|
+
selected = resource;
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Fallback: If none are referenced, pick first one
|
|
373
|
+
if (!selected) {
|
|
374
|
+
selected = resources[0];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
selectedResources.push(selected);
|
|
378
|
+
|
|
379
|
+
// Mark others as duplicates
|
|
380
|
+
resources.forEach((r) => {
|
|
381
|
+
if (r.physicalId !== selected.physicalId) {
|
|
382
|
+
duplicates.push(r);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
return { selectedResources, duplicates };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Extract all physical resource IDs referenced in deployed template
|
|
393
|
+
* Looks for hardcoded IDs in Lambda VPC configs, security group rules, etc.
|
|
394
|
+
* @private
|
|
395
|
+
*/
|
|
396
|
+
_extractReferencedIdsFromTemplate(template) {
|
|
397
|
+
const referenced = {
|
|
398
|
+
vpcIds: new Set(),
|
|
399
|
+
subnetIds: new Set(),
|
|
400
|
+
securityGroupIds: new Set(),
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
if (!template || !template.resources) {
|
|
404
|
+
return referenced;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Traverse all resources in template
|
|
408
|
+
Object.values(template.resources).forEach((resource) => {
|
|
409
|
+
// Lambda VPC config contains hardcoded IDs
|
|
410
|
+
if (
|
|
411
|
+
resource.Type === 'AWS::Lambda::Function' &&
|
|
412
|
+
resource.Properties?.VpcConfig
|
|
413
|
+
) {
|
|
414
|
+
const { SubnetIds, SecurityGroupIds } = resource.Properties.VpcConfig;
|
|
415
|
+
|
|
416
|
+
if (SubnetIds) {
|
|
417
|
+
SubnetIds.forEach((id) => {
|
|
418
|
+
if (typeof id === 'string' && id.startsWith('subnet-')) {
|
|
419
|
+
referenced.subnetIds.add(id);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (SecurityGroupIds) {
|
|
425
|
+
SecurityGroupIds.forEach((id) => {
|
|
426
|
+
if (typeof id === 'string' && id.startsWith('sg-')) {
|
|
427
|
+
referenced.securityGroupIds.add(id);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Security group rules may reference other security groups
|
|
434
|
+
if (resource.Type === 'AWS::EC2::SecurityGroupIngress' ||
|
|
435
|
+
resource.Type === 'AWS::EC2::SecurityGroupEgress') {
|
|
436
|
+
const groupId = resource.Properties?.GroupId;
|
|
437
|
+
const sourceSecurityGroupId = resource.Properties?.SourceSecurityGroupId;
|
|
438
|
+
|
|
439
|
+
if (typeof groupId === 'string' && groupId.startsWith('sg-')) {
|
|
440
|
+
referenced.securityGroupIds.add(groupId);
|
|
441
|
+
}
|
|
442
|
+
if (typeof sourceSecurityGroupId === 'string' && sourceSecurityGroupId.startsWith('sg-')) {
|
|
443
|
+
referenced.securityGroupIds.add(sourceSecurityGroupId);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
return referenced;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Check if a resource is referenced in the deployed template
|
|
453
|
+
* @private
|
|
454
|
+
*/
|
|
455
|
+
_isResourceReferenced(resource, referencedIds) {
|
|
456
|
+
const { resourceType, physicalId } = resource;
|
|
457
|
+
|
|
458
|
+
if (resourceType === 'AWS::EC2::VPC') {
|
|
459
|
+
return referencedIds.vpcIds.has(physicalId);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (resourceType === 'AWS::EC2::Subnet') {
|
|
463
|
+
return referencedIds.subnetIds.has(physicalId);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (resourceType === 'AWS::EC2::SecurityGroup') {
|
|
467
|
+
return referencedIds.securityGroupIds.has(physicalId);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// For other resource types, we can't determine
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Check for multiple resources of same type
|
|
476
|
+
* Returns warnings when user needs to manually select
|
|
477
|
+
* @private
|
|
478
|
+
*/
|
|
479
|
+
_checkForMultipleResources(mappings) {
|
|
480
|
+
const warnings = [];
|
|
481
|
+
const byType = {};
|
|
482
|
+
|
|
483
|
+
// Group by resource type
|
|
484
|
+
mappings.forEach((mapping) => {
|
|
485
|
+
if (!byType[mapping.resourceType]) {
|
|
486
|
+
byType[mapping.resourceType] = [];
|
|
487
|
+
}
|
|
488
|
+
byType[mapping.resourceType].push(mapping);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// Check for multiples
|
|
492
|
+
Object.entries(byType).forEach(([type, resources]) => {
|
|
493
|
+
if (resources.length > 1) {
|
|
494
|
+
const shortType = type.replace('AWS::EC2::', '');
|
|
495
|
+
warnings.push({
|
|
496
|
+
type: 'MULTIPLE_RESOURCES',
|
|
497
|
+
resourceType: type,
|
|
498
|
+
count: resources.length,
|
|
499
|
+
message: `Multiple ${shortType}s detected (${resources.length}). Review relationships before importing.`,
|
|
500
|
+
resources: resources.map((r) => ({
|
|
501
|
+
physicalId: r.physicalId,
|
|
502
|
+
logicalId: r.logicalId,
|
|
503
|
+
matchMethod: r.matchMethod,
|
|
504
|
+
confidence: r.confidence,
|
|
505
|
+
})),
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
return warnings;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Get CloudFormation resource identifier for import
|
|
515
|
+
* @private
|
|
516
|
+
*/
|
|
517
|
+
_getResourceIdentifier(mapping) {
|
|
518
|
+
const { resourceType, physicalId } = mapping;
|
|
519
|
+
|
|
520
|
+
// Map resource types to their identifier format
|
|
521
|
+
const identifierMap = {
|
|
522
|
+
'AWS::EC2::VPC': { VpcId: physicalId },
|
|
523
|
+
'AWS::EC2::Subnet': { SubnetId: physicalId },
|
|
524
|
+
'AWS::EC2::SecurityGroup': { GroupId: physicalId },
|
|
525
|
+
'AWS::EC2::InternetGateway': { InternetGatewayId: physicalId },
|
|
526
|
+
'AWS::EC2::NatGateway': { NatGatewayId: physicalId },
|
|
527
|
+
'AWS::EC2::RouteTable': { RouteTableId: physicalId },
|
|
528
|
+
'AWS::EC2::VPCEndpoint': { VpcEndpointId: physicalId },
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
return identifierMap[resourceType] || { Id: physicalId };
|
|
532
|
+
}
|
|
227
533
|
}
|
|
228
534
|
|
|
229
535
|
module.exports = RepairViaImportUseCase;
|
|
@@ -19,6 +19,7 @@ const StackHealthReport = require('../../domain/entities/stack-health-report');
|
|
|
19
19
|
const Resource = require('../../domain/entities/resource');
|
|
20
20
|
const Issue = require('../../domain/entities/issue');
|
|
21
21
|
const ResourceState = require('../../domain/value-objects/resource-state');
|
|
22
|
+
const { getPropertyMutability } = require('../../domain/services/property-mutability-config');
|
|
22
23
|
|
|
23
24
|
class RunHealthCheckUseCase {
|
|
24
25
|
/**
|
|
@@ -55,16 +56,27 @@ class RunHealthCheckUseCase {
|
|
|
55
56
|
*
|
|
56
57
|
* @param {Object} params
|
|
57
58
|
* @param {StackIdentifier} params.stackIdentifier - Stack to check
|
|
59
|
+
* @param {Function} params.onProgress - Optional progress callback (step, message)
|
|
58
60
|
* @returns {Promise<StackHealthReport>} Comprehensive health report
|
|
59
61
|
*/
|
|
60
|
-
async execute({ stackIdentifier }) {
|
|
62
|
+
async execute({ stackIdentifier, onProgress }) {
|
|
63
|
+
// Helper to call progress callback if provided
|
|
64
|
+
const progress = (step, message) => {
|
|
65
|
+
if (onProgress) {
|
|
66
|
+
onProgress(step, message);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
61
70
|
// 1. Verify stack exists
|
|
71
|
+
progress('📋 Step 1/5:', 'Verifying stack exists...');
|
|
62
72
|
await this.stackRepository.getStack(stackIdentifier);
|
|
63
73
|
|
|
64
74
|
// 2. Detect stack-level drift
|
|
75
|
+
progress('🔍 Step 2/5:', 'Detecting stack drift...');
|
|
65
76
|
const driftDetection = await this.stackRepository.detectStackDrift(stackIdentifier);
|
|
66
77
|
|
|
67
78
|
// 3. Get all stack resources
|
|
79
|
+
progress('📊 Step 3/5:', 'Analyzing stack resources...');
|
|
68
80
|
const stackResources = await this.stackRepository.listResources(stackIdentifier);
|
|
69
81
|
|
|
70
82
|
// 4. Build resource entities with drift status
|
|
@@ -101,10 +113,23 @@ class RunHealthCheckUseCase {
|
|
|
101
113
|
resourceDrift.propertyDifferences &&
|
|
102
114
|
resourceDrift.propertyDifferences.length > 0
|
|
103
115
|
) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
)
|
|
116
|
+
// Build property mutability map for this resource type
|
|
117
|
+
// AWS drift detection returns property paths, we need to provide mutability for each
|
|
118
|
+
const propertyMutabilityMap = {};
|
|
119
|
+
for (const propDiff of resourceDrift.propertyDifferences) {
|
|
120
|
+
const propertyPath = propDiff.PropertyPath.replace(/^\//, ''); // Remove leading slash
|
|
121
|
+
propertyMutabilityMap[propertyPath] = getPropertyMutability(
|
|
122
|
+
stackResource.resourceType,
|
|
123
|
+
propertyPath
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const propertyMismatches = this.mismatchAnalyzer.analyze({
|
|
128
|
+
expected: resourceDrift.expectedProperties,
|
|
129
|
+
actual: resourceDrift.actualProperties,
|
|
130
|
+
propertyMutability: propertyMutabilityMap,
|
|
131
|
+
ignoreProperties: [],
|
|
132
|
+
});
|
|
108
133
|
|
|
109
134
|
// Create issue for each property mismatch using factory method
|
|
110
135
|
for (const mismatch of propertyMismatches) {
|
|
@@ -135,6 +160,7 @@ class RunHealthCheckUseCase {
|
|
|
135
160
|
}
|
|
136
161
|
|
|
137
162
|
// 5. Find orphaned resources (exist in cloud but not in stack)
|
|
163
|
+
progress('🔎 Step 4/5:', 'Checking for orphaned resources...');
|
|
138
164
|
const orphanedResources = await this.resourceDetector.findOrphanedResources({
|
|
139
165
|
stackIdentifier,
|
|
140
166
|
stackResources,
|
|
@@ -142,11 +168,17 @@ class RunHealthCheckUseCase {
|
|
|
142
168
|
|
|
143
169
|
for (const orphan of orphanedResources) {
|
|
144
170
|
// Create resource entity for orphan
|
|
171
|
+
// IMPORTANT: Include both properties AND tags for template comparison
|
|
172
|
+
// Tags are stored in properties.tags for logical ID mapping
|
|
145
173
|
const orphanResource = new Resource({
|
|
146
174
|
logicalId: null, // No logical ID (not in template)
|
|
147
175
|
physicalId: orphan.physicalId,
|
|
148
176
|
resourceType: orphan.resourceType,
|
|
149
177
|
state: ResourceState.ORPHANED,
|
|
178
|
+
properties: {
|
|
179
|
+
...(orphan.properties || {}), // Include AWS properties
|
|
180
|
+
tags: orphan.tags || {}, // Include tags for logical ID matching
|
|
181
|
+
},
|
|
150
182
|
});
|
|
151
183
|
|
|
152
184
|
resources.push(orphanResource);
|
|
@@ -162,6 +194,7 @@ class RunHealthCheckUseCase {
|
|
|
162
194
|
}
|
|
163
195
|
|
|
164
196
|
// 6. Calculate health score using domain service
|
|
197
|
+
progress('🧮 Step 5/5:', 'Calculating health score...');
|
|
165
198
|
const healthScore = this.healthScoreCalculator.calculate({ resources, issues });
|
|
166
199
|
|
|
167
200
|
// 7. Build comprehensive health report (aggregate root)
|
package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js
CHANGED
|
@@ -33,7 +33,7 @@ describe('RunHealthCheckUseCase', () => {
|
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
mockMismatchAnalyzer = {
|
|
36
|
-
|
|
36
|
+
analyze: jest.fn(),
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
mockHealthScoreCalculator = {
|
|
@@ -149,7 +149,7 @@ describe('RunHealthCheckUseCase', () => {
|
|
|
149
149
|
mutability: PropertyMutability.MUTABLE,
|
|
150
150
|
});
|
|
151
151
|
|
|
152
|
-
mockMismatchAnalyzer.
|
|
152
|
+
mockMismatchAnalyzer.analyze.mockReturnValue([propertyMismatch]);
|
|
153
153
|
|
|
154
154
|
mockResourceDetector.findOrphanedResources.mockResolvedValue([]);
|
|
155
155
|
|
|
@@ -323,7 +323,7 @@ describe('RunHealthCheckUseCase', () => {
|
|
|
323
323
|
mutability: PropertyMutability.MUTABLE,
|
|
324
324
|
});
|
|
325
325
|
|
|
326
|
-
mockMismatchAnalyzer.
|
|
326
|
+
mockMismatchAnalyzer.analyze.mockReturnValue([propertyMismatch]);
|
|
327
327
|
|
|
328
328
|
// Mock orphaned resources
|
|
329
329
|
mockResourceDetector.findOrphanedResources.mockResolvedValue([
|