@friggframework/devtools 2.0.0--canary.474.86c5119.0 → 2.0.0--canary.474.6a0bba7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/infrastructure/domains/database/migration-builder.js +199 -1
- package/infrastructure/domains/database/migration-builder.test.js +73 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
- package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +6 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +307 -1
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +38 -5
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +3 -3
- package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
- package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
- package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
- package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
- package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
- package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
- package/infrastructure/domains/health/domain/entities/issue.js +50 -1
- package/infrastructure/domains/health/domain/entities/issue.test.js +111 -0
- package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
- package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
- package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
- package/infrastructure/domains/health/domain/services/health-score-calculator.js +174 -91
- package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +332 -228
- package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
- package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +407 -20
- package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +698 -26
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +108 -14
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +69 -12
- package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +392 -1
- package/package.json +6 -6
|
@@ -14,6 +14,7 @@ const IPropertyReconciler = require('../../application/ports/IPropertyReconciler
|
|
|
14
14
|
// Lazy-loaded AWS SDK clients
|
|
15
15
|
let CloudFormationClient, UpdateStackCommand, GetTemplateCommand;
|
|
16
16
|
let EC2Client, ModifyVpcAttributeCommand;
|
|
17
|
+
let LambdaClient, UpdateFunctionConfigurationCommand;
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Lazy load CloudFormation SDK
|
|
@@ -38,6 +39,17 @@ function loadEC2() {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
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
|
+
|
|
41
53
|
class AWSPropertyReconciler extends IPropertyReconciler {
|
|
42
54
|
/**
|
|
43
55
|
* Resource types that support reconciliation
|
|
@@ -80,6 +92,16 @@ class AWSPropertyReconciler extends IPropertyReconciler {
|
|
|
80
92
|
recommendedMode: 'template',
|
|
81
93
|
limitations: ['Key policy changes must be done via CloudFormation'],
|
|
82
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
|
+
},
|
|
83
105
|
};
|
|
84
106
|
|
|
85
107
|
/**
|
|
@@ -87,12 +109,15 @@ class AWSPropertyReconciler extends IPropertyReconciler {
|
|
|
87
109
|
*
|
|
88
110
|
* @param {Object} [config={}]
|
|
89
111
|
* @param {string} [config.region] - AWS region (defaults to AWS_REGION env var)
|
|
112
|
+
* @param {Object} [config.cloudFormationRepository] - CloudFormation repository for monitoring
|
|
90
113
|
*/
|
|
91
114
|
constructor(config = {}) {
|
|
92
115
|
super();
|
|
93
116
|
this.region = config.region || process.env.AWS_REGION || 'us-east-1';
|
|
94
117
|
this.cfClient = null;
|
|
95
118
|
this.ec2Client = null;
|
|
119
|
+
this.lambdaClient = null;
|
|
120
|
+
this.cfRepo = config.cloudFormationRepository || null;
|
|
96
121
|
}
|
|
97
122
|
|
|
98
123
|
/**
|
|
@@ -119,6 +144,18 @@ class AWSPropertyReconciler extends IPropertyReconciler {
|
|
|
119
144
|
return this.ec2Client;
|
|
120
145
|
}
|
|
121
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
|
+
|
|
122
159
|
/**
|
|
123
160
|
* Check if a property mismatch can be auto-fixed
|
|
124
161
|
*/
|
|
@@ -154,48 +191,186 @@ class AWSPropertyReconciler extends IPropertyReconciler {
|
|
|
154
191
|
|
|
155
192
|
/**
|
|
156
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.
|
|
157
200
|
*/
|
|
158
201
|
async reconcileMultipleProperties({
|
|
159
202
|
stackIdentifier,
|
|
160
203
|
logicalId,
|
|
204
|
+
physicalId,
|
|
205
|
+
resourceType,
|
|
161
206
|
mismatches,
|
|
162
207
|
mode = 'template',
|
|
208
|
+
progressMonitor = null, // Optional UpdateProgressMonitor for async tracking
|
|
163
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)
|
|
164
222
|
const results = [];
|
|
165
223
|
let reconciledCount = 0;
|
|
166
224
|
let failedCount = 0;
|
|
167
225
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const result = await this.reconcileProperty({
|
|
171
|
-
stackIdentifier,
|
|
172
|
-
logicalId,
|
|
173
|
-
mismatch,
|
|
174
|
-
mode,
|
|
175
|
-
});
|
|
226
|
+
try {
|
|
227
|
+
const client = this._getCFClient();
|
|
176
228
|
|
|
177
|
-
|
|
178
|
-
|
|
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
|
+
});
|
|
179
276
|
reconciledCount++;
|
|
180
|
-
}
|
|
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
|
+
});
|
|
181
285
|
failedCount++;
|
|
182
286
|
}
|
|
183
|
-
} catch (error) {
|
|
184
|
-
results.push({
|
|
185
|
-
success: false,
|
|
186
|
-
mode,
|
|
187
|
-
propertyPath: mismatch.propertyPath,
|
|
188
|
-
message: error.message,
|
|
189
|
-
});
|
|
190
|
-
failedCount++;
|
|
191
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
|
+
};
|
|
192
367
|
}
|
|
193
368
|
|
|
194
369
|
return {
|
|
195
370
|
reconciledCount,
|
|
196
371
|
failedCount,
|
|
197
372
|
results,
|
|
198
|
-
message: `Reconciled ${reconciledCount} of ${mismatches.length} properties`,
|
|
373
|
+
message: `Reconciled ${reconciledCount} of ${mismatches.length} properties in single UpdateStack call`,
|
|
199
374
|
};
|
|
200
375
|
}
|
|
201
376
|
|
|
@@ -261,6 +436,7 @@ class AWSPropertyReconciler extends IPropertyReconciler {
|
|
|
261
436
|
const updateCommand = new UpdateStackCommand({
|
|
262
437
|
StackName: stackIdentifier.stackName,
|
|
263
438
|
TemplateBody: JSON.stringify(template),
|
|
439
|
+
Capabilities: ['CAPABILITY_NAMED_IAM'],
|
|
264
440
|
});
|
|
265
441
|
|
|
266
442
|
const updateResponse = await client.send(updateCommand);
|
|
@@ -392,6 +568,217 @@ class AWSPropertyReconciler extends IPropertyReconciler {
|
|
|
392
568
|
updatedAt: new Date(),
|
|
393
569
|
};
|
|
394
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
|
+
}
|
|
395
782
|
}
|
|
396
783
|
|
|
397
784
|
module.exports = AWSPropertyReconciler;
|