@friggframework/devtools 2.0.0--canary.474.884529c.0 ā 2.0.0--canary.474.988ec0b.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/frigg-cli/deploy-command/index.js +148 -29
- package/frigg-cli/doctor-command/index.js +249 -0
- package/frigg-cli/index.js +24 -1
- package/frigg-cli/repair-command/index.js +341 -0
- package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +146 -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 +229 -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 +180 -0
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
- package/package.json +6 -6
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frigg Repair Command
|
|
3
|
+
*
|
|
4
|
+
* Repairs infrastructure issues detected by frigg doctor:
|
|
5
|
+
* - Import orphaned resources into CloudFormation stack
|
|
6
|
+
* - Reconcile property drift between template and actual resources
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* frigg repair --import <stack-name>
|
|
10
|
+
* frigg repair --reconcile <stack-name>
|
|
11
|
+
* frigg repair --import --reconcile <stack-name> # Fix all issues
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const readline = require('readline');
|
|
16
|
+
|
|
17
|
+
// Domain and Application Layer
|
|
18
|
+
const StackIdentifier = require('@friggframework/devtools/infrastructure/domains/health/domain/value-objects/stack-identifier');
|
|
19
|
+
const RunHealthCheckUseCase = require('@friggframework/devtools/infrastructure/domains/health/application/use-cases/run-health-check-use-case');
|
|
20
|
+
const RepairViaImportUseCase = require('@friggframework/devtools/infrastructure/domains/health/application/use-cases/repair-via-import-use-case');
|
|
21
|
+
const ReconcilePropertiesUseCase = require('@friggframework/devtools/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case');
|
|
22
|
+
|
|
23
|
+
// Infrastructure Layer - AWS Adapters
|
|
24
|
+
const AWSStackRepository = require('@friggframework/devtools/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository');
|
|
25
|
+
const AWSResourceDetector = require('@friggframework/devtools/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector');
|
|
26
|
+
const AWSResourceImporter = require('@friggframework/devtools/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer');
|
|
27
|
+
const AWSPropertyReconciler = require('@friggframework/devtools/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler');
|
|
28
|
+
|
|
29
|
+
// Domain Services
|
|
30
|
+
const MismatchAnalyzer = require('@friggframework/devtools/infrastructure/domains/health/domain/services/mismatch-analyzer');
|
|
31
|
+
const HealthScoreCalculator = require('@friggframework/devtools/infrastructure/domains/health/domain/services/health-score-calculator');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create readline interface for user prompts
|
|
35
|
+
* @returns {readline.Interface}
|
|
36
|
+
*/
|
|
37
|
+
function createReadlineInterface() {
|
|
38
|
+
return readline.createInterface({
|
|
39
|
+
input: process.stdin,
|
|
40
|
+
output: process.stdout,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Prompt user for confirmation
|
|
46
|
+
* @param {string} question - Question to ask
|
|
47
|
+
* @returns {Promise<boolean>} User confirmed
|
|
48
|
+
*/
|
|
49
|
+
function confirm(question) {
|
|
50
|
+
const rl = createReadlineInterface();
|
|
51
|
+
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
rl.question(`${question} (y/N): `, (answer) => {
|
|
54
|
+
rl.close();
|
|
55
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Handle import repair operation
|
|
62
|
+
* @param {StackIdentifier} stackIdentifier - Stack identifier
|
|
63
|
+
* @param {Object} report - Health check report
|
|
64
|
+
* @param {Object} options - Command options
|
|
65
|
+
*/
|
|
66
|
+
async function handleImportRepair(stackIdentifier, report, options) {
|
|
67
|
+
const orphanedResources = report.getOrphanedResources();
|
|
68
|
+
|
|
69
|
+
if (orphanedResources.length === 0) {
|
|
70
|
+
console.log('\nā No orphaned resources to import');
|
|
71
|
+
return { imported: 0, failed: 0 };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(`\nš¦ Found ${orphanedResources.length} orphaned resource(s) to import:`);
|
|
75
|
+
orphanedResources.forEach((resource, idx) => {
|
|
76
|
+
console.log(` ${idx + 1}. ${resource.resourceType} - ${resource.physicalId}`);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Confirm with user (unless --yes flag)
|
|
80
|
+
if (!options.yes) {
|
|
81
|
+
const confirmed = await confirm(`\nImport ${orphanedResources.length} orphaned resource(s)?`);
|
|
82
|
+
if (!confirmed) {
|
|
83
|
+
console.log('Import cancelled by user');
|
|
84
|
+
return { imported: 0, failed: 0, cancelled: true };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Wire up use case
|
|
89
|
+
const resourceDetector = new AWSResourceDetector({ region: stackIdentifier.region });
|
|
90
|
+
const resourceImporter = new AWSResourceImporter({ region: stackIdentifier.region });
|
|
91
|
+
const repairUseCase = new RepairViaImportUseCase({ resourceDetector, resourceImporter });
|
|
92
|
+
|
|
93
|
+
// Prepare resources for import
|
|
94
|
+
const resourcesToImport = orphanedResources.map((resource, idx) => ({
|
|
95
|
+
logicalId: `ImportedResource${idx + 1}`, // Generate logical ID
|
|
96
|
+
physicalId: resource.physicalId,
|
|
97
|
+
resourceType: resource.resourceType,
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
// Execute import
|
|
101
|
+
console.log('\nš§ Importing resources...');
|
|
102
|
+
const importResult = await repairUseCase.importMultipleResources({
|
|
103
|
+
stackIdentifier,
|
|
104
|
+
resources: resourcesToImport,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Report results
|
|
108
|
+
if (importResult.success) {
|
|
109
|
+
console.log(`\nā Successfully imported ${importResult.importedCount} resource(s)`);
|
|
110
|
+
} else {
|
|
111
|
+
console.log(`\nā Import failed: ${importResult.message}`);
|
|
112
|
+
if (importResult.validationErrors && importResult.validationErrors.length > 0) {
|
|
113
|
+
console.log('\nValidation errors:');
|
|
114
|
+
importResult.validationErrors.forEach((error) => {
|
|
115
|
+
console.log(` ⢠${error.logicalId}: ${error.reason}`);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
imported: importResult.importedCount,
|
|
122
|
+
failed: importResult.failedCount,
|
|
123
|
+
success: importResult.success,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Handle property reconciliation repair operation
|
|
129
|
+
* @param {StackIdentifier} stackIdentifier - Stack identifier
|
|
130
|
+
* @param {Object} report - Health check report
|
|
131
|
+
* @param {Object} options - Command options
|
|
132
|
+
*/
|
|
133
|
+
async function handleReconcileRepair(stackIdentifier, report, options) {
|
|
134
|
+
const driftedResources = report.getDriftedResources();
|
|
135
|
+
|
|
136
|
+
if (driftedResources.length === 0) {
|
|
137
|
+
console.log('\nā No property drift to reconcile');
|
|
138
|
+
return { reconciled: 0, failed: 0 };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Count total property mismatches
|
|
142
|
+
let totalMismatches = 0;
|
|
143
|
+
driftedResources.forEach((resource) => {
|
|
144
|
+
const issues = report.issues.filter(
|
|
145
|
+
(issue) => issue.type === 'PROPERTY_MISMATCH' && issue.resourceId === resource.physicalId
|
|
146
|
+
);
|
|
147
|
+
totalMismatches += issues.length;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
console.log(`\nš§ Found ${driftedResources.length} drifted resource(s) with ${totalMismatches} property mismatch(es):`);
|
|
151
|
+
driftedResources.forEach((resource) => {
|
|
152
|
+
const issues = report.issues.filter(
|
|
153
|
+
(issue) => issue.type === 'PROPERTY_MISMATCH' && issue.resourceId === resource.physicalId
|
|
154
|
+
);
|
|
155
|
+
console.log(` ⢠${resource.logicalId} (${resource.resourceType}): ${issues.length} mismatch(es)`);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Determine mode (template or resource)
|
|
159
|
+
const mode = options.mode || 'template';
|
|
160
|
+
const modeDescription = mode === 'template'
|
|
161
|
+
? 'Update CloudFormation template to match actual resource state'
|
|
162
|
+
: 'Update cloud resources to match CloudFormation template';
|
|
163
|
+
|
|
164
|
+
console.log(`\nReconciliation mode: ${mode}`);
|
|
165
|
+
console.log(` ${modeDescription}`);
|
|
166
|
+
|
|
167
|
+
// Confirm with user (unless --yes flag)
|
|
168
|
+
if (!options.yes) {
|
|
169
|
+
const confirmed = await confirm(`\nReconcile ${totalMismatches} property mismatch(es) in ${mode} mode?`);
|
|
170
|
+
if (!confirmed) {
|
|
171
|
+
console.log('Reconciliation cancelled by user');
|
|
172
|
+
return { reconciled: 0, failed: 0, cancelled: true };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Wire up use case
|
|
177
|
+
const propertyReconciler = new AWSPropertyReconciler({ region: stackIdentifier.region });
|
|
178
|
+
const reconcileUseCase = new ReconcilePropertiesUseCase({ propertyReconciler });
|
|
179
|
+
|
|
180
|
+
// Execute reconciliation for each drifted resource
|
|
181
|
+
console.log('\nš§ Reconciling property drift...');
|
|
182
|
+
let reconciledCount = 0;
|
|
183
|
+
let failedCount = 0;
|
|
184
|
+
|
|
185
|
+
for (const resource of driftedResources) {
|
|
186
|
+
// Get property mismatches for this resource
|
|
187
|
+
const resourceIssues = report.issues.filter(
|
|
188
|
+
(issue) => issue.type === 'PROPERTY_MISMATCH' && issue.resourceId === resource.physicalId
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (resourceIssues.length === 0) continue;
|
|
192
|
+
|
|
193
|
+
const mismatches = resourceIssues.map((issue) => issue.propertyMismatch);
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const result = await reconcileUseCase.reconcileMultipleProperties({
|
|
197
|
+
stackIdentifier,
|
|
198
|
+
logicalId: resource.logicalId,
|
|
199
|
+
mismatches,
|
|
200
|
+
mode,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
reconciledCount += result.reconciledCount;
|
|
204
|
+
failedCount += result.failedCount;
|
|
205
|
+
|
|
206
|
+
console.log(` ā ${resource.logicalId}: Reconciled ${result.reconciledCount} property(ies)`);
|
|
207
|
+
if (result.skippedCount > 0) {
|
|
208
|
+
console.log(` (Skipped ${result.skippedCount} immutable property(ies))`);
|
|
209
|
+
}
|
|
210
|
+
} catch (error) {
|
|
211
|
+
failedCount++;
|
|
212
|
+
console.log(` ā ${resource.logicalId}: ${error.message}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Report results
|
|
217
|
+
if (failedCount === 0) {
|
|
218
|
+
console.log(`\nā Successfully reconciled ${reconciledCount} property mismatch(es)`);
|
|
219
|
+
} else {
|
|
220
|
+
console.log(`\nā Reconciled ${reconciledCount} property(ies), ${failedCount} failed`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return { reconciled: reconciledCount, failed: failedCount, success: failedCount === 0 };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Execute repair operations
|
|
228
|
+
* @param {string} stackName - CloudFormation stack name
|
|
229
|
+
* @param {Object} options - Command options
|
|
230
|
+
*/
|
|
231
|
+
async function repairCommand(stackName, options = {}) {
|
|
232
|
+
try {
|
|
233
|
+
// Validate required parameter
|
|
234
|
+
if (!stackName) {
|
|
235
|
+
console.error('Error: Stack name is required');
|
|
236
|
+
console.log('Usage: frigg repair [options] <stack-name>');
|
|
237
|
+
console.log('Options:');
|
|
238
|
+
console.log(' --import Import orphaned resources');
|
|
239
|
+
console.log(' --reconcile Reconcile property drift');
|
|
240
|
+
console.log(' --yes Skip confirmation prompts');
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Validate at least one repair operation is selected
|
|
245
|
+
if (!options.import && !options.reconcile) {
|
|
246
|
+
console.error('Error: At least one repair operation must be specified (--import or --reconcile)');
|
|
247
|
+
console.log('Usage: frigg repair [options] <stack-name>');
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Extract options with defaults
|
|
252
|
+
const region = options.region || process.env.AWS_REGION || 'us-east-1';
|
|
253
|
+
const verbose = options.verbose || false;
|
|
254
|
+
|
|
255
|
+
console.log(`\nš„ Running Frigg Repair on stack: ${stackName} (${region})`);
|
|
256
|
+
|
|
257
|
+
// 1. Create stack identifier
|
|
258
|
+
const stackIdentifier = new StackIdentifier({ stackName, region });
|
|
259
|
+
|
|
260
|
+
// 2. Run health check first to identify issues
|
|
261
|
+
console.log('\nš Running health check to identify issues...');
|
|
262
|
+
|
|
263
|
+
const stackRepository = new AWSStackRepository({ region });
|
|
264
|
+
const resourceDetector = new AWSResourceDetector({ region });
|
|
265
|
+
const mismatchAnalyzer = new MismatchAnalyzer();
|
|
266
|
+
const healthScoreCalculator = new HealthScoreCalculator();
|
|
267
|
+
|
|
268
|
+
const runHealthCheckUseCase = new RunHealthCheckUseCase({
|
|
269
|
+
stackRepository,
|
|
270
|
+
resourceDetector,
|
|
271
|
+
mismatchAnalyzer,
|
|
272
|
+
healthScoreCalculator,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const report = await runHealthCheckUseCase.execute({ stackIdentifier });
|
|
276
|
+
|
|
277
|
+
console.log(`\nHealth Score: ${report.healthScore.value}/100 (${report.healthScore.qualitativeAssessment()})`);
|
|
278
|
+
console.log(`Issues: ${report.getIssueCount()} total (${report.getCriticalIssueCount()} critical)`);
|
|
279
|
+
|
|
280
|
+
// 3. Execute requested repair operations
|
|
281
|
+
const results = {
|
|
282
|
+
imported: 0,
|
|
283
|
+
reconciled: 0,
|
|
284
|
+
failed: 0,
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
if (options.import) {
|
|
288
|
+
const importResult = await handleImportRepair(stackIdentifier, report, options);
|
|
289
|
+
if (!importResult.cancelled) {
|
|
290
|
+
results.imported = importResult.imported || 0;
|
|
291
|
+
results.failed += importResult.failed || 0;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (options.reconcile) {
|
|
296
|
+
const reconcileResult = await handleReconcileRepair(stackIdentifier, report, options);
|
|
297
|
+
if (!reconcileResult.cancelled) {
|
|
298
|
+
results.reconciled = reconcileResult.reconciled || 0;
|
|
299
|
+
results.failed += reconcileResult.failed || 0;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 4. Final summary
|
|
304
|
+
console.log('\n' + 'ā'.repeat(80));
|
|
305
|
+
console.log('Repair Summary:');
|
|
306
|
+
if (options.import) {
|
|
307
|
+
console.log(` Imported: ${results.imported} resource(s)`);
|
|
308
|
+
}
|
|
309
|
+
if (options.reconcile) {
|
|
310
|
+
console.log(` Reconciled: ${results.reconciled} property(ies)`);
|
|
311
|
+
}
|
|
312
|
+
console.log(` Failed: ${results.failed}`);
|
|
313
|
+
console.log('ā'.repeat(80));
|
|
314
|
+
|
|
315
|
+
// Run health check again to verify repairs
|
|
316
|
+
console.log('\nš Running health check to verify repairs...');
|
|
317
|
+
const verifyReport = await runHealthCheckUseCase.execute({ stackIdentifier });
|
|
318
|
+
console.log(`\nNew Health Score: ${verifyReport.healthScore.value}/100 (${verifyReport.healthScore.qualitativeAssessment()})`);
|
|
319
|
+
|
|
320
|
+
if (verifyReport.healthScore.value > report.healthScore.value) {
|
|
321
|
+
console.log(`\nā Health improved by ${verifyReport.healthScore.value - report.healthScore.value} points!`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 5. Exit with appropriate code
|
|
325
|
+
if (results.failed > 0) {
|
|
326
|
+
process.exit(1);
|
|
327
|
+
} else {
|
|
328
|
+
process.exit(0);
|
|
329
|
+
}
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.error(`\nā Repair failed: ${error.message}`);
|
|
332
|
+
|
|
333
|
+
if (options.verbose && error.stack) {
|
|
334
|
+
console.error(`\nStack trace:\n${error.stack}`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
module.exports = { repairCommand };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReconcilePropertiesUseCase - Reconcile Property Drift
|
|
3
|
+
*
|
|
4
|
+
* Application Layer - Use Case
|
|
5
|
+
*
|
|
6
|
+
* Business logic for the "frigg repair --reconcile" command. Orchestrates property
|
|
7
|
+
* drift reconciliation to fix mutable property mismatches between CloudFormation
|
|
8
|
+
* template and actual cloud resources.
|
|
9
|
+
*
|
|
10
|
+
* Responsibilities:
|
|
11
|
+
* - Validate properties can be reconciled
|
|
12
|
+
* - Preview reconciliation impact
|
|
13
|
+
* - Execute reconciliation (template mode or resource mode)
|
|
14
|
+
* - Handle batch reconciliations
|
|
15
|
+
* - Skip immutable properties
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
class ReconcilePropertiesUseCase {
|
|
19
|
+
/**
|
|
20
|
+
* Create use case with required dependencies
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} params
|
|
23
|
+
* @param {IPropertyReconciler} params.propertyReconciler - Property reconciliation operations
|
|
24
|
+
*/
|
|
25
|
+
constructor({ propertyReconciler }) {
|
|
26
|
+
if (!propertyReconciler) {
|
|
27
|
+
throw new Error('propertyReconciler is required');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.propertyReconciler = propertyReconciler;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Reconcile a single property mismatch
|
|
35
|
+
*
|
|
36
|
+
* @param {Object} params
|
|
37
|
+
* @param {StackIdentifier} params.stackIdentifier - Stack identifier
|
|
38
|
+
* @param {string} params.logicalId - Logical resource ID
|
|
39
|
+
* @param {PropertyMismatch} params.mismatch - Property mismatch to reconcile
|
|
40
|
+
* @param {string} [params.mode='template'] - Reconciliation mode
|
|
41
|
+
* @returns {Promise<Object>} Reconciliation result
|
|
42
|
+
*/
|
|
43
|
+
async reconcileSingleProperty({ stackIdentifier, logicalId, mismatch, mode = 'template' }) {
|
|
44
|
+
// 1. Check if property can be reconciled
|
|
45
|
+
const canReconcile = await this.propertyReconciler.canReconcile(mismatch);
|
|
46
|
+
|
|
47
|
+
if (!canReconcile) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Property ${mismatch.propertyPath} cannot be reconciled automatically (immutable property requires replacement)`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. Execute reconciliation
|
|
54
|
+
const result = await this.propertyReconciler.reconcileProperty({
|
|
55
|
+
stackIdentifier,
|
|
56
|
+
logicalId,
|
|
57
|
+
mismatch,
|
|
58
|
+
mode,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reconcile multiple property mismatches for a resource
|
|
66
|
+
*
|
|
67
|
+
* @param {Object} params
|
|
68
|
+
* @param {StackIdentifier} params.stackIdentifier - Stack identifier
|
|
69
|
+
* @param {string} params.logicalId - Logical resource ID
|
|
70
|
+
* @param {PropertyMismatch[]} params.mismatches - Property mismatches to reconcile
|
|
71
|
+
* @param {string} [params.mode='template'] - Reconciliation mode
|
|
72
|
+
* @returns {Promise<Object>} Batch reconciliation result
|
|
73
|
+
*/
|
|
74
|
+
async reconcileMultipleProperties({
|
|
75
|
+
stackIdentifier,
|
|
76
|
+
logicalId,
|
|
77
|
+
mismatches,
|
|
78
|
+
mode = 'template',
|
|
79
|
+
}) {
|
|
80
|
+
// 1. Filter out immutable properties (cannot be reconciled)
|
|
81
|
+
const reconcilableProperties = [];
|
|
82
|
+
const skippedProperties = [];
|
|
83
|
+
|
|
84
|
+
for (const mismatch of mismatches) {
|
|
85
|
+
const canReconcile = await this.propertyReconciler.canReconcile(mismatch);
|
|
86
|
+
|
|
87
|
+
if (canReconcile) {
|
|
88
|
+
reconcilableProperties.push(mismatch);
|
|
89
|
+
} else {
|
|
90
|
+
skippedProperties.push(mismatch);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 2. If no properties can be reconciled, return early
|
|
95
|
+
if (reconcilableProperties.length === 0) {
|
|
96
|
+
return {
|
|
97
|
+
reconciledCount: 0,
|
|
98
|
+
failedCount: 0,
|
|
99
|
+
skippedCount: skippedProperties.length,
|
|
100
|
+
reconcilableCount: 0,
|
|
101
|
+
message: `All ${mismatches.length} property mismatch(es) require resource replacement (immutable)`,
|
|
102
|
+
results: [],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 3. Reconcile the reconcilable properties
|
|
107
|
+
const batchResult = await this.propertyReconciler.reconcileMultipleProperties({
|
|
108
|
+
stackIdentifier,
|
|
109
|
+
logicalId,
|
|
110
|
+
mismatches: reconcilableProperties,
|
|
111
|
+
mode,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// 4. Return combined result
|
|
115
|
+
return {
|
|
116
|
+
reconciledCount: batchResult.reconciledCount,
|
|
117
|
+
failedCount: batchResult.failedCount,
|
|
118
|
+
skippedCount: skippedProperties.length,
|
|
119
|
+
reconcilableCount: reconcilableProperties.length,
|
|
120
|
+
message: batchResult.message,
|
|
121
|
+
results: batchResult.results,
|
|
122
|
+
skippedProperties: skippedProperties.length > 0 ? skippedProperties : undefined,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Preview reconciliation without applying changes
|
|
128
|
+
*
|
|
129
|
+
* @param {Object} params
|
|
130
|
+
* @param {StackIdentifier} params.stackIdentifier - Stack identifier
|
|
131
|
+
* @param {string} params.logicalId - Logical resource ID
|
|
132
|
+
* @param {PropertyMismatch} params.mismatch - Property mismatch to preview
|
|
133
|
+
* @param {string} [params.mode='template'] - Reconciliation mode
|
|
134
|
+
* @returns {Promise<Object>} Preview result
|
|
135
|
+
*/
|
|
136
|
+
async previewReconciliation({ stackIdentifier, logicalId, mismatch, mode = 'template' }) {
|
|
137
|
+
return await this.propertyReconciler.previewReconciliation({
|
|
138
|
+
stackIdentifier,
|
|
139
|
+
logicalId,
|
|
140
|
+
mismatch,
|
|
141
|
+
mode,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = ReconcilePropertiesUseCase;
|