@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,784 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWSPropertyReconciler - AWS Property Drift Reconciliation Adapter
|
|
3
|
+
*
|
|
4
|
+
* Infrastructure Adapter - Hexagonal Architecture
|
|
5
|
+
*
|
|
6
|
+
* Implements IPropertyReconciler port for AWS.
|
|
7
|
+
* Handles property drift reconciliation via template or resource updates.
|
|
8
|
+
*
|
|
9
|
+
* Lazy-loads AWS SDK to minimize cold start time and memory usage.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const IPropertyReconciler = require('../../application/ports/IPropertyReconciler');
|
|
13
|
+
|
|
14
|
+
// Lazy-loaded AWS SDK clients
|
|
15
|
+
let CloudFormationClient, UpdateStackCommand, GetTemplateCommand;
|
|
16
|
+
let EC2Client, ModifyVpcAttributeCommand;
|
|
17
|
+
let LambdaClient, UpdateFunctionConfigurationCommand;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Lazy load CloudFormation SDK
|
|
21
|
+
*/
|
|
22
|
+
function loadCloudFormation() {
|
|
23
|
+
if (!CloudFormationClient) {
|
|
24
|
+
const cfModule = require('@aws-sdk/client-cloudformation');
|
|
25
|
+
CloudFormationClient = cfModule.CloudFormationClient;
|
|
26
|
+
UpdateStackCommand = cfModule.UpdateStackCommand;
|
|
27
|
+
GetTemplateCommand = cfModule.GetTemplateCommand;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Lazy load EC2 SDK
|
|
33
|
+
*/
|
|
34
|
+
function loadEC2() {
|
|
35
|
+
if (!EC2Client) {
|
|
36
|
+
const ec2Module = require('@aws-sdk/client-ec2');
|
|
37
|
+
EC2Client = ec2Module.EC2Client;
|
|
38
|
+
ModifyVpcAttributeCommand = ec2Module.ModifyVpcAttributeCommand;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Lazy load Lambda SDK
|
|
44
|
+
*/
|
|
45
|
+
function loadLambda() {
|
|
46
|
+
if (!LambdaClient) {
|
|
47
|
+
const lambdaModule = require('@aws-sdk/client-lambda');
|
|
48
|
+
LambdaClient = lambdaModule.LambdaClient;
|
|
49
|
+
UpdateFunctionConfigurationCommand = lambdaModule.UpdateFunctionConfigurationCommand;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
class AWSPropertyReconciler extends IPropertyReconciler {
|
|
54
|
+
/**
|
|
55
|
+
* Resource types that support reconciliation
|
|
56
|
+
* @private
|
|
57
|
+
*/
|
|
58
|
+
static SUPPORTED_TYPES = {
|
|
59
|
+
'AWS::EC2::VPC': {
|
|
60
|
+
templateUpdate: true,
|
|
61
|
+
resourceUpdate: true,
|
|
62
|
+
recommendedMode: 'template',
|
|
63
|
+
limitations: ['Some VPC properties require resource replacement (e.g., CidrBlock)'],
|
|
64
|
+
},
|
|
65
|
+
'AWS::EC2::Subnet': {
|
|
66
|
+
templateUpdate: true,
|
|
67
|
+
resourceUpdate: false,
|
|
68
|
+
recommendedMode: 'template',
|
|
69
|
+
limitations: ['Most Subnet properties are immutable'],
|
|
70
|
+
},
|
|
71
|
+
'AWS::EC2::SecurityGroup': {
|
|
72
|
+
templateUpdate: true,
|
|
73
|
+
resourceUpdate: true,
|
|
74
|
+
recommendedMode: 'template',
|
|
75
|
+
limitations: ['Rule changes may cause brief connectivity interruption'],
|
|
76
|
+
},
|
|
77
|
+
'AWS::EC2::RouteTable': {
|
|
78
|
+
templateUpdate: true,
|
|
79
|
+
resourceUpdate: false,
|
|
80
|
+
recommendedMode: 'template',
|
|
81
|
+
limitations: ['Route changes require CloudFormation update'],
|
|
82
|
+
},
|
|
83
|
+
'AWS::RDS::DBCluster': {
|
|
84
|
+
templateUpdate: true,
|
|
85
|
+
resourceUpdate: false,
|
|
86
|
+
recommendedMode: 'template',
|
|
87
|
+
limitations: ['Many DBCluster properties require specific update windows'],
|
|
88
|
+
},
|
|
89
|
+
'AWS::KMS::Key': {
|
|
90
|
+
templateUpdate: true,
|
|
91
|
+
resourceUpdate: false,
|
|
92
|
+
recommendedMode: 'template',
|
|
93
|
+
limitations: ['Key policy changes must be done via CloudFormation'],
|
|
94
|
+
},
|
|
95
|
+
'AWS::Lambda::Function': {
|
|
96
|
+
templateUpdate: true,
|
|
97
|
+
resourceUpdate: true,
|
|
98
|
+
recommendedMode: 'template',
|
|
99
|
+
limitations: [
|
|
100
|
+
'VpcConfig changes may take several minutes to propagate',
|
|
101
|
+
'Code updates are handled separately via UpdateFunctionCode',
|
|
102
|
+
'Environment variable changes may cause brief invocation errors during update',
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create AWS Property Reconciler
|
|
109
|
+
*
|
|
110
|
+
* @param {Object} [config={}]
|
|
111
|
+
* @param {string} [config.region] - AWS region (defaults to AWS_REGION env var)
|
|
112
|
+
* @param {Object} [config.cloudFormationRepository] - CloudFormation repository for monitoring
|
|
113
|
+
*/
|
|
114
|
+
constructor(config = {}) {
|
|
115
|
+
super();
|
|
116
|
+
this.region = config.region || process.env.AWS_REGION || 'us-east-1';
|
|
117
|
+
this.cfClient = null;
|
|
118
|
+
this.ec2Client = null;
|
|
119
|
+
this.lambdaClient = null;
|
|
120
|
+
this.cfRepo = config.cloudFormationRepository || null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get or create CloudFormation client
|
|
125
|
+
* @private
|
|
126
|
+
*/
|
|
127
|
+
_getCFClient() {
|
|
128
|
+
if (!this.cfClient) {
|
|
129
|
+
loadCloudFormation();
|
|
130
|
+
this.cfClient = new CloudFormationClient({ region: this.region });
|
|
131
|
+
}
|
|
132
|
+
return this.cfClient;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get or create EC2 client
|
|
137
|
+
* @private
|
|
138
|
+
*/
|
|
139
|
+
_getEC2Client() {
|
|
140
|
+
if (!this.ec2Client) {
|
|
141
|
+
loadEC2();
|
|
142
|
+
this.ec2Client = new EC2Client({ region: this.region });
|
|
143
|
+
}
|
|
144
|
+
return this.ec2Client;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get or create Lambda client
|
|
149
|
+
* @private
|
|
150
|
+
*/
|
|
151
|
+
_getLambdaClient() {
|
|
152
|
+
if (!this.lambdaClient) {
|
|
153
|
+
loadLambda();
|
|
154
|
+
this.lambdaClient = new LambdaClient({ region: this.region });
|
|
155
|
+
}
|
|
156
|
+
return this.lambdaClient;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if a property mismatch can be auto-fixed
|
|
161
|
+
*/
|
|
162
|
+
async canReconcile(mismatch) {
|
|
163
|
+
// Immutable properties cannot be reconciled (require replacement)
|
|
164
|
+
if (mismatch.requiresReplacement()) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Mutable and conditional properties can be reconciled
|
|
169
|
+
// Note: CONDITIONAL may require additional validation, but we treat it as reconcilable
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Reconcile a single property mismatch
|
|
175
|
+
*/
|
|
176
|
+
async reconcileProperty({ stackIdentifier, logicalId, mismatch, mode = 'template' }) {
|
|
177
|
+
if (mode === 'template') {
|
|
178
|
+
return await this._reconcileViaTemplate({
|
|
179
|
+
stackIdentifier,
|
|
180
|
+
logicalId,
|
|
181
|
+
mismatch,
|
|
182
|
+
});
|
|
183
|
+
} else {
|
|
184
|
+
return await this._reconcileViaResource({
|
|
185
|
+
stackIdentifier,
|
|
186
|
+
logicalId,
|
|
187
|
+
mismatch,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Reconcile multiple property mismatches for a resource
|
|
194
|
+
*
|
|
195
|
+
* IMPORTANT: Batches all property updates into a SINGLE UpdateStack call
|
|
196
|
+
* to avoid "stack is already updating" errors from CloudFormation.
|
|
197
|
+
*
|
|
198
|
+
* MONITORING: After calling UpdateStack, monitors the stack until UPDATE_COMPLETE
|
|
199
|
+
* or UPDATE_FAILED to ensure the update actually succeeded.
|
|
200
|
+
*/
|
|
201
|
+
async reconcileMultipleProperties({
|
|
202
|
+
stackIdentifier,
|
|
203
|
+
logicalId,
|
|
204
|
+
physicalId,
|
|
205
|
+
resourceType,
|
|
206
|
+
mismatches,
|
|
207
|
+
mode = 'template',
|
|
208
|
+
progressMonitor = null, // Optional UpdateProgressMonitor for async tracking
|
|
209
|
+
}) {
|
|
210
|
+
// Route to appropriate reconciliation method based on mode
|
|
211
|
+
if (mode === 'resource') {
|
|
212
|
+
return await this._reconcileMultiplePropertiesViaResource({
|
|
213
|
+
stackIdentifier,
|
|
214
|
+
logicalId,
|
|
215
|
+
physicalId,
|
|
216
|
+
resourceType,
|
|
217
|
+
mismatches,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Template mode (original implementation)
|
|
222
|
+
const results = [];
|
|
223
|
+
let reconciledCount = 0;
|
|
224
|
+
let failedCount = 0;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const client = this._getCFClient();
|
|
228
|
+
|
|
229
|
+
// 1. Get current template ONCE
|
|
230
|
+
const getTemplateCommand = new GetTemplateCommand({
|
|
231
|
+
StackName: stackIdentifier.stackName,
|
|
232
|
+
TemplateStage: 'Original',
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const templateResponse = await client.send(getTemplateCommand);
|
|
236
|
+
const template = JSON.parse(templateResponse.TemplateBody);
|
|
237
|
+
|
|
238
|
+
// 2. Apply ALL property changes to the template
|
|
239
|
+
for (const mismatch of mismatches) {
|
|
240
|
+
try {
|
|
241
|
+
// Navigate to the property in the template
|
|
242
|
+
// AWS drift detection returns paths without 'Properties.' prefix (e.g., 'VpcConfig.SubnetIds')
|
|
243
|
+
// But CloudFormation templates have 'Properties' section, so we need to navigate there
|
|
244
|
+
const pathParts = mismatch.propertyPath.split('.');
|
|
245
|
+
let current = template.Resources[logicalId];
|
|
246
|
+
|
|
247
|
+
// Ensure Properties section exists
|
|
248
|
+
if (!current.Properties) {
|
|
249
|
+
current.Properties = {};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Start navigation at Properties level
|
|
253
|
+
current = current.Properties;
|
|
254
|
+
|
|
255
|
+
// Create nested objects if they don't exist
|
|
256
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
257
|
+
if (!current[pathParts[i]]) {
|
|
258
|
+
current[pathParts[i]] = {};
|
|
259
|
+
}
|
|
260
|
+
current = current[pathParts[i]];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Update the property value
|
|
264
|
+
const lastPart = pathParts[pathParts.length - 1];
|
|
265
|
+
current[lastPart] = mismatch.actualValue;
|
|
266
|
+
|
|
267
|
+
// Track as pending (will be confirmed by monitor)
|
|
268
|
+
results.push({
|
|
269
|
+
success: true,
|
|
270
|
+
mode: 'template',
|
|
271
|
+
propertyPath: mismatch.propertyPath,
|
|
272
|
+
oldValue: mismatch.expectedValue,
|
|
273
|
+
newValue: mismatch.actualValue,
|
|
274
|
+
message: 'Property updated in template',
|
|
275
|
+
});
|
|
276
|
+
reconciledCount++;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
// Track as failed
|
|
279
|
+
results.push({
|
|
280
|
+
success: false,
|
|
281
|
+
mode: 'template',
|
|
282
|
+
propertyPath: mismatch.propertyPath,
|
|
283
|
+
message: `Failed to update property: ${error.message}`,
|
|
284
|
+
});
|
|
285
|
+
failedCount++;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 3. If any properties were updated, call UpdateStack ONCE with all changes
|
|
290
|
+
if (reconciledCount > 0) {
|
|
291
|
+
const templateBody = JSON.stringify(template);
|
|
292
|
+
const templateSize = templateBody.length;
|
|
293
|
+
const TEMPLATE_SIZE_LIMIT = 51200; // CloudFormation inline template limit
|
|
294
|
+
|
|
295
|
+
// Use S3 for large templates, inline for small templates
|
|
296
|
+
const updateParams = {
|
|
297
|
+
StackName: stackIdentifier.stackName,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
if (templateSize > TEMPLATE_SIZE_LIMIT && this.cfRepo) {
|
|
301
|
+
// Upload template to S3 and use TemplateURL
|
|
302
|
+
const templateUrl = await this.cfRepo.uploadTemplate({
|
|
303
|
+
stackName: stackIdentifier.stackName,
|
|
304
|
+
templateBody,
|
|
305
|
+
});
|
|
306
|
+
updateParams.TemplateURL = templateUrl;
|
|
307
|
+
} else {
|
|
308
|
+
// Use inline template body
|
|
309
|
+
updateParams.TemplateBody = templateBody;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Add capabilities required for IAM resources
|
|
313
|
+
updateParams.Capabilities = ['CAPABILITY_NAMED_IAM'];
|
|
314
|
+
|
|
315
|
+
const updateCommand = new UpdateStackCommand(updateParams);
|
|
316
|
+
await client.send(updateCommand);
|
|
317
|
+
|
|
318
|
+
// 4. Monitor UpdateStack operation if CloudFormation repository available
|
|
319
|
+
if (this.cfRepo) {
|
|
320
|
+
const { UpdateProgressMonitor } = require('../../domain/services/update-progress-monitor');
|
|
321
|
+
const monitor = new UpdateProgressMonitor({
|
|
322
|
+
cloudFormationRepository: this.cfRepo,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const monitorResult = await monitor.monitorUpdate({
|
|
326
|
+
stackIdentifier,
|
|
327
|
+
resourceLogicalIds: [logicalId],
|
|
328
|
+
onProgress: (progress) => {
|
|
329
|
+
// Progress callback for UI updates (optional)
|
|
330
|
+
if (progress.status === 'FAILED') {
|
|
331
|
+
console.log(` ⚠ ${progress.logicalId}: Update failed - ${progress.reason}`);
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// If monitoring detected failures, update results
|
|
337
|
+
if (!monitorResult.success) {
|
|
338
|
+
reconciledCount = 0;
|
|
339
|
+
failedCount = mismatches.length;
|
|
340
|
+
results.forEach(r => {
|
|
341
|
+
r.success = false;
|
|
342
|
+
r.message = 'CloudFormation update failed';
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
reconciledCount,
|
|
347
|
+
failedCount,
|
|
348
|
+
results,
|
|
349
|
+
message: `Update failed: ${monitorResult.failedResources.map(f => f.reason).join(', ')}`,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
} catch (error) {
|
|
355
|
+
// If UpdateStack fails, mark all as failed
|
|
356
|
+
return {
|
|
357
|
+
reconciledCount: 0,
|
|
358
|
+
failedCount: mismatches.length,
|
|
359
|
+
results: mismatches.map(m => ({
|
|
360
|
+
success: false,
|
|
361
|
+
mode: 'template',
|
|
362
|
+
propertyPath: m.propertyPath,
|
|
363
|
+
message: `UpdateStack failed: ${error.message}`,
|
|
364
|
+
})),
|
|
365
|
+
message: `UpdateStack failed: ${error.message}`,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
reconciledCount,
|
|
371
|
+
failedCount,
|
|
372
|
+
results,
|
|
373
|
+
message: `Reconciled ${reconciledCount} of ${mismatches.length} properties in single UpdateStack call`,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Preview property reconciliation without applying changes
|
|
379
|
+
*/
|
|
380
|
+
async previewReconciliation({ stackIdentifier, logicalId, mismatch, mode = 'template' }) {
|
|
381
|
+
const canReconcile = await this.canReconcile(mismatch);
|
|
382
|
+
|
|
383
|
+
const warnings = [];
|
|
384
|
+
if (mismatch.requiresReplacement()) {
|
|
385
|
+
warnings.push('Property is immutable - requires resource replacement');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let impact = '';
|
|
389
|
+
if (mode === 'template') {
|
|
390
|
+
impact = 'Will update CloudFormation template to match actual resource state';
|
|
391
|
+
} else {
|
|
392
|
+
impact = 'Will update cloud resource to match template definition';
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
canReconcile,
|
|
397
|
+
mode,
|
|
398
|
+
propertyPath: mismatch.propertyPath,
|
|
399
|
+
currentValue: mismatch.expectedValue,
|
|
400
|
+
proposedValue: mismatch.actualValue,
|
|
401
|
+
impact,
|
|
402
|
+
warnings,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Update CloudFormation template property
|
|
408
|
+
*/
|
|
409
|
+
async updateTemplateProperty({ stackIdentifier, logicalId, propertyPath, newValue }) {
|
|
410
|
+
const client = this._getCFClient();
|
|
411
|
+
|
|
412
|
+
// Get current template
|
|
413
|
+
const getTemplateCommand = new GetTemplateCommand({
|
|
414
|
+
StackName: stackIdentifier.stackName,
|
|
415
|
+
TemplateStage: 'Original',
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const templateResponse = await client.send(getTemplateCommand);
|
|
419
|
+
const template = JSON.parse(templateResponse.TemplateBody);
|
|
420
|
+
|
|
421
|
+
// Update property in template
|
|
422
|
+
const pathParts = propertyPath.split('.');
|
|
423
|
+
let current = template.Resources[logicalId];
|
|
424
|
+
|
|
425
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
426
|
+
if (!current[pathParts[i]]) {
|
|
427
|
+
current[pathParts[i]] = {};
|
|
428
|
+
}
|
|
429
|
+
current = current[pathParts[i]];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const lastPart = pathParts[pathParts.length - 1];
|
|
433
|
+
current[lastPart] = newValue;
|
|
434
|
+
|
|
435
|
+
// Update stack with new template
|
|
436
|
+
const updateCommand = new UpdateStackCommand({
|
|
437
|
+
StackName: stackIdentifier.stackName,
|
|
438
|
+
TemplateBody: JSON.stringify(template),
|
|
439
|
+
Capabilities: ['CAPABILITY_NAMED_IAM'],
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const updateResponse = await client.send(updateCommand);
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
success: true,
|
|
446
|
+
changeSetId: updateResponse.StackId,
|
|
447
|
+
message: 'Template property updated successfully',
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Update cloud resource property directly
|
|
453
|
+
*/
|
|
454
|
+
async updateResourceProperty({ resourceType, physicalId, region, propertyPath, newValue }) {
|
|
455
|
+
// Only VPC properties are supported for direct resource updates in this implementation
|
|
456
|
+
if (resourceType === 'AWS::EC2::VPC') {
|
|
457
|
+
return await this._updateVpcProperty({ physicalId, propertyPath, newValue });
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
throw new Error(`Resource type ${resourceType} updates not supported`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Get reconciliation strategy for a resource type
|
|
465
|
+
*/
|
|
466
|
+
async getReconciliationStrategy(resourceType) {
|
|
467
|
+
if (!(resourceType in AWSPropertyReconciler.SUPPORTED_TYPES)) {
|
|
468
|
+
throw new Error(`Resource type ${resourceType} not supported`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const config = AWSPropertyReconciler.SUPPORTED_TYPES[resourceType];
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
supportsTemplateUpdate: config.templateUpdate,
|
|
475
|
+
supportsResourceUpdate: config.resourceUpdate,
|
|
476
|
+
recommendedMode: config.recommendedMode,
|
|
477
|
+
limitations: config.limitations,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ========================================
|
|
482
|
+
// Private Helper Methods
|
|
483
|
+
// ========================================
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Reconcile via template update
|
|
487
|
+
* @private
|
|
488
|
+
*/
|
|
489
|
+
async _reconcileViaTemplate({ stackIdentifier, logicalId, mismatch }) {
|
|
490
|
+
await this.updateTemplateProperty({
|
|
491
|
+
stackIdentifier,
|
|
492
|
+
logicalId,
|
|
493
|
+
propertyPath: mismatch.propertyPath,
|
|
494
|
+
newValue: mismatch.actualValue,
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
success: true,
|
|
499
|
+
mode: 'template',
|
|
500
|
+
propertyPath: mismatch.propertyPath,
|
|
501
|
+
oldValue: mismatch.expectedValue,
|
|
502
|
+
newValue: mismatch.actualValue,
|
|
503
|
+
message: 'Template updated to match actual resource state',
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Reconcile via resource update
|
|
509
|
+
* @private
|
|
510
|
+
*/
|
|
511
|
+
async _reconcileViaResource({ stackIdentifier, logicalId, mismatch }) {
|
|
512
|
+
// This is a simplified implementation
|
|
513
|
+
// In production, would need resource type detection and proper API calls
|
|
514
|
+
|
|
515
|
+
// For now, only support VPC properties
|
|
516
|
+
if (!mismatch.propertyPath.includes('EnableDns')) {
|
|
517
|
+
throw new Error('Resource property update not supported for this property');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Mock resource update (in real implementation, would use EC2 API)
|
|
521
|
+
const client = this._getEC2Client();
|
|
522
|
+
const command = new ModifyVpcAttributeCommand({
|
|
523
|
+
VpcId: 'vpc-placeholder',
|
|
524
|
+
EnableDnsSupport: { Value: mismatch.expectedValue },
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
await client.send(command);
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
success: true,
|
|
531
|
+
mode: 'resource',
|
|
532
|
+
propertyPath: mismatch.propertyPath,
|
|
533
|
+
oldValue: mismatch.actualValue,
|
|
534
|
+
newValue: mismatch.expectedValue,
|
|
535
|
+
message: 'Resource updated to match template definition',
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Update VPC property directly
|
|
541
|
+
* @private
|
|
542
|
+
*/
|
|
543
|
+
async _updateVpcProperty({ physicalId, propertyPath, newValue }) {
|
|
544
|
+
const client = this._getEC2Client();
|
|
545
|
+
|
|
546
|
+
// Map property paths to VPC attribute names
|
|
547
|
+
if (propertyPath === 'Properties.EnableDnsSupport') {
|
|
548
|
+
const command = new ModifyVpcAttributeCommand({
|
|
549
|
+
VpcId: physicalId,
|
|
550
|
+
EnableDnsSupport: { Value: newValue },
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
await client.send(command);
|
|
554
|
+
} else if (propertyPath === 'Properties.EnableDnsHostnames') {
|
|
555
|
+
const command = new ModifyVpcAttributeCommand({
|
|
556
|
+
VpcId: physicalId,
|
|
557
|
+
EnableDnsHostnames: { Value: newValue },
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
await client.send(command);
|
|
561
|
+
} else {
|
|
562
|
+
throw new Error(`Property ${propertyPath} cannot be updated directly`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return {
|
|
566
|
+
success: true,
|
|
567
|
+
message: `VPC property ${propertyPath} updated successfully`,
|
|
568
|
+
updatedAt: new Date(),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Reconcile multiple properties via resource update (resource mode)
|
|
574
|
+
*
|
|
575
|
+
* Domain Service - Hexagonal Architecture
|
|
576
|
+
* Coordinates AWS API calls to update cloud resources directly
|
|
577
|
+
*
|
|
578
|
+
* @private
|
|
579
|
+
*/
|
|
580
|
+
async _reconcileMultiplePropertiesViaResource({
|
|
581
|
+
stackIdentifier,
|
|
582
|
+
logicalId,
|
|
583
|
+
physicalId,
|
|
584
|
+
resourceType,
|
|
585
|
+
mismatches,
|
|
586
|
+
}) {
|
|
587
|
+
// Validate resource type supports resource mode
|
|
588
|
+
if (resourceType !== 'AWS::Lambda::Function') {
|
|
589
|
+
throw new Error(`Resource mode reconciliation not supported for ${resourceType}`);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const results = [];
|
|
593
|
+
let reconciledCount = 0;
|
|
594
|
+
let failedCount = 0;
|
|
595
|
+
let skippedCount = 0;
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
// Separate mutable from immutable properties
|
|
599
|
+
const mutableMismatches = [];
|
|
600
|
+
const mutableIndexMap = new Map(); // Track original index for mutable properties
|
|
601
|
+
|
|
602
|
+
for (let i = 0; i < mismatches.length; i++) {
|
|
603
|
+
const mismatch = mismatches[i];
|
|
604
|
+
if (mismatch.requiresReplacement()) {
|
|
605
|
+
skippedCount++;
|
|
606
|
+
} else {
|
|
607
|
+
mutableIndexMap.set(mismatch, i);
|
|
608
|
+
mutableMismatches.push(mismatch);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// If no mutable properties, return early with all marked as skipped
|
|
613
|
+
if (mutableMismatches.length === 0) {
|
|
614
|
+
const skippedResults = mismatches.map(m => ({
|
|
615
|
+
success: false,
|
|
616
|
+
mode: 'resource',
|
|
617
|
+
propertyPath: m.propertyPath,
|
|
618
|
+
message: `Skipped: Property is immutable and cannot be updated without replacement`,
|
|
619
|
+
}));
|
|
620
|
+
|
|
621
|
+
return {
|
|
622
|
+
reconciledCount: 0,
|
|
623
|
+
failedCount: 0,
|
|
624
|
+
skippedCount,
|
|
625
|
+
results: skippedResults,
|
|
626
|
+
message: `All ${mismatches.length} properties are immutable and cannot be reconciled in resource mode`,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Route to resource-specific updater
|
|
631
|
+
let lambdaResults = [];
|
|
632
|
+
if (resourceType === 'AWS::Lambda::Function') {
|
|
633
|
+
const lambdaResult = await this._updateLambdaFunction({
|
|
634
|
+
physicalId,
|
|
635
|
+
mismatches: mutableMismatches,
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
reconciledCount = lambdaResult.reconciledCount;
|
|
639
|
+
failedCount = lambdaResult.failedCount;
|
|
640
|
+
lambdaResults = lambdaResult.results;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Build results array in original input order
|
|
644
|
+
for (let i = 0; i < mismatches.length; i++) {
|
|
645
|
+
const mismatch = mismatches[i];
|
|
646
|
+
if (mismatch.requiresReplacement()) {
|
|
647
|
+
// Immutable - add skip result
|
|
648
|
+
results.push({
|
|
649
|
+
success: false,
|
|
650
|
+
mode: 'resource',
|
|
651
|
+
propertyPath: mismatch.propertyPath,
|
|
652
|
+
message: `Skipped: Property is immutable and cannot be updated without replacement`,
|
|
653
|
+
});
|
|
654
|
+
} else {
|
|
655
|
+
// Mutable - find corresponding result from Lambda update
|
|
656
|
+
const lambdaResult = lambdaResults.find(r => r.propertyPath === mismatch.propertyPath);
|
|
657
|
+
if (lambdaResult) {
|
|
658
|
+
results.push(lambdaResult);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
} catch (error) {
|
|
663
|
+
// If update fails, mark all as failed
|
|
664
|
+
return {
|
|
665
|
+
reconciledCount: 0,
|
|
666
|
+
failedCount: mismatches.length,
|
|
667
|
+
skippedCount: 0,
|
|
668
|
+
results: mismatches.map(m => ({
|
|
669
|
+
success: false,
|
|
670
|
+
mode: 'resource',
|
|
671
|
+
propertyPath: m.propertyPath,
|
|
672
|
+
message: `Failed to update Lambda: ${error.message}`,
|
|
673
|
+
})),
|
|
674
|
+
message: `Failed to update Lambda: ${error.message}`,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Determine appropriate message based on outcome
|
|
679
|
+
let message;
|
|
680
|
+
if (failedCount > 0 && reconciledCount === 0) {
|
|
681
|
+
message = `Failed to update Lambda: ${results.find(r => !r.success)?.message || 'Unknown error'}`;
|
|
682
|
+
} else if (failedCount > 0) {
|
|
683
|
+
message = `Partially updated Lambda VpcConfig (${reconciledCount} succeeded, ${failedCount} failed)`;
|
|
684
|
+
} else {
|
|
685
|
+
message = `Lambda VpcConfig updated via UpdateFunctionConfiguration (${reconciledCount} properties reconciled)`;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return {
|
|
689
|
+
reconciledCount,
|
|
690
|
+
failedCount,
|
|
691
|
+
skippedCount,
|
|
692
|
+
results,
|
|
693
|
+
message,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Update Lambda function configuration via AWS Lambda API
|
|
699
|
+
*
|
|
700
|
+
* Infrastructure Adapter
|
|
701
|
+
* Translates domain mismatches to AWS Lambda UpdateFunctionConfiguration call
|
|
702
|
+
*
|
|
703
|
+
* @private
|
|
704
|
+
*/
|
|
705
|
+
async _updateLambdaFunction({ physicalId, mismatches }) {
|
|
706
|
+
const client = this._getLambdaClient();
|
|
707
|
+
const results = [];
|
|
708
|
+
let reconciledCount = 0;
|
|
709
|
+
let failedCount = 0;
|
|
710
|
+
|
|
711
|
+
try {
|
|
712
|
+
// Build VpcConfig update from mismatches
|
|
713
|
+
const vpcConfigUpdate = {};
|
|
714
|
+
const vpcConfigMismatches = mismatches.filter(m =>
|
|
715
|
+
m.propertyPath.startsWith('VpcConfig')
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
for (const mismatch of vpcConfigMismatches) {
|
|
719
|
+
// Property path format: "VpcConfig.SubnetIds" or "VpcConfig.SecurityGroupIds"
|
|
720
|
+
const parts = mismatch.propertyPath.split('.');
|
|
721
|
+
if (parts.length === 2 && parts[0] === 'VpcConfig') {
|
|
722
|
+
vpcConfigUpdate[parts[1]] = mismatch.expectedValue;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// If we have VpcConfig updates, call UpdateFunctionConfiguration
|
|
727
|
+
if (Object.keys(vpcConfigUpdate).length > 0) {
|
|
728
|
+
const command = new UpdateFunctionConfigurationCommand({
|
|
729
|
+
FunctionName: physicalId,
|
|
730
|
+
VpcConfig: vpcConfigUpdate,
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
await client.send(command);
|
|
734
|
+
|
|
735
|
+
// Mark all VpcConfig properties as successful
|
|
736
|
+
for (const mismatch of vpcConfigMismatches) {
|
|
737
|
+
results.push({
|
|
738
|
+
success: true,
|
|
739
|
+
mode: 'resource',
|
|
740
|
+
propertyPath: mismatch.propertyPath,
|
|
741
|
+
oldValue: mismatch.actualValue,
|
|
742
|
+
newValue: mismatch.expectedValue,
|
|
743
|
+
message: 'Lambda VpcConfig updated successfully',
|
|
744
|
+
});
|
|
745
|
+
reconciledCount++;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Handle non-VpcConfig properties (currently unsupported)
|
|
750
|
+
const otherMismatches = mismatches.filter(m =>
|
|
751
|
+
!m.propertyPath.startsWith('VpcConfig')
|
|
752
|
+
);
|
|
753
|
+
for (const mismatch of otherMismatches) {
|
|
754
|
+
results.push({
|
|
755
|
+
success: false,
|
|
756
|
+
mode: 'resource',
|
|
757
|
+
propertyPath: mismatch.propertyPath,
|
|
758
|
+
message: `Property ${mismatch.propertyPath} updates not yet supported in resource mode`,
|
|
759
|
+
});
|
|
760
|
+
failedCount++;
|
|
761
|
+
}
|
|
762
|
+
} catch (error) {
|
|
763
|
+
// If Lambda API call fails, mark all as failed
|
|
764
|
+
for (const mismatch of mismatches) {
|
|
765
|
+
results.push({
|
|
766
|
+
success: false,
|
|
767
|
+
mode: 'resource',
|
|
768
|
+
propertyPath: mismatch.propertyPath,
|
|
769
|
+
message: `Lambda update failed: ${error.message}`,
|
|
770
|
+
});
|
|
771
|
+
failedCount++;
|
|
772
|
+
}
|
|
773
|
+
reconciledCount = 0;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
return {
|
|
777
|
+
reconciledCount,
|
|
778
|
+
failedCount,
|
|
779
|
+
results,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
module.exports = AWSPropertyReconciler;
|