@friggframework/devtools 2.0.0-next.45 → 2.0.0-next.46
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/ARCHITECTURE.md +487 -0
- package/infrastructure/HEALTH.md +468 -0
- package/infrastructure/README.md +51 -0
- package/infrastructure/__tests__/postgres-config.test.js +914 -0
- package/infrastructure/__tests__/template-generation.test.js +687 -0
- package/infrastructure/create-frigg-infrastructure.js +1 -1
- package/infrastructure/docs/POSTGRES-CONFIGURATION.md +630 -0
- package/infrastructure/{DEPLOYMENT-INSTRUCTIONS.md → docs/deployment-instructions.md} +3 -3
- package/infrastructure/{IAM-POLICY-TEMPLATES.md → docs/iam-policy-templates.md} +9 -10
- package/infrastructure/domains/database/aurora-builder.js +809 -0
- package/infrastructure/domains/database/aurora-builder.test.js +950 -0
- package/infrastructure/domains/database/aurora-discovery.js +87 -0
- package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
- package/infrastructure/domains/database/aurora-resolver.js +210 -0
- package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
- package/infrastructure/domains/database/migration-builder.js +633 -0
- package/infrastructure/domains/database/migration-builder.test.js +294 -0
- package/infrastructure/domains/database/migration-resolver.js +163 -0
- package/infrastructure/domains/database/migration-resolver.test.js +337 -0
- package/infrastructure/domains/health/application/ports/IPropertyReconciler.js +164 -0
- package/infrastructure/domains/health/application/ports/IResourceDetector.js +129 -0
- package/infrastructure/domains/health/application/ports/IResourceImporter.js +142 -0
- package/infrastructure/domains/health/application/ports/IStackRepository.js +131 -0
- package/infrastructure/domains/health/application/ports/index.js +26 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -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/execute-resource-import-use-case.js +221 -0
- package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +152 -0
- package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +535 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +213 -0
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
- 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 +299 -0
- package/infrastructure/domains/health/domain/entities/issue.test.js +528 -0
- package/infrastructure/domains/health/domain/entities/property-mismatch.js +108 -0
- package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +275 -0
- package/infrastructure/domains/health/domain/entities/resource.js +159 -0
- package/infrastructure/domains/health/domain/entities/resource.test.js +432 -0
- package/infrastructure/domains/health/domain/entities/stack-health-report.js +306 -0
- package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +601 -0
- package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
- package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +971 -0
- package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +1150 -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/__tests__/update-progress-monitor.test.js +419 -0
- package/infrastructure/domains/health/domain/services/health-score-calculator.js +248 -0
- package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +504 -0
- package/infrastructure/domains/health/domain/services/import-progress-monitor.js +195 -0
- package/infrastructure/domains/health/domain/services/import-template-generator.js +435 -0
- package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
- package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +234 -0
- package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +431 -0
- package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
- package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
- package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -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 +192 -0
- package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +262 -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 +784 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +1133 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +565 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +554 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +318 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +398 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +777 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +580 -0
- package/infrastructure/domains/integration/integration-builder.js +397 -0
- package/infrastructure/domains/integration/integration-builder.test.js +593 -0
- package/infrastructure/domains/integration/integration-resolver.js +170 -0
- package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
- package/infrastructure/domains/integration/websocket-builder.js +69 -0
- package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
- package/infrastructure/domains/networking/vpc-builder.js +1829 -0
- package/infrastructure/domains/networking/vpc-builder.test.js +1262 -0
- package/infrastructure/domains/networking/vpc-discovery.js +177 -0
- package/infrastructure/domains/networking/vpc-discovery.test.js +350 -0
- package/infrastructure/domains/networking/vpc-resolver.js +324 -0
- package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
- package/infrastructure/domains/parameters/ssm-builder.js +79 -0
- package/infrastructure/domains/parameters/ssm-builder.test.js +189 -0
- package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
- package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
- package/infrastructure/{iam-generator.js → domains/security/iam-generator.js} +2 -2
- package/infrastructure/domains/security/kms-builder.js +366 -0
- package/infrastructure/domains/security/kms-builder.test.js +374 -0
- package/infrastructure/domains/security/kms-discovery.js +80 -0
- package/infrastructure/domains/security/kms-discovery.test.js +177 -0
- package/infrastructure/domains/security/kms-resolver.js +96 -0
- package/infrastructure/domains/security/kms-resolver.test.js +216 -0
- package/infrastructure/domains/shared/base-builder.js +112 -0
- package/infrastructure/domains/shared/base-resolver.js +186 -0
- package/infrastructure/domains/shared/base-resolver.test.js +305 -0
- package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
- package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
- package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
- package/infrastructure/domains/shared/cloudformation-discovery.js +375 -0
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +590 -0
- package/infrastructure/domains/shared/environment-builder.js +119 -0
- package/infrastructure/domains/shared/environment-builder.test.js +247 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.js +544 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +377 -0
- package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
- package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
- package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
- package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
- package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
- package/infrastructure/domains/shared/resource-discovery.js +192 -0
- package/infrastructure/domains/shared/resource-discovery.test.js +552 -0
- package/infrastructure/domains/shared/types/app-definition.js +205 -0
- package/infrastructure/domains/shared/types/discovery-result.js +106 -0
- package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
- package/infrastructure/domains/shared/types/index.js +46 -0
- package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
- package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +380 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +248 -0
- package/infrastructure/domains/shared/utilities/handler-path-resolver.js +134 -0
- package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +268 -0
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +55 -0
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +138 -0
- package/infrastructure/{env-validator.js → domains/shared/validation/env-validator.js} +2 -1
- package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
- package/infrastructure/esbuild.config.js +53 -0
- package/infrastructure/infrastructure-composer.js +87 -0
- package/infrastructure/{serverless-template.test.js → infrastructure-composer.test.js} +115 -24
- package/infrastructure/scripts/build-prisma-layer.js +553 -0
- package/infrastructure/scripts/build-prisma-layer.test.js +102 -0
- package/infrastructure/{build-time-discovery.js → scripts/build-time-discovery.js} +80 -48
- package/infrastructure/{build-time-discovery.test.js → scripts/build-time-discovery.test.js} +5 -4
- package/layers/prisma/nodejs/package.json +8 -0
- package/management-ui/server/utils/cliIntegration.js +1 -1
- package/management-ui/server/utils/environment/awsParameterStore.js +29 -18
- package/package.json +11 -11
- package/frigg-cli/.eslintrc.js +0 -141
- package/frigg-cli/__tests__/unit/commands/build.test.js +0 -251
- package/frigg-cli/__tests__/unit/commands/db-setup.test.js +0 -548
- package/frigg-cli/__tests__/unit/commands/install.test.js +0 -400
- package/frigg-cli/__tests__/unit/commands/ui.test.js +0 -346
- package/frigg-cli/__tests__/unit/utils/database-validator.test.js +0 -366
- package/frigg-cli/__tests__/unit/utils/error-messages.test.js +0 -304
- package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +0 -486
- package/frigg-cli/__tests__/utils/mock-factory.js +0 -270
- package/frigg-cli/__tests__/utils/prisma-mock.js +0 -194
- package/frigg-cli/__tests__/utils/test-fixtures.js +0 -463
- package/frigg-cli/__tests__/utils/test-setup.js +0 -287
- package/frigg-cli/build-command/index.js +0 -65
- package/frigg-cli/db-setup-command/index.js +0 -193
- package/frigg-cli/deploy-command/index.js +0 -175
- package/frigg-cli/generate-command/__tests__/generate-command.test.js +0 -301
- package/frigg-cli/generate-command/azure-generator.js +0 -43
- package/frigg-cli/generate-command/gcp-generator.js +0 -47
- package/frigg-cli/generate-command/index.js +0 -332
- package/frigg-cli/generate-command/terraform-generator.js +0 -555
- package/frigg-cli/generate-iam-command.js +0 -118
- package/frigg-cli/index.js +0 -75
- package/frigg-cli/index.test.js +0 -158
- package/frigg-cli/init-command/backend-first-handler.js +0 -756
- package/frigg-cli/init-command/index.js +0 -93
- package/frigg-cli/init-command/template-handler.js +0 -143
- package/frigg-cli/install-command/backend-js.js +0 -33
- package/frigg-cli/install-command/commit-changes.js +0 -16
- package/frigg-cli/install-command/environment-variables.js +0 -127
- package/frigg-cli/install-command/environment-variables.test.js +0 -136
- package/frigg-cli/install-command/index.js +0 -54
- package/frigg-cli/install-command/install-package.js +0 -13
- package/frigg-cli/install-command/integration-file.js +0 -30
- package/frigg-cli/install-command/logger.js +0 -12
- package/frigg-cli/install-command/template.js +0 -90
- package/frigg-cli/install-command/validate-package.js +0 -75
- package/frigg-cli/jest.config.js +0 -124
- package/frigg-cli/package.json +0 -54
- package/frigg-cli/start-command/index.js +0 -149
- package/frigg-cli/start-command/start-command.test.js +0 -297
- package/frigg-cli/test/init-command.test.js +0 -180
- package/frigg-cli/test/npm-registry.test.js +0 -319
- package/frigg-cli/ui-command/index.js +0 -154
- package/frigg-cli/utils/app-resolver.js +0 -319
- package/frigg-cli/utils/backend-path.js +0 -25
- package/frigg-cli/utils/database-validator.js +0 -161
- package/frigg-cli/utils/error-messages.js +0 -257
- package/frigg-cli/utils/npm-registry.js +0 -167
- package/frigg-cli/utils/prisma-runner.js +0 -280
- package/frigg-cli/utils/process-manager.js +0 -199
- package/frigg-cli/utils/repo-detection.js +0 -405
- package/infrastructure/aws-discovery.js +0 -1176
- package/infrastructure/aws-discovery.test.js +0 -1220
- package/infrastructure/serverless-template.js +0 -2094
- /package/infrastructure/{WEBSOCKET-CONFIGURATION.md → docs/WEBSOCKET-CONFIGURATION.md} +0 -0
- /package/infrastructure/{GENERATE-IAM-DOCS.md → docs/generate-iam-command.md} +0 -0
- /package/infrastructure/{iam-generator.test.js → domains/security/iam-generator.test.js} +0 -0
- /package/infrastructure/{frigg-deployment-iam-stack.yaml → domains/security/templates/frigg-deployment-iam-stack.yaml} +0 -0
- /package/infrastructure/{iam-policy-basic.json → domains/security/templates/iam-policy-basic.json} +0 -0
- /package/infrastructure/{iam-policy-full.json → domains/security/templates/iam-policy-full.json} +0 -0
- /package/infrastructure/{run-discovery.js → scripts/run-discovery.js} +0 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for HealthScoreCalculator Domain Service
|
|
3
|
+
*
|
|
4
|
+
* Updated for percentage-based health scoring (2025-10-26)
|
|
5
|
+
*
|
|
6
|
+
* NEW SYSTEM (Percentage-Based):
|
|
7
|
+
* - Critical issues: up to 50 points (% of total resources)
|
|
8
|
+
* - Functional drift: up to 30 points (% of critical resources)
|
|
9
|
+
* - Infrastructure drift: up to 20 points (% of infra resources)
|
|
10
|
+
* - Example: 16/16 Lambdas drifted = 100% × 30 = 30 penalty → 70/100 ✅
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const HealthScoreCalculator = require('./health-score-calculator');
|
|
14
|
+
const Resource = require('../entities/resource');
|
|
15
|
+
const Issue = require('../entities/issue');
|
|
16
|
+
const ResourceState = require('../value-objects/resource-state');
|
|
17
|
+
const PropertyMutability = require('../value-objects/property-mutability');
|
|
18
|
+
const PropertyMismatch = require('../entities/property-mismatch');
|
|
19
|
+
|
|
20
|
+
describe('HealthScoreCalculator', () => {
|
|
21
|
+
let calculator;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
calculator = new HealthScoreCalculator();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('perfect health (100)', () => {
|
|
28
|
+
it('should return 100 for no resources and no issues', () => {
|
|
29
|
+
const score = calculator.calculate({ resources: [], issues: [] });
|
|
30
|
+
expect(score.value).toBe(100);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should return 100 for all healthy resources with no issues', () => {
|
|
34
|
+
const resources = [
|
|
35
|
+
new Resource({
|
|
36
|
+
logicalId: 'VPC',
|
|
37
|
+
physicalId: 'vpc-123',
|
|
38
|
+
resourceType: 'AWS::EC2::VPC',
|
|
39
|
+
state: ResourceState.IN_STACK,
|
|
40
|
+
}),
|
|
41
|
+
new Resource({
|
|
42
|
+
logicalId: 'Subnet',
|
|
43
|
+
physicalId: 'subnet-123',
|
|
44
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
45
|
+
state: ResourceState.IN_STACK,
|
|
46
|
+
}),
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const score = calculator.calculate({ resources, issues: [] });
|
|
50
|
+
expect(score.value).toBe(100);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('critical issues (orphaned, missing)', () => {
|
|
55
|
+
it('should penalize based on percentage of resources that are orphaned', () => {
|
|
56
|
+
// 10 resources, 1 orphaned = 10% critical impact
|
|
57
|
+
const resources = Array.from({ length: 10 }, (_, i) =>
|
|
58
|
+
new Resource({
|
|
59
|
+
logicalId: `Resource${i}`,
|
|
60
|
+
physicalId: `resource-${i}`,
|
|
61
|
+
resourceType: 'AWS::EC2::VPC',
|
|
62
|
+
state: ResourceState.IN_STACK,
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const issues = [
|
|
67
|
+
Issue.orphanedResource({
|
|
68
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
69
|
+
resourceId: 'orphan-123',
|
|
70
|
+
description: 'Orphaned cluster',
|
|
71
|
+
}),
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const score = calculator.calculate({ resources, issues });
|
|
75
|
+
// Critical impact: 1/10 = 10% → penalty: 10% × 50 = 5
|
|
76
|
+
// Score: 100 - 5 = 95
|
|
77
|
+
expect(score.value).toBe(95);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should penalize based on percentage of resources that are missing', () => {
|
|
81
|
+
// 10 resources, 2 missing = 20% critical impact
|
|
82
|
+
const resources = Array.from({ length: 10 }, (_, i) =>
|
|
83
|
+
new Resource({
|
|
84
|
+
logicalId: `Resource${i}`,
|
|
85
|
+
physicalId: `resource-${i}`,
|
|
86
|
+
resourceType: 'AWS::KMS::Key',
|
|
87
|
+
state: ResourceState.IN_STACK,
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const issues = [
|
|
92
|
+
Issue.missingResource({
|
|
93
|
+
resourceType: 'AWS::KMS::Key',
|
|
94
|
+
resourceId: 'MissingKey1',
|
|
95
|
+
description: 'Missing key 1',
|
|
96
|
+
}),
|
|
97
|
+
Issue.missingResource({
|
|
98
|
+
resourceType: 'AWS::KMS::Key',
|
|
99
|
+
resourceId: 'MissingKey2',
|
|
100
|
+
description: 'Missing key 2',
|
|
101
|
+
}),
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const score = calculator.calculate({ resources, issues });
|
|
105
|
+
// Critical impact: 2/10 = 20% → penalty: 20% × 50 = 10
|
|
106
|
+
// Score: 100 - 10 = 90
|
|
107
|
+
expect(score.value).toBe(90);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should apply higher penalties for larger percentages of orphaned resources', () => {
|
|
111
|
+
// 10 resources, 4 orphaned = 40% critical impact
|
|
112
|
+
const resources = Array.from({ length: 10 }, (_, i) =>
|
|
113
|
+
new Resource({
|
|
114
|
+
logicalId: `Resource${i}`,
|
|
115
|
+
physicalId: `resource-${i}`,
|
|
116
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
117
|
+
state: ResourceState.IN_STACK,
|
|
118
|
+
})
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const issues = Array.from({ length: 4 }, (_, i) =>
|
|
122
|
+
Issue.orphanedResource({
|
|
123
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
124
|
+
resourceId: `orphan-${i}`,
|
|
125
|
+
description: `Orphaned cluster ${i}`,
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const score = calculator.calculate({ resources, issues });
|
|
130
|
+
// Critical impact: 4/10 = 40% → penalty: 40% × 50 = 20
|
|
131
|
+
// Score: 100 - 20 = 80
|
|
132
|
+
expect(score.value).toBe(80);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('functional drift (critical resources)', () => {
|
|
137
|
+
it('should penalize drift on Lambda functions', () => {
|
|
138
|
+
// 10 Lambda, 10 VPC; 1 Lambda drifted = 10% functional drift
|
|
139
|
+
const resources = [
|
|
140
|
+
...Array.from({ length: 10 }, (_, i) =>
|
|
141
|
+
new Resource({
|
|
142
|
+
logicalId: `Lambda${i}`,
|
|
143
|
+
physicalId: `lambda-${i}`,
|
|
144
|
+
resourceType: 'AWS::Lambda::Function',
|
|
145
|
+
state: ResourceState.IN_STACK,
|
|
146
|
+
})
|
|
147
|
+
),
|
|
148
|
+
...Array.from({ length: 10 }, (_, i) =>
|
|
149
|
+
new Resource({
|
|
150
|
+
logicalId: `VPC${i}`,
|
|
151
|
+
physicalId: `vpc-${i}`,
|
|
152
|
+
resourceType: 'AWS::EC2::VPC',
|
|
153
|
+
state: ResourceState.IN_STACK,
|
|
154
|
+
})
|
|
155
|
+
),
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
const mismatch = new PropertyMismatch({
|
|
159
|
+
propertyPath: 'Properties.Environment',
|
|
160
|
+
expectedValue: { VAR: 'expected' },
|
|
161
|
+
actualValue: { VAR: 'actual' },
|
|
162
|
+
mutability: PropertyMutability.MUTABLE,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const issues = [
|
|
166
|
+
Issue.propertyMismatch({
|
|
167
|
+
resourceType: 'AWS::Lambda::Function',
|
|
168
|
+
resourceId: 'lambda-0',
|
|
169
|
+
mismatch,
|
|
170
|
+
}),
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
const score = calculator.calculate({ resources, issues });
|
|
174
|
+
// Functional drift: 1/10 = 10% → penalty: 10% × 30 = 3
|
|
175
|
+
// Score: 100 - 3 = 97
|
|
176
|
+
expect(score.value).toBe(97);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should count unique drifted resources when multiple properties drifted', () => {
|
|
180
|
+
// 10 Lambda; 1 Lambda with 2 property mismatches = 10% functional drift (not 20%)
|
|
181
|
+
const resources = Array.from({ length: 10 }, (_, i) =>
|
|
182
|
+
new Resource({
|
|
183
|
+
logicalId: `Lambda${i}`,
|
|
184
|
+
physicalId: `lambda-${i}`,
|
|
185
|
+
resourceType: 'AWS::Lambda::Function',
|
|
186
|
+
state: ResourceState.IN_STACK,
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const mismatch1 = new PropertyMismatch({
|
|
191
|
+
propertyPath: 'Properties.Environment',
|
|
192
|
+
expectedValue: { VAR: 'expected' },
|
|
193
|
+
actualValue: { VAR: 'actual' },
|
|
194
|
+
mutability: PropertyMutability.MUTABLE,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const mismatch2 = new PropertyMismatch({
|
|
198
|
+
propertyPath: 'Properties.Timeout',
|
|
199
|
+
expectedValue: 30,
|
|
200
|
+
actualValue: 60,
|
|
201
|
+
mutability: PropertyMutability.MUTABLE,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const issues = [
|
|
205
|
+
Issue.propertyMismatch({
|
|
206
|
+
resourceType: 'AWS::Lambda::Function',
|
|
207
|
+
resourceId: 'lambda-0',
|
|
208
|
+
mismatch: mismatch1,
|
|
209
|
+
}),
|
|
210
|
+
Issue.propertyMismatch({
|
|
211
|
+
resourceType: 'AWS::Lambda::Function',
|
|
212
|
+
resourceId: 'lambda-0',
|
|
213
|
+
mismatch: mismatch2,
|
|
214
|
+
}),
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
const score = calculator.calculate({ resources, issues });
|
|
218
|
+
// Functional drift: 1/10 = 10% (unique resources) → penalty: 10% × 30 = 3
|
|
219
|
+
// Score: 100 - 3 = 97
|
|
220
|
+
expect(score.value).toBe(97);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('infrastructure drift (non-critical resources)', () => {
|
|
225
|
+
it('should penalize drift on VPC/networking resources with lower weight', () => {
|
|
226
|
+
// 10 VPC, 10 Lambda; 5 VPC drifted = 50% infrastructure drift
|
|
227
|
+
const resources = [
|
|
228
|
+
...Array.from({ length: 10 }, (_, i) =>
|
|
229
|
+
new Resource({
|
|
230
|
+
logicalId: `Lambda${i}`,
|
|
231
|
+
physicalId: `lambda-${i}`,
|
|
232
|
+
resourceType: 'AWS::Lambda::Function',
|
|
233
|
+
state: ResourceState.IN_STACK,
|
|
234
|
+
})
|
|
235
|
+
),
|
|
236
|
+
...Array.from({ length: 10 }, (_, i) =>
|
|
237
|
+
new Resource({
|
|
238
|
+
logicalId: `VPC${i}`,
|
|
239
|
+
physicalId: `vpc-${i}`,
|
|
240
|
+
resourceType: 'AWS::EC2::VPC',
|
|
241
|
+
state: ResourceState.IN_STACK,
|
|
242
|
+
})
|
|
243
|
+
),
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
const issues = Array.from({ length: 5 }, (_, i) => {
|
|
247
|
+
const mismatch = new PropertyMismatch({
|
|
248
|
+
propertyPath: 'Properties.Tags',
|
|
249
|
+
expectedValue: ['tag1'],
|
|
250
|
+
actualValue: ['tag2'],
|
|
251
|
+
mutability: PropertyMutability.MUTABLE,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
return Issue.propertyMismatch({
|
|
255
|
+
resourceType: 'AWS::EC2::VPC',
|
|
256
|
+
resourceId: `vpc-${i}`,
|
|
257
|
+
mismatch,
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const score = calculator.calculate({ resources, issues });
|
|
262
|
+
// Infra drift: 5/10 = 50% → penalty: 50% × 20 = 10
|
|
263
|
+
// Score: 100 - 10 = 90
|
|
264
|
+
expect(score.value).toBe(90);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe('mixed issues', () => {
|
|
269
|
+
it('should correctly combine penalties from different categories', () => {
|
|
270
|
+
// 20 resources: 10 Lambda, 10 VPC
|
|
271
|
+
// 1 orphaned, 2 Lambda drifted, 5 VPC drifted
|
|
272
|
+
const resources = [
|
|
273
|
+
...Array.from({ length: 10 }, (_, i) =>
|
|
274
|
+
new Resource({
|
|
275
|
+
logicalId: `Lambda${i}`,
|
|
276
|
+
physicalId: `lambda-${i}`,
|
|
277
|
+
resourceType: 'AWS::Lambda::Function',
|
|
278
|
+
state: ResourceState.IN_STACK,
|
|
279
|
+
})
|
|
280
|
+
),
|
|
281
|
+
...Array.from({ length: 10 }, (_, i) =>
|
|
282
|
+
new Resource({
|
|
283
|
+
logicalId: `VPC${i}`,
|
|
284
|
+
physicalId: `vpc-${i}`,
|
|
285
|
+
resourceType: 'AWS::EC2::VPC',
|
|
286
|
+
state: ResourceState.IN_STACK,
|
|
287
|
+
})
|
|
288
|
+
),
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
const orphanedIssue = Issue.orphanedResource({
|
|
292
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
293
|
+
resourceId: 'orphan-123',
|
|
294
|
+
description: 'Orphaned cluster',
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const lambdaDriftIssues = Array.from({ length: 2 }, (_, i) => {
|
|
298
|
+
const mismatch = new PropertyMismatch({
|
|
299
|
+
propertyPath: 'Properties.Environment',
|
|
300
|
+
expectedValue: { VAR: 'expected' },
|
|
301
|
+
actualValue: { VAR: 'actual' },
|
|
302
|
+
mutability: PropertyMutability.MUTABLE,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
return Issue.propertyMismatch({
|
|
306
|
+
resourceType: 'AWS::Lambda::Function',
|
|
307
|
+
resourceId: `lambda-${i}`,
|
|
308
|
+
mismatch,
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const vpcDriftIssues = Array.from({ length: 5 }, (_, i) => {
|
|
313
|
+
const mismatch = new PropertyMismatch({
|
|
314
|
+
propertyPath: 'Properties.Tags',
|
|
315
|
+
expectedValue: ['tag1'],
|
|
316
|
+
actualValue: ['tag2'],
|
|
317
|
+
mutability: PropertyMutability.MUTABLE,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return Issue.propertyMismatch({
|
|
321
|
+
resourceType: 'AWS::EC2::VPC',
|
|
322
|
+
resourceId: `vpc-${i}`,
|
|
323
|
+
mismatch,
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const issues = [orphanedIssue, ...lambdaDriftIssues, ...vpcDriftIssues];
|
|
328
|
+
|
|
329
|
+
const score = calculator.calculate({ resources, issues });
|
|
330
|
+
// Critical impact: 1/20 = 5% → penalty: 5% × 50 = 2.5
|
|
331
|
+
// Functional drift: 2/10 = 20% → penalty: 20% × 30 = 6
|
|
332
|
+
// Infra drift: 5/10 = 50% → penalty: 50% × 20 = 10
|
|
333
|
+
// Total: 2.5 + 6 + 10 = 18.5 → Score: 100 - 19 = 81 (rounded)
|
|
334
|
+
expect(score.value).toBeGreaterThanOrEqual(81);
|
|
335
|
+
expect(score.value).toBeLessThanOrEqual(82);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe('minimum score (0)', () => {
|
|
340
|
+
it('should not go below 0', () => {
|
|
341
|
+
// 2 resources, 10 critical issues = 500% critical impact (capped at 100%)
|
|
342
|
+
const resources = [
|
|
343
|
+
new Resource({
|
|
344
|
+
logicalId: 'Resource1',
|
|
345
|
+
physicalId: 'resource-1',
|
|
346
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
347
|
+
state: ResourceState.IN_STACK,
|
|
348
|
+
}),
|
|
349
|
+
new Resource({
|
|
350
|
+
logicalId: 'Resource2',
|
|
351
|
+
physicalId: 'resource-2',
|
|
352
|
+
resourceType: 'AWS::Lambda::Function',
|
|
353
|
+
state: ResourceState.IN_STACK,
|
|
354
|
+
}),
|
|
355
|
+
];
|
|
356
|
+
|
|
357
|
+
const issues = Array.from({ length: 10 }, (_, i) =>
|
|
358
|
+
Issue.orphanedResource({
|
|
359
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
360
|
+
resourceId: `orphan-${i}`,
|
|
361
|
+
description: `Orphaned cluster ${i}`,
|
|
362
|
+
})
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
const score = calculator.calculate({ resources, issues });
|
|
366
|
+
// Critical impact: 10/2 = 500% (but max penalty is 50)
|
|
367
|
+
// Penalty: min(500% × 50, 50) = 50
|
|
368
|
+
// Score: max(0, 100 - 50) = 50
|
|
369
|
+
// But with such extreme issues, score should be very low
|
|
370
|
+
expect(score.value).toBeGreaterThanOrEqual(0);
|
|
371
|
+
expect(score.value).toBeLessThanOrEqual(50);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
describe('penalty configuration', () => {
|
|
376
|
+
it('should use custom max penalty configuration', () => {
|
|
377
|
+
const customCalculator = new HealthScoreCalculator({
|
|
378
|
+
maxPenalties: {
|
|
379
|
+
criticalIssues: 40, // Custom: 40 instead of default 50
|
|
380
|
+
functionalDrift: 35, // Custom: 35 instead of default 30
|
|
381
|
+
infrastructureDrift: 25, // Custom: 25 instead of default 20
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// 10 resources, 5 orphaned = 50% critical impact
|
|
386
|
+
const resources = Array.from({ length: 10 }, (_, i) =>
|
|
387
|
+
new Resource({
|
|
388
|
+
logicalId: `Resource${i}`,
|
|
389
|
+
physicalId: `resource-${i}`,
|
|
390
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
391
|
+
state: ResourceState.IN_STACK,
|
|
392
|
+
})
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
const issues = Array.from({ length: 5 }, (_, i) =>
|
|
396
|
+
Issue.orphanedResource({
|
|
397
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
398
|
+
resourceId: `orphan-${i}`,
|
|
399
|
+
description: `Orphaned cluster ${i}`,
|
|
400
|
+
})
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
const score = customCalculator.calculate({ resources, issues });
|
|
404
|
+
// Critical impact: 5/10 = 50% → penalty: 50% × 40 (custom) = 20
|
|
405
|
+
// Score: 100 - 20 = 80
|
|
406
|
+
expect(score.value).toBe(80);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
describe('explainScore', () => {
|
|
411
|
+
it('should explain score with percentage-based breakdown', () => {
|
|
412
|
+
// 20 resources: 10 Lambda, 10 VPC
|
|
413
|
+
// 1 orphaned, 2 Lambda drifted, 5 VPC drifted
|
|
414
|
+
const resources = [
|
|
415
|
+
...Array.from({ length: 10 }, (_, i) =>
|
|
416
|
+
new Resource({
|
|
417
|
+
logicalId: `Lambda${i}`,
|
|
418
|
+
physicalId: `lambda-${i}`,
|
|
419
|
+
resourceType: 'AWS::Lambda::Function',
|
|
420
|
+
state: ResourceState.IN_STACK,
|
|
421
|
+
})
|
|
422
|
+
),
|
|
423
|
+
...Array.from({ length: 10 }, (_, i) =>
|
|
424
|
+
new Resource({
|
|
425
|
+
logicalId: `VPC${i}`,
|
|
426
|
+
physicalId: `vpc-${i}`,
|
|
427
|
+
resourceType: 'AWS::EC2::VPC',
|
|
428
|
+
state: ResourceState.IN_STACK,
|
|
429
|
+
})
|
|
430
|
+
),
|
|
431
|
+
];
|
|
432
|
+
|
|
433
|
+
const orphanedIssue = Issue.orphanedResource({
|
|
434
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
435
|
+
resourceId: 'orphan-123',
|
|
436
|
+
description: 'Orphaned cluster',
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const lambdaDriftIssues = Array.from({ length: 2 }, (_, i) => {
|
|
440
|
+
const mismatch = new PropertyMismatch({
|
|
441
|
+
propertyPath: 'Properties.Environment',
|
|
442
|
+
expectedValue: { VAR: 'expected' },
|
|
443
|
+
actualValue: { VAR: 'actual' },
|
|
444
|
+
mutability: PropertyMutability.MUTABLE,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
return Issue.propertyMismatch({
|
|
448
|
+
resourceType: 'AWS::Lambda::Function',
|
|
449
|
+
resourceId: `lambda-${i}`,
|
|
450
|
+
mismatch,
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const vpcDriftIssues = Array.from({ length: 5 }, (_, i) => {
|
|
455
|
+
const mismatch = new PropertyMismatch({
|
|
456
|
+
propertyPath: 'Properties.Tags',
|
|
457
|
+
expectedValue: ['tag1'],
|
|
458
|
+
actualValue: ['tag2'],
|
|
459
|
+
mutability: PropertyMutability.MUTABLE,
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
return Issue.propertyMismatch({
|
|
463
|
+
resourceType: 'AWS::EC2::VPC',
|
|
464
|
+
resourceId: `vpc-${i}`,
|
|
465
|
+
mismatch,
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const issues = [orphanedIssue, ...lambdaDriftIssues, ...vpcDriftIssues];
|
|
470
|
+
|
|
471
|
+
const explanation = calculator.explainScore({ resources, issues });
|
|
472
|
+
|
|
473
|
+
expect(explanation.startingScore).toBe(100);
|
|
474
|
+
expect(explanation.finalScore).toBeGreaterThanOrEqual(81);
|
|
475
|
+
expect(explanation.finalScore).toBeLessThanOrEqual(82);
|
|
476
|
+
expect(explanation.breakdown).toHaveProperty('criticalIssues');
|
|
477
|
+
expect(explanation.breakdown).toHaveProperty('functionalDrift');
|
|
478
|
+
expect(explanation.breakdown).toHaveProperty('infrastructureDrift');
|
|
479
|
+
expect(explanation.breakdown.criticalIssues.count).toBe(1);
|
|
480
|
+
expect(explanation.breakdown.functionalDrift.count).toBe(2);
|
|
481
|
+
expect(explanation.breakdown.infrastructureDrift.count).toBe(5);
|
|
482
|
+
expect(explanation.resourceCounts).toEqual({
|
|
483
|
+
total: 20,
|
|
484
|
+
critical: 10,
|
|
485
|
+
infrastructure: 10,
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('should explain perfect score', () => {
|
|
490
|
+
const explanation = calculator.explainScore({ resources: [], issues: [] });
|
|
491
|
+
|
|
492
|
+
expect(explanation).toEqual({
|
|
493
|
+
finalScore: 100,
|
|
494
|
+
startingScore: 100,
|
|
495
|
+
totalPenalty: 0,
|
|
496
|
+
breakdown: {
|
|
497
|
+
criticalIssues: { count: 0, impactPercent: 0, penalty: 0 },
|
|
498
|
+
functionalDrift: { count: 0, impactPercent: 0, penalty: 0 },
|
|
499
|
+
infrastructureDrift: { count: 0, impactPercent: 0, penalty: 0 },
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImportProgressMonitor - Monitor CloudFormation Import Operation Progress
|
|
3
|
+
*
|
|
4
|
+
* Domain Layer - Service
|
|
5
|
+
*
|
|
6
|
+
* Monitors CloudFormation import operations by polling stack events and tracking
|
|
7
|
+
* resource import progress. Provides real-time progress callbacks and detects
|
|
8
|
+
* failures, rollbacks, and timeouts.
|
|
9
|
+
*
|
|
10
|
+
* Responsibilities:
|
|
11
|
+
* - Poll CloudFormation stack events during import
|
|
12
|
+
* - Track progress per resource (IN_PROGRESS, COMPLETE, FAILED)
|
|
13
|
+
* - Detect stack rollback states
|
|
14
|
+
* - Timeout after 5 minutes
|
|
15
|
+
* - Provide progress callbacks for UI updates
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
class ImportProgressMonitor {
|
|
19
|
+
/**
|
|
20
|
+
* Create progress monitor with CloudFormation repository dependency
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} params
|
|
23
|
+
* @param {Object} params.cloudFormationRepository - CloudFormation operations
|
|
24
|
+
*/
|
|
25
|
+
constructor({ cloudFormationRepository }) {
|
|
26
|
+
if (!cloudFormationRepository) {
|
|
27
|
+
throw new Error('cloudFormationRepository is required');
|
|
28
|
+
}
|
|
29
|
+
this.cfRepo = cloudFormationRepository;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Monitor import operation progress
|
|
34
|
+
*
|
|
35
|
+
* Polls CloudFormation stack events every 2 seconds to track resource import progress.
|
|
36
|
+
* Calls onProgress callback with status updates for each resource.
|
|
37
|
+
* Detects failures, rollbacks, and timeouts.
|
|
38
|
+
*
|
|
39
|
+
* @param {Object} params
|
|
40
|
+
* @param {Object} params.stackIdentifier - Stack identifier { stackName, region }
|
|
41
|
+
* @param {Array<string>} params.resourceLogicalIds - Logical IDs to track
|
|
42
|
+
* @param {Function} params.onProgress - Progress callback function
|
|
43
|
+
* @returns {Promise<Object>} Import result
|
|
44
|
+
*/
|
|
45
|
+
async monitorImport({ stackIdentifier, resourceLogicalIds, onProgress }) {
|
|
46
|
+
const importedResources = new Set();
|
|
47
|
+
const failedResources = [];
|
|
48
|
+
const processedEvents = new Set(); // Track processed events by timestamp + logicalId
|
|
49
|
+
let elapsedTime = 0; // Track elapsed time manually for fake timers compatibility
|
|
50
|
+
const TIMEOUT_MS = 300000; // 5 minutes
|
|
51
|
+
const POLL_INTERVAL_MS = 2000; // 2 seconds
|
|
52
|
+
|
|
53
|
+
// Continue polling until all resources are complete or failed
|
|
54
|
+
while (
|
|
55
|
+
importedResources.size + failedResources.length <
|
|
56
|
+
resourceLogicalIds.length
|
|
57
|
+
) {
|
|
58
|
+
// Wait 2 seconds before polling
|
|
59
|
+
await this._delay(POLL_INTERVAL_MS);
|
|
60
|
+
elapsedTime += POLL_INTERVAL_MS;
|
|
61
|
+
|
|
62
|
+
// Check for timeout
|
|
63
|
+
if (elapsedTime > TIMEOUT_MS) {
|
|
64
|
+
throw new Error('Import operation timed out');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Get stack events
|
|
68
|
+
const events = await this.cfRepo.getStackEvents({
|
|
69
|
+
stackIdentifier,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Sort events by timestamp (oldest first) for consistent processing
|
|
73
|
+
const sortedEvents = [...events].sort(
|
|
74
|
+
(a, b) => new Date(a.Timestamp) - new Date(b.Timestamp)
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Track if we processed any new events in this iteration
|
|
78
|
+
let processedNewEvents = false;
|
|
79
|
+
|
|
80
|
+
// Process events for tracked resources
|
|
81
|
+
for (const event of sortedEvents) {
|
|
82
|
+
const logicalId = event.LogicalResourceId;
|
|
83
|
+
|
|
84
|
+
// Skip if not a tracked resource
|
|
85
|
+
if (!resourceLogicalIds.includes(logicalId)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Create unique event key to avoid duplicate processing
|
|
90
|
+
const eventKey = `${event.Timestamp.toISOString()}_${logicalId}_${event.ResourceStatus}`;
|
|
91
|
+
|
|
92
|
+
// Skip if already processed
|
|
93
|
+
if (processedEvents.has(eventKey)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
processedEvents.add(eventKey);
|
|
98
|
+
processedNewEvents = true;
|
|
99
|
+
|
|
100
|
+
// Handle different resource statuses
|
|
101
|
+
if (event.ResourceStatus === 'IMPORT_IN_PROGRESS') {
|
|
102
|
+
// Call progress callback with IN_PROGRESS status
|
|
103
|
+
if (onProgress) {
|
|
104
|
+
onProgress({
|
|
105
|
+
logicalId,
|
|
106
|
+
status: 'IN_PROGRESS',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
} else if (event.ResourceStatus === 'IMPORT_COMPLETE' || event.ResourceStatus === 'UPDATE_COMPLETE') {
|
|
110
|
+
// Mark resource as imported
|
|
111
|
+
// Note: CloudFormation sends IMPORT_COMPLETE then UPDATE_COMPLETE (for tagging)
|
|
112
|
+
// We count either as successfully imported
|
|
113
|
+
importedResources.add(logicalId);
|
|
114
|
+
|
|
115
|
+
// Call progress callback with COMPLETE status
|
|
116
|
+
if (onProgress) {
|
|
117
|
+
onProgress({
|
|
118
|
+
logicalId,
|
|
119
|
+
status: 'COMPLETE',
|
|
120
|
+
progress: importedResources.size,
|
|
121
|
+
total: resourceLogicalIds.length,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
} else if (event.ResourceStatus === 'IMPORT_FAILED') {
|
|
125
|
+
// Add to failed resources
|
|
126
|
+
const reason = event.ResourceStatusReason || 'Unknown error';
|
|
127
|
+
failedResources.push({
|
|
128
|
+
logicalId,
|
|
129
|
+
reason,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Call progress callback with FAILED status
|
|
133
|
+
if (onProgress) {
|
|
134
|
+
onProgress({
|
|
135
|
+
logicalId,
|
|
136
|
+
status: 'FAILED',
|
|
137
|
+
reason,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check if all resources are now accounted for
|
|
144
|
+
const allResourcesProcessed =
|
|
145
|
+
importedResources.size + failedResources.length >=
|
|
146
|
+
resourceLogicalIds.length;
|
|
147
|
+
|
|
148
|
+
// If all resources processed, exit loop to return result
|
|
149
|
+
if (allResourcesProcessed) {
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check stack status AFTER processing events - if rollback in progress, throw
|
|
154
|
+
const stackStatus = await this.cfRepo.getStackStatus(stackIdentifier);
|
|
155
|
+
if (stackStatus.includes('ROLLBACK') && stackStatus !== 'IMPORT_ROLLBACK_COMPLETE') {
|
|
156
|
+
throw new Error('Import operation failed and rolled back');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check final stack status before returning
|
|
161
|
+
const finalStackStatus = await this.cfRepo.getStackStatus(stackIdentifier);
|
|
162
|
+
|
|
163
|
+
// If stack rolled back completely and monitoring just finished,
|
|
164
|
+
// check if we should throw or return
|
|
165
|
+
if (finalStackStatus.includes('ROLLBACK')) {
|
|
166
|
+
// If we have any imported resources, return result (partial success)
|
|
167
|
+
// Otherwise, throw error (complete failure)
|
|
168
|
+
if (importedResources.size === 0 && failedResources.length > 0) {
|
|
169
|
+
throw new Error('Import operation failed and rolled back');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Return result
|
|
174
|
+
const success = failedResources.length === 0;
|
|
175
|
+
return {
|
|
176
|
+
success,
|
|
177
|
+
importedCount: importedResources.size,
|
|
178
|
+
failedCount: failedResources.length,
|
|
179
|
+
failedResources,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Delay helper for polling intervals
|
|
185
|
+
*
|
|
186
|
+
* @param {number} ms - Milliseconds to delay
|
|
187
|
+
* @returns {Promise<void>}
|
|
188
|
+
* @private
|
|
189
|
+
*/
|
|
190
|
+
async _delay(ms) {
|
|
191
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = { ImportProgressMonitor };
|