@friggframework/devtools 2.0.0-next.45 → 2.0.0-next.47
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 +695 -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/application/use-cases/repair-via-import-use-case.test.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for RepairViaImportUseCase
|
|
3
|
+
*
|
|
4
|
+
* Use case for importing orphaned resources into CloudFormation stack
|
|
5
|
+
* (frigg repair --import command)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const RepairViaImportUseCase = require('./repair-via-import-use-case');
|
|
9
|
+
const StackIdentifier = require('../../domain/value-objects/stack-identifier');
|
|
10
|
+
|
|
11
|
+
describe('RepairViaImportUseCase', () => {
|
|
12
|
+
let useCase;
|
|
13
|
+
let mockResourceImporter;
|
|
14
|
+
let mockResourceDetector;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// Mock repositories
|
|
18
|
+
mockResourceImporter = {
|
|
19
|
+
validateImport: jest.fn(),
|
|
20
|
+
importResource: jest.fn(),
|
|
21
|
+
importMultipleResources: jest.fn(),
|
|
22
|
+
getImportStatus: jest.fn(),
|
|
23
|
+
generateTemplateSnippet: jest.fn(),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
mockResourceDetector = {
|
|
27
|
+
getResourceDetails: jest.fn(),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
useCase = new RepairViaImportUseCase({
|
|
31
|
+
resourceImporter: mockResourceImporter,
|
|
32
|
+
resourceDetector: mockResourceDetector,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('importSingleResource', () => {
|
|
37
|
+
it('should import a single orphaned resource', async () => {
|
|
38
|
+
const stackIdentifier = new StackIdentifier({
|
|
39
|
+
stackName: 'my-app-prod',
|
|
40
|
+
region: 'us-east-1',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const resourceToImport = {
|
|
44
|
+
logicalId: 'OrphanedDBCluster',
|
|
45
|
+
physicalId: 'my-orphan-cluster',
|
|
46
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Mock validation
|
|
50
|
+
mockResourceImporter.validateImport.mockResolvedValue({
|
|
51
|
+
canImport: true,
|
|
52
|
+
reason: null,
|
|
53
|
+
warnings: [],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Mock resource details retrieval
|
|
57
|
+
mockResourceDetector.getResourceDetails.mockResolvedValue({
|
|
58
|
+
physicalId: 'my-orphan-cluster',
|
|
59
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
60
|
+
properties: {
|
|
61
|
+
Engine: 'aurora-postgresql',
|
|
62
|
+
EngineVersion: '13.7',
|
|
63
|
+
MasterUsername: 'admin',
|
|
64
|
+
},
|
|
65
|
+
tags: [
|
|
66
|
+
{ Key: 'frigg:stack', Value: 'my-app-prod' },
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Mock import operation
|
|
71
|
+
mockResourceImporter.importResource.mockResolvedValue({
|
|
72
|
+
operationId: 'arn:aws:cloudformation:us-east-1:123456789012:changeSet/import-xyz',
|
|
73
|
+
status: 'IN_PROGRESS',
|
|
74
|
+
message: 'Resource import initiated',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const result = await useCase.importSingleResource({
|
|
78
|
+
stackIdentifier,
|
|
79
|
+
logicalId: resourceToImport.logicalId,
|
|
80
|
+
physicalId: resourceToImport.physicalId,
|
|
81
|
+
resourceType: resourceToImport.resourceType,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(result.success).toBe(true);
|
|
85
|
+
expect(result.operationId).toBeDefined();
|
|
86
|
+
expect(result.status).toBe('IN_PROGRESS');
|
|
87
|
+
expect(mockResourceImporter.validateImport).toHaveBeenCalledWith({
|
|
88
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
89
|
+
physicalId: 'my-orphan-cluster',
|
|
90
|
+
region: 'us-east-1',
|
|
91
|
+
});
|
|
92
|
+
expect(mockResourceImporter.importResource).toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should fail if resource cannot be imported', async () => {
|
|
96
|
+
const stackIdentifier = new StackIdentifier({
|
|
97
|
+
stackName: 'my-app-prod',
|
|
98
|
+
region: 'us-east-1',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Mock validation failure
|
|
102
|
+
mockResourceImporter.validateImport.mockResolvedValue({
|
|
103
|
+
canImport: false,
|
|
104
|
+
reason: 'Resource type AWS::Lambda::Function does not support import',
|
|
105
|
+
warnings: [],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await expect(
|
|
109
|
+
useCase.importSingleResource({
|
|
110
|
+
stackIdentifier,
|
|
111
|
+
logicalId: 'MyFunction',
|
|
112
|
+
physicalId: 'my-function',
|
|
113
|
+
resourceType: 'AWS::Lambda::Function',
|
|
114
|
+
})
|
|
115
|
+
).rejects.toThrow('Resource type AWS::Lambda::Function does not support import');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should include warnings in result', async () => {
|
|
119
|
+
const stackIdentifier = new StackIdentifier({
|
|
120
|
+
stackName: 'my-app-prod',
|
|
121
|
+
region: 'us-east-1',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Mock validation with warnings
|
|
125
|
+
mockResourceImporter.validateImport.mockResolvedValue({
|
|
126
|
+
canImport: true,
|
|
127
|
+
reason: null,
|
|
128
|
+
warnings: ['Resource has manual configuration changes that may be lost'],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
mockResourceDetector.getResourceDetails.mockResolvedValue({
|
|
132
|
+
physicalId: 'vpc-123',
|
|
133
|
+
resourceType: 'AWS::EC2::VPC',
|
|
134
|
+
properties: {
|
|
135
|
+
CidrBlock: '10.0.0.0/16',
|
|
136
|
+
},
|
|
137
|
+
tags: [],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
mockResourceImporter.importResource.mockResolvedValue({
|
|
141
|
+
operationId: 'arn:aws:cloudformation:us-east-1:123456789012:changeSet/import-xyz',
|
|
142
|
+
status: 'IN_PROGRESS',
|
|
143
|
+
message: 'Resource import initiated',
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const result = await useCase.importSingleResource({
|
|
147
|
+
stackIdentifier,
|
|
148
|
+
logicalId: 'MyVPC',
|
|
149
|
+
physicalId: 'vpc-123',
|
|
150
|
+
resourceType: 'AWS::EC2::VPC',
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(result.success).toBe(true);
|
|
154
|
+
expect(result.warnings).toHaveLength(1);
|
|
155
|
+
expect(result.warnings[0]).toContain('manual configuration changes');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('importMultipleResources', () => {
|
|
160
|
+
it('should import multiple orphaned resources in batch', async () => {
|
|
161
|
+
const stackIdentifier = new StackIdentifier({
|
|
162
|
+
stackName: 'my-app-prod',
|
|
163
|
+
region: 'us-east-1',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const resourcesToImport = [
|
|
167
|
+
{
|
|
168
|
+
logicalId: 'OrphanedVPC',
|
|
169
|
+
physicalId: 'vpc-123',
|
|
170
|
+
resourceType: 'AWS::EC2::VPC',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
logicalId: 'OrphanedSubnet',
|
|
174
|
+
physicalId: 'subnet-456',
|
|
175
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
// Mock validation for both resources
|
|
180
|
+
mockResourceImporter.validateImport.mockResolvedValue({
|
|
181
|
+
canImport: true,
|
|
182
|
+
reason: null,
|
|
183
|
+
warnings: [],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Mock resource details
|
|
187
|
+
mockResourceDetector.getResourceDetails
|
|
188
|
+
.mockResolvedValueOnce({
|
|
189
|
+
physicalId: 'vpc-123',
|
|
190
|
+
resourceType: 'AWS::EC2::VPC',
|
|
191
|
+
properties: { CidrBlock: '10.0.0.0/16' },
|
|
192
|
+
tags: [],
|
|
193
|
+
})
|
|
194
|
+
.mockResolvedValueOnce({
|
|
195
|
+
physicalId: 'subnet-456',
|
|
196
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
197
|
+
properties: { CidrBlock: '10.0.1.0/24' },
|
|
198
|
+
tags: [],
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Mock batch import
|
|
202
|
+
mockResourceImporter.importMultipleResources.mockResolvedValue({
|
|
203
|
+
operationId: 'arn:aws:cloudformation:us-east-1:123456789012:changeSet/batch-import-xyz',
|
|
204
|
+
status: 'IN_PROGRESS',
|
|
205
|
+
importedCount: 2,
|
|
206
|
+
failedCount: 0,
|
|
207
|
+
message: 'Batch import initiated',
|
|
208
|
+
details: [
|
|
209
|
+
{ logicalId: 'OrphanedVPC', status: 'IN_PROGRESS' },
|
|
210
|
+
{ logicalId: 'OrphanedSubnet', status: 'IN_PROGRESS' },
|
|
211
|
+
],
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const result = await useCase.importMultipleResources({
|
|
215
|
+
stackIdentifier,
|
|
216
|
+
resources: resourcesToImport,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(result.success).toBe(true);
|
|
220
|
+
expect(result.importedCount).toBe(2);
|
|
221
|
+
expect(result.failedCount).toBe(0);
|
|
222
|
+
expect(mockResourceImporter.importMultipleResources).toHaveBeenCalled();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should handle partial failures in batch import', async () => {
|
|
226
|
+
const stackIdentifier = new StackIdentifier({
|
|
227
|
+
stackName: 'my-app-prod',
|
|
228
|
+
region: 'us-east-1',
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const resourcesToImport = [
|
|
232
|
+
{
|
|
233
|
+
logicalId: 'OrphanedVPC',
|
|
234
|
+
physicalId: 'vpc-123',
|
|
235
|
+
resourceType: 'AWS::EC2::VPC',
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
logicalId: 'InvalidResource',
|
|
239
|
+
physicalId: 'invalid-123',
|
|
240
|
+
resourceType: 'AWS::Lambda::Function',
|
|
241
|
+
},
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
// Mock validation - first passes, second fails
|
|
245
|
+
mockResourceImporter.validateImport
|
|
246
|
+
.mockResolvedValueOnce({
|
|
247
|
+
canImport: true,
|
|
248
|
+
reason: null,
|
|
249
|
+
warnings: [],
|
|
250
|
+
})
|
|
251
|
+
.mockResolvedValueOnce({
|
|
252
|
+
canImport: false,
|
|
253
|
+
reason: 'Resource does not exist',
|
|
254
|
+
warnings: [],
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Mock resource details for first resource only
|
|
258
|
+
mockResourceDetector.getResourceDetails.mockResolvedValueOnce({
|
|
259
|
+
physicalId: 'vpc-123',
|
|
260
|
+
resourceType: 'AWS::EC2::VPC',
|
|
261
|
+
properties: { CidrBlock: '10.0.0.0/16' },
|
|
262
|
+
tags: [],
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const result = await useCase.importMultipleResources({
|
|
266
|
+
stackIdentifier,
|
|
267
|
+
resources: resourcesToImport,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
expect(result.success).toBe(false); // Overall failure due to partial failure
|
|
271
|
+
expect(result.importedCount).toBe(0); // No resources actually imported yet
|
|
272
|
+
expect(result.failedCount).toBe(1);
|
|
273
|
+
expect(result.validationErrors).toHaveLength(1);
|
|
274
|
+
expect(result.validationErrors[0].logicalId).toBe('InvalidResource');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('getImportStatus', () => {
|
|
279
|
+
it('should get status of import operation', async () => {
|
|
280
|
+
const operationId = 'arn:aws:cloudformation:us-east-1:123456789012:changeSet/import-xyz';
|
|
281
|
+
|
|
282
|
+
mockResourceImporter.getImportStatus.mockResolvedValue({
|
|
283
|
+
operationId,
|
|
284
|
+
status: 'COMPLETE',
|
|
285
|
+
progress: 100,
|
|
286
|
+
message: 'Resource import completed successfully',
|
|
287
|
+
completedTime: new Date('2024-01-15T12:00:00Z'),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const status = await useCase.getImportStatus({ operationId });
|
|
291
|
+
|
|
292
|
+
expect(status.status).toBe('COMPLETE');
|
|
293
|
+
expect(status.progress).toBe(100);
|
|
294
|
+
expect(mockResourceImporter.getImportStatus).toHaveBeenCalledWith(operationId);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should return in-progress status for ongoing import', async () => {
|
|
298
|
+
const operationId = 'arn:aws:cloudformation:us-east-1:123456789012:changeSet/import-xyz';
|
|
299
|
+
|
|
300
|
+
mockResourceImporter.getImportStatus.mockResolvedValue({
|
|
301
|
+
operationId,
|
|
302
|
+
status: 'IN_PROGRESS',
|
|
303
|
+
progress: 50,
|
|
304
|
+
message: 'Importing resources...',
|
|
305
|
+
completedTime: null,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const status = await useCase.getImportStatus({ operationId });
|
|
309
|
+
|
|
310
|
+
expect(status.status).toBe('IN_PROGRESS');
|
|
311
|
+
expect(status.progress).toBe(50);
|
|
312
|
+
expect(status.completedTime).toBeNull();
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe('previewImport', () => {
|
|
317
|
+
it('should preview template changes for import', async () => {
|
|
318
|
+
const stackIdentifier = new StackIdentifier({
|
|
319
|
+
stackName: 'my-app-prod',
|
|
320
|
+
region: 'us-east-1',
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Mock resource details
|
|
324
|
+
mockResourceDetector.getResourceDetails.mockResolvedValue({
|
|
325
|
+
physicalId: 'my-orphan-cluster',
|
|
326
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
327
|
+
properties: {
|
|
328
|
+
Engine: 'aurora-postgresql',
|
|
329
|
+
EngineVersion: '13.7',
|
|
330
|
+
},
|
|
331
|
+
tags: [],
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Mock template snippet generation
|
|
335
|
+
mockResourceImporter.generateTemplateSnippet.mockResolvedValue({
|
|
336
|
+
OrphanedDBCluster: {
|
|
337
|
+
Type: 'AWS::RDS::DBCluster',
|
|
338
|
+
Properties: {
|
|
339
|
+
Engine: 'aurora-postgresql',
|
|
340
|
+
EngineVersion: '13.7',
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const preview = await useCase.previewImport({
|
|
346
|
+
stackIdentifier,
|
|
347
|
+
logicalId: 'OrphanedDBCluster',
|
|
348
|
+
physicalId: 'my-orphan-cluster',
|
|
349
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
expect(preview.logicalId).toBe('OrphanedDBCluster');
|
|
353
|
+
expect(preview.physicalId).toBe('my-orphan-cluster');
|
|
354
|
+
expect(preview.templateSnippet).toBeDefined();
|
|
355
|
+
expect(preview.templateSnippet.OrphanedDBCluster.Type).toBe('AWS::RDS::DBCluster');
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe('constructor', () => {
|
|
360
|
+
it('should require resourceImporter', () => {
|
|
361
|
+
expect(() => {
|
|
362
|
+
new RepairViaImportUseCase({
|
|
363
|
+
resourceDetector: mockResourceDetector,
|
|
364
|
+
});
|
|
365
|
+
}).toThrow('resourceImporter is required');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should require resourceDetector', () => {
|
|
369
|
+
expect(() => {
|
|
370
|
+
new RepairViaImportUseCase({
|
|
371
|
+
resourceImporter: mockResourceImporter,
|
|
372
|
+
});
|
|
373
|
+
}).toThrow('resourceDetector is required');
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RunHealthCheckUseCase - Orchestrate Complete Stack Health Check
|
|
3
|
+
*
|
|
4
|
+
* Application Layer - Use Case
|
|
5
|
+
*
|
|
6
|
+
* Business logic for the "frigg doctor" command. Orchestrates multiple
|
|
7
|
+
* repositories and domain services to produce a comprehensive health report.
|
|
8
|
+
*
|
|
9
|
+
* Responsibilities:
|
|
10
|
+
* - Coordinate stack information retrieval
|
|
11
|
+
* - Detect drift at stack and resource level
|
|
12
|
+
* - Find orphaned resources
|
|
13
|
+
* - Analyze property mismatches
|
|
14
|
+
* - Calculate health score
|
|
15
|
+
* - Build comprehensive health report
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const StackHealthReport = require('../../domain/entities/stack-health-report');
|
|
19
|
+
const Resource = require('../../domain/entities/resource');
|
|
20
|
+
const Issue = require('../../domain/entities/issue');
|
|
21
|
+
const ResourceState = require('../../domain/value-objects/resource-state');
|
|
22
|
+
const { getPropertyMutability } = require('../../domain/services/property-mutability-config');
|
|
23
|
+
|
|
24
|
+
class RunHealthCheckUseCase {
|
|
25
|
+
/**
|
|
26
|
+
* Create use case with required dependencies
|
|
27
|
+
*
|
|
28
|
+
* @param {Object} params
|
|
29
|
+
* @param {IStackRepository} params.stackRepository - Stack operations
|
|
30
|
+
* @param {IResourceDetector} params.resourceDetector - Resource discovery
|
|
31
|
+
* @param {MismatchAnalyzer} params.mismatchAnalyzer - Property drift analysis
|
|
32
|
+
* @param {HealthScoreCalculator} params.healthScoreCalculator - Health scoring
|
|
33
|
+
*/
|
|
34
|
+
constructor({ stackRepository, resourceDetector, mismatchAnalyzer, healthScoreCalculator }) {
|
|
35
|
+
if (!stackRepository) {
|
|
36
|
+
throw new Error('stackRepository is required');
|
|
37
|
+
}
|
|
38
|
+
if (!resourceDetector) {
|
|
39
|
+
throw new Error('resourceDetector is required');
|
|
40
|
+
}
|
|
41
|
+
if (!mismatchAnalyzer) {
|
|
42
|
+
throw new Error('mismatchAnalyzer is required');
|
|
43
|
+
}
|
|
44
|
+
if (!healthScoreCalculator) {
|
|
45
|
+
throw new Error('healthScoreCalculator is required');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.stackRepository = stackRepository;
|
|
49
|
+
this.resourceDetector = resourceDetector;
|
|
50
|
+
this.mismatchAnalyzer = mismatchAnalyzer;
|
|
51
|
+
this.healthScoreCalculator = healthScoreCalculator;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Execute complete health check for a stack
|
|
56
|
+
*
|
|
57
|
+
* @param {Object} params
|
|
58
|
+
* @param {StackIdentifier} params.stackIdentifier - Stack to check
|
|
59
|
+
* @param {Function} params.onProgress - Optional progress callback (step, message)
|
|
60
|
+
* @returns {Promise<StackHealthReport>} Comprehensive health report
|
|
61
|
+
*/
|
|
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
|
+
|
|
70
|
+
// 1. Verify stack exists
|
|
71
|
+
progress('📋 Step 1/5:', 'Verifying stack exists...');
|
|
72
|
+
await this.stackRepository.getStack(stackIdentifier);
|
|
73
|
+
|
|
74
|
+
// 2. Detect stack-level drift
|
|
75
|
+
progress('🔍 Step 2/5:', 'Detecting stack drift...');
|
|
76
|
+
const driftDetection = await this.stackRepository.detectStackDrift(stackIdentifier);
|
|
77
|
+
|
|
78
|
+
// 3. Get all stack resources
|
|
79
|
+
progress('📊 Step 3/5:', 'Analyzing stack resources...');
|
|
80
|
+
const stackResources = await this.stackRepository.listResources(stackIdentifier);
|
|
81
|
+
|
|
82
|
+
// 4. Build resource entities with drift status
|
|
83
|
+
const resources = [];
|
|
84
|
+
const issues = [];
|
|
85
|
+
|
|
86
|
+
for (const stackResource of stackResources) {
|
|
87
|
+
let resourceState;
|
|
88
|
+
|
|
89
|
+
// Determine resource state
|
|
90
|
+
if (!stackResource.physicalId || stackResource.driftStatus === 'DELETED') {
|
|
91
|
+
// Missing resource (defined in template but doesn't exist in cloud)
|
|
92
|
+
resourceState = ResourceState.MISSING;
|
|
93
|
+
|
|
94
|
+
// Create issue for missing resource using factory method
|
|
95
|
+
issues.push(
|
|
96
|
+
Issue.missingResource({
|
|
97
|
+
resourceType: stackResource.resourceType,
|
|
98
|
+
resourceId: stackResource.logicalId,
|
|
99
|
+
description: `CloudFormation resource ${stackResource.logicalId} (${stackResource.resourceType}) is defined in the template but does not exist in the cloud.`,
|
|
100
|
+
})
|
|
101
|
+
);
|
|
102
|
+
} else if (stackResource.driftStatus === 'MODIFIED') {
|
|
103
|
+
// Drifted resource - get detailed drift information
|
|
104
|
+
resourceState = ResourceState.DRIFTED;
|
|
105
|
+
|
|
106
|
+
const resourceDrift = await this.stackRepository.getResourceDrift(
|
|
107
|
+
stackIdentifier,
|
|
108
|
+
stackResource.logicalId
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Analyze property mismatches using domain service
|
|
112
|
+
if (
|
|
113
|
+
resourceDrift.propertyDifferences &&
|
|
114
|
+
resourceDrift.propertyDifferences.length > 0
|
|
115
|
+
) {
|
|
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
|
+
});
|
|
133
|
+
|
|
134
|
+
// Create issue for each property mismatch using factory method
|
|
135
|
+
for (const mismatch of propertyMismatches) {
|
|
136
|
+
issues.push(
|
|
137
|
+
Issue.propertyMismatch({
|
|
138
|
+
resourceType: stackResource.resourceType,
|
|
139
|
+
resourceId: stackResource.physicalId,
|
|
140
|
+
mismatch,
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
// Resource is in sync
|
|
147
|
+
resourceState = ResourceState.IN_STACK;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Create resource entity
|
|
151
|
+
// For missing resources, use placeholder physicalId since they don't exist in cloud
|
|
152
|
+
const resource = new Resource({
|
|
153
|
+
logicalId: stackResource.logicalId,
|
|
154
|
+
physicalId: stackResource.physicalId || `missing-${stackResource.logicalId}`,
|
|
155
|
+
resourceType: stackResource.resourceType,
|
|
156
|
+
state: resourceState,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
resources.push(resource);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 5. Find orphaned resources (exist in cloud but not in stack)
|
|
163
|
+
progress('🔎 Step 4/5:', 'Checking for orphaned resources...');
|
|
164
|
+
const orphanedResources = await this.resourceDetector.findOrphanedResources({
|
|
165
|
+
stackIdentifier,
|
|
166
|
+
stackResources,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
for (const orphan of orphanedResources) {
|
|
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
|
|
173
|
+
const orphanResource = new Resource({
|
|
174
|
+
logicalId: null, // No logical ID (not in template)
|
|
175
|
+
physicalId: orphan.physicalId,
|
|
176
|
+
resourceType: orphan.resourceType,
|
|
177
|
+
state: ResourceState.ORPHANED,
|
|
178
|
+
properties: {
|
|
179
|
+
...(orphan.properties || {}), // Include AWS properties
|
|
180
|
+
tags: orphan.tags || {}, // Include tags for logical ID matching
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
resources.push(orphanResource);
|
|
185
|
+
|
|
186
|
+
// Create issue for orphaned resource using factory method
|
|
187
|
+
issues.push(
|
|
188
|
+
Issue.orphanedResource({
|
|
189
|
+
resourceType: orphan.resourceType,
|
|
190
|
+
resourceId: orphan.physicalId,
|
|
191
|
+
description: `Resource ${orphan.physicalId} exists in the cloud but is not managed by CloudFormation stack ${stackIdentifier.stackName}.`,
|
|
192
|
+
})
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 6. Calculate health score using domain service
|
|
197
|
+
progress('🧮 Step 5/5:', 'Calculating health score...');
|
|
198
|
+
const healthScore = this.healthScoreCalculator.calculate({ resources, issues });
|
|
199
|
+
|
|
200
|
+
// 7. Build comprehensive health report (aggregate root)
|
|
201
|
+
const report = new StackHealthReport({
|
|
202
|
+
stackIdentifier,
|
|
203
|
+
healthScore,
|
|
204
|
+
resources,
|
|
205
|
+
issues,
|
|
206
|
+
timestamp: new Date(),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return report;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = RunHealthCheckUseCase;
|