@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
package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UpdateProgressMonitor Tests
|
|
3
|
+
*
|
|
4
|
+
* TDD tests for monitoring CloudFormation UPDATE operations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { UpdateProgressMonitor } = require('../update-progress-monitor');
|
|
8
|
+
const StackIdentifier = require('../../../domain/value-objects/stack-identifier');
|
|
9
|
+
|
|
10
|
+
// Mock timers for testing delays and timeouts
|
|
11
|
+
jest.useFakeTimers();
|
|
12
|
+
|
|
13
|
+
describe('UpdateProgressMonitor', () => {
|
|
14
|
+
let monitor;
|
|
15
|
+
let mockCFRepo;
|
|
16
|
+
let onProgressCallback;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
// Reset mock CloudFormation repository
|
|
20
|
+
mockCFRepo = {
|
|
21
|
+
getStackEvents: jest.fn(),
|
|
22
|
+
getStackStatus: jest.fn(),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Reset progress callback
|
|
26
|
+
onProgressCallback = jest.fn();
|
|
27
|
+
|
|
28
|
+
// Create monitor instance
|
|
29
|
+
monitor = new UpdateProgressMonitor({
|
|
30
|
+
cloudFormationRepository: mockCFRepo,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Clear all timers
|
|
34
|
+
jest.clearAllTimers();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
jest.clearAllTimers();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('constructor', () => {
|
|
42
|
+
it('should require cloudFormationRepository', () => {
|
|
43
|
+
expect(() => new UpdateProgressMonitor({})).toThrow(
|
|
44
|
+
'cloudFormationRepository is required'
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should create instance with valid dependencies', () => {
|
|
49
|
+
const monitor = new UpdateProgressMonitor({
|
|
50
|
+
cloudFormationRepository: mockCFRepo,
|
|
51
|
+
});
|
|
52
|
+
expect(monitor).toBeInstanceOf(UpdateProgressMonitor);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('monitorUpdate - successful update', () => {
|
|
57
|
+
it('should monitor single resource update to completion', async () => {
|
|
58
|
+
const stackIdentifier = new StackIdentifier({
|
|
59
|
+
stackName: 'test-stack',
|
|
60
|
+
region: 'us-east-1',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const resourceLogicalIds = ['AttioLambdaFunction'];
|
|
64
|
+
|
|
65
|
+
// Mock stack events sequence
|
|
66
|
+
mockCFRepo.getStackEvents
|
|
67
|
+
// First poll: UPDATE_IN_PROGRESS
|
|
68
|
+
.mockResolvedValueOnce([
|
|
69
|
+
{
|
|
70
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
71
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
72
|
+
Timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
73
|
+
},
|
|
74
|
+
])
|
|
75
|
+
// Second poll: UPDATE_COMPLETE
|
|
76
|
+
.mockResolvedValueOnce([
|
|
77
|
+
{
|
|
78
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
79
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
80
|
+
Timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
84
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
85
|
+
Timestamp: new Date('2025-01-01T00:00:05Z'),
|
|
86
|
+
},
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
// Mock stack status
|
|
90
|
+
mockCFRepo.getStackStatus.mockResolvedValue('UPDATE_COMPLETE');
|
|
91
|
+
|
|
92
|
+
// Start monitoring in background
|
|
93
|
+
const monitorPromise = monitor.monitorUpdate({
|
|
94
|
+
stackIdentifier,
|
|
95
|
+
resourceLogicalIds,
|
|
96
|
+
onProgress: onProgressCallback,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Advance timers to trigger first poll (2 seconds)
|
|
100
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
101
|
+
|
|
102
|
+
// Advance timers to trigger second poll (2 more seconds)
|
|
103
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
104
|
+
|
|
105
|
+
// Wait for monitoring to complete
|
|
106
|
+
const result = await monitorPromise;
|
|
107
|
+
|
|
108
|
+
// Verify result
|
|
109
|
+
expect(result.success).toBe(true);
|
|
110
|
+
expect(result.updatedCount).toBe(1);
|
|
111
|
+
expect(result.failedCount).toBe(0);
|
|
112
|
+
|
|
113
|
+
// Verify progress callbacks
|
|
114
|
+
expect(onProgressCallback).toHaveBeenCalledWith({
|
|
115
|
+
logicalId: 'AttioLambdaFunction',
|
|
116
|
+
status: 'IN_PROGRESS',
|
|
117
|
+
});
|
|
118
|
+
expect(onProgressCallback).toHaveBeenCalledWith({
|
|
119
|
+
logicalId: 'AttioLambdaFunction',
|
|
120
|
+
status: 'COMPLETE',
|
|
121
|
+
progress: 1,
|
|
122
|
+
total: 1,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should monitor multiple resources updating simultaneously', async () => {
|
|
127
|
+
const stackIdentifier = new StackIdentifier({
|
|
128
|
+
stackName: 'test-stack',
|
|
129
|
+
region: 'us-east-1',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const resourceLogicalIds = [
|
|
133
|
+
'AttioLambdaFunction',
|
|
134
|
+
'PipedriveLambdaFunction',
|
|
135
|
+
'ZohoCrmLambdaFunction',
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
// Mock stack events - all resources update together
|
|
139
|
+
mockCFRepo.getStackEvents
|
|
140
|
+
// First poll: All IN_PROGRESS
|
|
141
|
+
.mockResolvedValueOnce([
|
|
142
|
+
{
|
|
143
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
144
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
145
|
+
Timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
LogicalResourceId: 'PipedriveLambdaFunction',
|
|
149
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
150
|
+
Timestamp: new Date('2025-01-01T00:00:02Z'),
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
LogicalResourceId: 'ZohoCrmLambdaFunction',
|
|
154
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
155
|
+
Timestamp: new Date('2025-01-01T00:00:03Z'),
|
|
156
|
+
},
|
|
157
|
+
])
|
|
158
|
+
// Second poll: First complete
|
|
159
|
+
.mockResolvedValueOnce([
|
|
160
|
+
{
|
|
161
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
162
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
163
|
+
Timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
167
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
168
|
+
Timestamp: new Date('2025-01-01T00:00:10Z'),
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
LogicalResourceId: 'PipedriveLambdaFunction',
|
|
172
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
173
|
+
Timestamp: new Date('2025-01-01T00:00:02Z'),
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
LogicalResourceId: 'ZohoCrmLambdaFunction',
|
|
177
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
178
|
+
Timestamp: new Date('2025-01-01T00:00:03Z'),
|
|
179
|
+
},
|
|
180
|
+
])
|
|
181
|
+
// Third poll: All complete
|
|
182
|
+
.mockResolvedValueOnce([
|
|
183
|
+
{
|
|
184
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
185
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
186
|
+
Timestamp: new Date('2025-01-01T00:00:10Z'),
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
LogicalResourceId: 'PipedriveLambdaFunction',
|
|
190
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
191
|
+
Timestamp: new Date('2025-01-01T00:00:11Z'),
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
LogicalResourceId: 'ZohoCrmLambdaFunction',
|
|
195
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
196
|
+
Timestamp: new Date('2025-01-01T00:00:12Z'),
|
|
197
|
+
},
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
mockCFRepo.getStackStatus.mockResolvedValue('UPDATE_COMPLETE');
|
|
201
|
+
|
|
202
|
+
const monitorPromise = monitor.monitorUpdate({
|
|
203
|
+
stackIdentifier,
|
|
204
|
+
resourceLogicalIds,
|
|
205
|
+
onProgress: onProgressCallback,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Advance through polling intervals
|
|
209
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
210
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
211
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
212
|
+
|
|
213
|
+
const result = await monitorPromise;
|
|
214
|
+
|
|
215
|
+
expect(result.success).toBe(true);
|
|
216
|
+
expect(result.updatedCount).toBe(3);
|
|
217
|
+
expect(result.failedCount).toBe(0);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('monitorUpdate - failed updates', () => {
|
|
222
|
+
it('should detect and report UPDATE_FAILED resources', async () => {
|
|
223
|
+
const stackIdentifier = new StackIdentifier({
|
|
224
|
+
stackName: 'test-stack',
|
|
225
|
+
region: 'us-east-1',
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const resourceLogicalIds = ['AttioLambdaFunction'];
|
|
229
|
+
|
|
230
|
+
mockCFRepo.getStackEvents
|
|
231
|
+
.mockResolvedValueOnce([
|
|
232
|
+
{
|
|
233
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
234
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
235
|
+
Timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
236
|
+
},
|
|
237
|
+
])
|
|
238
|
+
.mockResolvedValueOnce([
|
|
239
|
+
{
|
|
240
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
241
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
242
|
+
Timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
246
|
+
ResourceStatus: 'UPDATE_FAILED',
|
|
247
|
+
ResourceStatusReason: 'Subnet does not exist: subnet-invalid',
|
|
248
|
+
Timestamp: new Date('2025-01-01T00:00:05Z'),
|
|
249
|
+
},
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
mockCFRepo.getStackStatus.mockResolvedValue('UPDATE_ROLLBACK_COMPLETE');
|
|
253
|
+
|
|
254
|
+
const monitorPromise = monitor.monitorUpdate({
|
|
255
|
+
stackIdentifier,
|
|
256
|
+
resourceLogicalIds,
|
|
257
|
+
onProgress: onProgressCallback,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
261
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
262
|
+
|
|
263
|
+
const result = await monitorPromise;
|
|
264
|
+
|
|
265
|
+
expect(result.success).toBe(false);
|
|
266
|
+
expect(result.updatedCount).toBe(0);
|
|
267
|
+
expect(result.failedCount).toBe(1);
|
|
268
|
+
expect(result.failedResources).toHaveLength(1);
|
|
269
|
+
expect(result.failedResources[0].logicalId).toBe('AttioLambdaFunction');
|
|
270
|
+
expect(result.failedResources[0].reason).toBe('Subnet does not exist: subnet-invalid');
|
|
271
|
+
|
|
272
|
+
// Verify FAILED callback was triggered
|
|
273
|
+
expect(onProgressCallback).toHaveBeenCalledWith({
|
|
274
|
+
logicalId: 'AttioLambdaFunction',
|
|
275
|
+
status: 'FAILED',
|
|
276
|
+
reason: 'Subnet does not exist: subnet-invalid',
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should detect stack rollback during update', async () => {
|
|
281
|
+
const stackIdentifier = new StackIdentifier({
|
|
282
|
+
stackName: 'test-stack',
|
|
283
|
+
region: 'us-east-1',
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const resourceLogicalIds = ['AttioLambdaFunction'];
|
|
287
|
+
|
|
288
|
+
mockCFRepo.getStackEvents.mockResolvedValue([
|
|
289
|
+
{
|
|
290
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
291
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
292
|
+
Timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
293
|
+
},
|
|
294
|
+
]);
|
|
295
|
+
|
|
296
|
+
// Stack status shows rollback in progress
|
|
297
|
+
mockCFRepo.getStackStatus.mockResolvedValue('UPDATE_ROLLBACK_IN_PROGRESS');
|
|
298
|
+
|
|
299
|
+
const monitorPromise = monitor.monitorUpdate({
|
|
300
|
+
stackIdentifier,
|
|
301
|
+
resourceLogicalIds,
|
|
302
|
+
onProgress: onProgressCallback,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
306
|
+
|
|
307
|
+
await expect(monitorPromise).rejects.toThrow('Update operation failed and rolled back');
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('monitorUpdate - timeout handling', () => {
|
|
312
|
+
it('should timeout after 5 minutes', async () => {
|
|
313
|
+
const stackIdentifier = new StackIdentifier({
|
|
314
|
+
stackName: 'test-stack',
|
|
315
|
+
region: 'us-east-1',
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const resourceLogicalIds = ['AttioLambdaFunction'];
|
|
319
|
+
|
|
320
|
+
// Mock events that never complete
|
|
321
|
+
mockCFRepo.getStackEvents.mockResolvedValue([
|
|
322
|
+
{
|
|
323
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
324
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
325
|
+
Timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
326
|
+
},
|
|
327
|
+
]);
|
|
328
|
+
|
|
329
|
+
mockCFRepo.getStackStatus.mockResolvedValue('UPDATE_IN_PROGRESS');
|
|
330
|
+
|
|
331
|
+
const monitorPromise = monitor.monitorUpdate({
|
|
332
|
+
stackIdentifier,
|
|
333
|
+
resourceLogicalIds,
|
|
334
|
+
onProgress: onProgressCallback,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Advance past 5 minute timeout (300000ms)
|
|
338
|
+
await jest.advanceTimersByTimeAsync(300000 + 2000);
|
|
339
|
+
|
|
340
|
+
await expect(monitorPromise).rejects.toThrow('Update operation timed out');
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe('monitorUpdate - event deduplication', () => {
|
|
345
|
+
it('should not process duplicate events', async () => {
|
|
346
|
+
const stackIdentifier = new StackIdentifier({
|
|
347
|
+
stackName: 'test-stack',
|
|
348
|
+
region: 'us-east-1',
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const resourceLogicalIds = ['AttioLambdaFunction'];
|
|
352
|
+
|
|
353
|
+
// Mock duplicate events (same timestamp + logicalId + status)
|
|
354
|
+
mockCFRepo.getStackEvents
|
|
355
|
+
.mockResolvedValueOnce([
|
|
356
|
+
{
|
|
357
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
358
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
359
|
+
Timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
360
|
+
},
|
|
361
|
+
])
|
|
362
|
+
.mockResolvedValueOnce([
|
|
363
|
+
// Duplicate event
|
|
364
|
+
{
|
|
365
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
366
|
+
ResourceStatus: 'UPDATE_IN_PROGRESS',
|
|
367
|
+
Timestamp: new Date('2025-01-01T00:00:01Z'),
|
|
368
|
+
},
|
|
369
|
+
// New event
|
|
370
|
+
{
|
|
371
|
+
LogicalResourceId: 'AttioLambdaFunction',
|
|
372
|
+
ResourceStatus: 'UPDATE_COMPLETE',
|
|
373
|
+
Timestamp: new Date('2025-01-01T00:00:05Z'),
|
|
374
|
+
},
|
|
375
|
+
]);
|
|
376
|
+
|
|
377
|
+
mockCFRepo.getStackStatus.mockResolvedValue('UPDATE_COMPLETE');
|
|
378
|
+
|
|
379
|
+
const monitorPromise = monitor.monitorUpdate({
|
|
380
|
+
stackIdentifier,
|
|
381
|
+
resourceLogicalIds,
|
|
382
|
+
onProgress: onProgressCallback,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
386
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
387
|
+
|
|
388
|
+
await monitorPromise;
|
|
389
|
+
|
|
390
|
+
// IN_PROGRESS callback should only be called once (not twice for duplicate)
|
|
391
|
+
const inProgressCalls = onProgressCallback.mock.calls.filter(
|
|
392
|
+
(call) => call[0].status === 'IN_PROGRESS'
|
|
393
|
+
);
|
|
394
|
+
expect(inProgressCalls).toHaveLength(1);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
describe('monitorUpdate - no resources to track', () => {
|
|
399
|
+
it('should return immediately if no resources to track', async () => {
|
|
400
|
+
const stackIdentifier = new StackIdentifier({
|
|
401
|
+
stackName: 'test-stack',
|
|
402
|
+
region: 'us-east-1',
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const result = await monitor.monitorUpdate({
|
|
406
|
+
stackIdentifier,
|
|
407
|
+
resourceLogicalIds: [],
|
|
408
|
+
onProgress: onProgressCallback,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
expect(result.success).toBe(true);
|
|
412
|
+
expect(result.updatedCount).toBe(0);
|
|
413
|
+
expect(result.failedCount).toBe(0);
|
|
414
|
+
|
|
415
|
+
// Should not have polled CloudFormation
|
|
416
|
+
expect(mockCFRepo.getStackEvents).not.toHaveBeenCalled();
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
});
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HealthScoreCalculator Domain Service
|
|
3
|
+
*
|
|
4
|
+
* Calculates health scores (0-100) based on percentage-based penalties
|
|
5
|
+
* weighted by resource criticality.
|
|
6
|
+
*
|
|
7
|
+
* Resource Criticality:
|
|
8
|
+
* - Critical: Lambda, RDS, DynamoDB (affect application functionality)
|
|
9
|
+
* - Infrastructure: VPC, Subnet, SecurityGroup, KMS, etc.
|
|
10
|
+
*
|
|
11
|
+
* Percentage-Based Penalties (max 100 points):
|
|
12
|
+
* - Critical issues (orphaned, missing): up to 50 points (% of total resources)
|
|
13
|
+
* - Functional drift: up to 30 points (% of critical resources drifted)
|
|
14
|
+
* - Infrastructure drift: up to 20 points (% of infrastructure resources drifted)
|
|
15
|
+
*
|
|
16
|
+
* Example: 16/16 Lambdas drifted = 100% functional drift = 30 penalty → 70/100 score
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const HealthScore = require('../value-objects/health-score');
|
|
20
|
+
|
|
21
|
+
class HealthScoreCalculator {
|
|
22
|
+
/**
|
|
23
|
+
* Critical resource types that affect application functionality
|
|
24
|
+
* @private
|
|
25
|
+
*/
|
|
26
|
+
static CRITICAL_RESOURCE_TYPES = [
|
|
27
|
+
'AWS::Lambda::Function',
|
|
28
|
+
'AWS::RDS::DBCluster',
|
|
29
|
+
'AWS::RDS::DBInstance',
|
|
30
|
+
'AWS::DynamoDB::Table',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Maximum penalties for each category (sum = 100)
|
|
35
|
+
* @private
|
|
36
|
+
*/
|
|
37
|
+
static MAX_PENALTIES = {
|
|
38
|
+
criticalIssues: 50, // Orphaned resources, missing resources
|
|
39
|
+
functionalDrift: 30, // Drift on critical resources (Lambda, RDS, DynamoDB)
|
|
40
|
+
infrastructureDrift: 20, // Drift on infrastructure resources (VPC, networking, KMS)
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create a new HealthScoreCalculator
|
|
45
|
+
*
|
|
46
|
+
* @param {Object} [config={}]
|
|
47
|
+
* @param {Object} [config.maxPenalties] - Custom max penalty configuration
|
|
48
|
+
* @param {number} [config.maxPenalties.criticalIssues] - Max penalty for critical issues (default: 50)
|
|
49
|
+
* @param {number} [config.maxPenalties.functionalDrift] - Max penalty for functional drift (default: 30)
|
|
50
|
+
* @param {number} [config.maxPenalties.infrastructureDrift] - Max penalty for infra drift (default: 20)
|
|
51
|
+
*/
|
|
52
|
+
constructor(config = {}) {
|
|
53
|
+
this.maxPenalties = {
|
|
54
|
+
...HealthScoreCalculator.MAX_PENALTIES,
|
|
55
|
+
...(config.maxPenalties || {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if resource type is critical (affects functionality)
|
|
61
|
+
* @private
|
|
62
|
+
*/
|
|
63
|
+
_isCriticalResourceType(resourceType) {
|
|
64
|
+
return HealthScoreCalculator.CRITICAL_RESOURCE_TYPES.includes(resourceType);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Calculate health score based on percentage-based penalties
|
|
69
|
+
*
|
|
70
|
+
* @param {Object} params
|
|
71
|
+
* @param {Resource[]} params.resources - Resources in the stack
|
|
72
|
+
* @param {Issue[]} params.issues - Detected issues
|
|
73
|
+
* @returns {HealthScore}
|
|
74
|
+
*/
|
|
75
|
+
calculate({ resources, issues }) {
|
|
76
|
+
const startingScore = 100;
|
|
77
|
+
|
|
78
|
+
// Handle empty stack edge case
|
|
79
|
+
if (resources.length === 0) {
|
|
80
|
+
return new HealthScore(startingScore);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Categorize resources by criticality
|
|
84
|
+
const criticalResources = resources.filter((r) =>
|
|
85
|
+
this._isCriticalResourceType(r.resourceType)
|
|
86
|
+
);
|
|
87
|
+
const infraResources = resources.filter(
|
|
88
|
+
(r) => !this._isCriticalResourceType(r.resourceType)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Count issues by category
|
|
92
|
+
const criticalIssues = issues.filter(
|
|
93
|
+
(issue) => issue.type === 'ORPHANED_RESOURCE' || issue.type === 'MISSING_RESOURCE'
|
|
94
|
+
);
|
|
95
|
+
const functionalDriftIssues = issues.filter(
|
|
96
|
+
(issue) =>
|
|
97
|
+
issue.type === 'PROPERTY_MISMATCH' &&
|
|
98
|
+
this._isCriticalResourceType(issue.resourceType)
|
|
99
|
+
);
|
|
100
|
+
const infraDriftIssues = issues.filter(
|
|
101
|
+
(issue) =>
|
|
102
|
+
issue.type === 'PROPERTY_MISMATCH' &&
|
|
103
|
+
!this._isCriticalResourceType(issue.resourceType)
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Calculate percentage-based penalties
|
|
107
|
+
let totalPenalty = 0;
|
|
108
|
+
|
|
109
|
+
// 1. Critical issues penalty (up to 50 points)
|
|
110
|
+
if (criticalIssues.length > 0) {
|
|
111
|
+
const criticalImpactPercent = criticalIssues.length / resources.length;
|
|
112
|
+
totalPenalty += criticalImpactPercent * this.maxPenalties.criticalIssues;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 2. Functional drift penalty (up to 30 points)
|
|
116
|
+
if (functionalDriftIssues.length > 0 && criticalResources.length > 0) {
|
|
117
|
+
// Get unique drifted critical resources
|
|
118
|
+
const driftedCriticalResourceIds = new Set(
|
|
119
|
+
functionalDriftIssues.map((issue) => issue.resourceId)
|
|
120
|
+
);
|
|
121
|
+
const functionalDriftPercent =
|
|
122
|
+
driftedCriticalResourceIds.size / criticalResources.length;
|
|
123
|
+
totalPenalty += functionalDriftPercent * this.maxPenalties.functionalDrift;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 3. Infrastructure drift penalty (up to 20 points)
|
|
127
|
+
if (infraDriftIssues.length > 0 && infraResources.length > 0) {
|
|
128
|
+
// Get unique drifted infrastructure resources
|
|
129
|
+
const driftedInfraResourceIds = new Set(
|
|
130
|
+
infraDriftIssues.map((issue) => issue.resourceId)
|
|
131
|
+
);
|
|
132
|
+
const infraDriftPercent = driftedInfraResourceIds.size / infraResources.length;
|
|
133
|
+
totalPenalty += infraDriftPercent * this.maxPenalties.infrastructureDrift;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Calculate final score (capped at 0)
|
|
137
|
+
const finalScore = Math.max(0, Math.round(startingScore - totalPenalty));
|
|
138
|
+
|
|
139
|
+
return new HealthScore(finalScore);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Explain the score calculation with detailed percentage-based breakdown
|
|
144
|
+
*
|
|
145
|
+
* @param {Object} params
|
|
146
|
+
* @param {Resource[]} params.resources - Resources in the stack
|
|
147
|
+
* @param {Issue[]} params.issues - Detected issues
|
|
148
|
+
* @returns {Object} Explanation with breakdown
|
|
149
|
+
*/
|
|
150
|
+
explainScore({ resources, issues }) {
|
|
151
|
+
const startingScore = 100;
|
|
152
|
+
|
|
153
|
+
if (resources.length === 0) {
|
|
154
|
+
return {
|
|
155
|
+
finalScore: startingScore,
|
|
156
|
+
startingScore,
|
|
157
|
+
totalPenalty: 0,
|
|
158
|
+
breakdown: {
|
|
159
|
+
criticalIssues: { count: 0, impactPercent: 0, penalty: 0 },
|
|
160
|
+
functionalDrift: { count: 0, impactPercent: 0, penalty: 0 },
|
|
161
|
+
infrastructureDrift: { count: 0, impactPercent: 0, penalty: 0 },
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Categorize resources
|
|
167
|
+
const criticalResources = resources.filter((r) =>
|
|
168
|
+
this._isCriticalResourceType(r.resourceType)
|
|
169
|
+
);
|
|
170
|
+
const infraResources = resources.filter(
|
|
171
|
+
(r) => !this._isCriticalResourceType(r.resourceType)
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Count issues by category
|
|
175
|
+
const criticalIssues = issues.filter(
|
|
176
|
+
(issue) => issue.type === 'ORPHANED_RESOURCE' || issue.type === 'MISSING_RESOURCE'
|
|
177
|
+
);
|
|
178
|
+
const functionalDriftIssues = issues.filter(
|
|
179
|
+
(issue) =>
|
|
180
|
+
issue.type === 'PROPERTY_MISMATCH' &&
|
|
181
|
+
this._isCriticalResourceType(issue.resourceType)
|
|
182
|
+
);
|
|
183
|
+
const infraDriftIssues = issues.filter(
|
|
184
|
+
(issue) =>
|
|
185
|
+
issue.type === 'PROPERTY_MISMATCH' &&
|
|
186
|
+
!this._isCriticalResourceType(issue.resourceType)
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Calculate penalties
|
|
190
|
+
const breakdown = {
|
|
191
|
+
criticalIssues: {
|
|
192
|
+
count: criticalIssues.length,
|
|
193
|
+
impactPercent: criticalIssues.length / resources.length,
|
|
194
|
+
penalty:
|
|
195
|
+
(criticalIssues.length / resources.length) * this.maxPenalties.criticalIssues,
|
|
196
|
+
},
|
|
197
|
+
functionalDrift: {
|
|
198
|
+
count: functionalDriftIssues.length,
|
|
199
|
+
impactPercent:
|
|
200
|
+
criticalResources.length > 0
|
|
201
|
+
? new Set(functionalDriftIssues.map((i) => i.resourceId)).size /
|
|
202
|
+
criticalResources.length
|
|
203
|
+
: 0,
|
|
204
|
+
penalty:
|
|
205
|
+
criticalResources.length > 0
|
|
206
|
+
? (new Set(functionalDriftIssues.map((i) => i.resourceId)).size /
|
|
207
|
+
criticalResources.length) *
|
|
208
|
+
this.maxPenalties.functionalDrift
|
|
209
|
+
: 0,
|
|
210
|
+
},
|
|
211
|
+
infrastructureDrift: {
|
|
212
|
+
count: infraDriftIssues.length,
|
|
213
|
+
impactPercent:
|
|
214
|
+
infraResources.length > 0
|
|
215
|
+
? new Set(infraDriftIssues.map((i) => i.resourceId)).size /
|
|
216
|
+
infraResources.length
|
|
217
|
+
: 0,
|
|
218
|
+
penalty:
|
|
219
|
+
infraResources.length > 0
|
|
220
|
+
? (new Set(infraDriftIssues.map((i) => i.resourceId)).size /
|
|
221
|
+
infraResources.length) *
|
|
222
|
+
this.maxPenalties.infrastructureDrift
|
|
223
|
+
: 0,
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const totalPenalty =
|
|
228
|
+
breakdown.criticalIssues.penalty +
|
|
229
|
+
breakdown.functionalDrift.penalty +
|
|
230
|
+
breakdown.infrastructureDrift.penalty;
|
|
231
|
+
|
|
232
|
+
const finalScore = Math.max(0, Math.round(startingScore - totalPenalty));
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
finalScore,
|
|
236
|
+
startingScore,
|
|
237
|
+
totalPenalty: Math.round(totalPenalty * 100) / 100, // Round to 2 decimal places
|
|
238
|
+
breakdown,
|
|
239
|
+
resourceCounts: {
|
|
240
|
+
total: resources.length,
|
|
241
|
+
critical: criticalResources.length,
|
|
242
|
+
infrastructure: infraResources.length,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
module.exports = HealthScoreCalculator;
|