@friggframework/devtools 2.0.0-next.47 → 2.0.0-next.48
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/README.md +1290 -0
- package/frigg-cli/__tests__/unit/commands/build.test.js +279 -0
- package/frigg-cli/__tests__/unit/commands/db-setup.test.js +548 -0
- package/frigg-cli/__tests__/unit/commands/deploy.test.js +320 -0
- package/frigg-cli/__tests__/unit/commands/doctor.test.js +309 -0
- package/frigg-cli/__tests__/unit/commands/install.test.js +400 -0
- package/frigg-cli/__tests__/unit/commands/ui.test.js +346 -0
- package/frigg-cli/__tests__/unit/dependencies.test.js +74 -0
- package/frigg-cli/__tests__/unit/utils/database-validator.test.js +366 -0
- package/frigg-cli/__tests__/unit/utils/error-messages.test.js +304 -0
- package/frigg-cli/__tests__/unit/version-detection.test.js +171 -0
- package/frigg-cli/__tests__/utils/mock-factory.js +270 -0
- package/frigg-cli/__tests__/utils/prisma-mock.js +194 -0
- package/frigg-cli/__tests__/utils/test-fixtures.js +463 -0
- package/frigg-cli/__tests__/utils/test-setup.js +287 -0
- package/frigg-cli/build-command/index.js +66 -0
- package/frigg-cli/db-setup-command/index.js +193 -0
- package/frigg-cli/deploy-command/SPEC-DEPLOY-DRY-RUN.md +981 -0
- package/frigg-cli/deploy-command/index.js +302 -0
- package/frigg-cli/doctor-command/index.js +335 -0
- package/frigg-cli/generate-command/__tests__/generate-command.test.js +301 -0
- package/frigg-cli/generate-command/azure-generator.js +43 -0
- package/frigg-cli/generate-command/gcp-generator.js +47 -0
- package/frigg-cli/generate-command/index.js +332 -0
- package/frigg-cli/generate-command/terraform-generator.js +555 -0
- package/frigg-cli/generate-iam-command.js +118 -0
- package/frigg-cli/index.js +173 -0
- package/frigg-cli/index.test.js +158 -0
- package/frigg-cli/init-command/backend-first-handler.js +756 -0
- package/frigg-cli/init-command/index.js +93 -0
- package/frigg-cli/init-command/template-handler.js +143 -0
- package/frigg-cli/install-command/backend-js.js +33 -0
- package/frigg-cli/install-command/commit-changes.js +16 -0
- package/frigg-cli/install-command/environment-variables.js +127 -0
- package/frigg-cli/install-command/environment-variables.test.js +136 -0
- package/frigg-cli/install-command/index.js +54 -0
- package/frigg-cli/install-command/install-package.js +13 -0
- package/frigg-cli/install-command/integration-file.js +30 -0
- package/frigg-cli/install-command/logger.js +12 -0
- package/frigg-cli/install-command/template.js +90 -0
- package/frigg-cli/install-command/validate-package.js +75 -0
- package/frigg-cli/jest.config.js +124 -0
- package/frigg-cli/package.json +63 -0
- package/frigg-cli/repair-command/index.js +564 -0
- package/frigg-cli/start-command/index.js +149 -0
- package/frigg-cli/start-command/start-command.test.js +297 -0
- package/frigg-cli/test/init-command.test.js +180 -0
- package/frigg-cli/test/npm-registry.test.js +319 -0
- package/frigg-cli/ui-command/index.js +154 -0
- package/frigg-cli/utils/app-resolver.js +319 -0
- package/frigg-cli/utils/backend-path.js +25 -0
- package/frigg-cli/utils/database-validator.js +154 -0
- package/frigg-cli/utils/error-messages.js +257 -0
- package/frigg-cli/utils/npm-registry.js +167 -0
- package/frigg-cli/utils/process-manager.js +199 -0
- package/frigg-cli/utils/repo-detection.js +405 -0
- package/infrastructure/create-frigg-infrastructure.js +125 -12
- package/infrastructure/docs/PRE-DEPLOYMENT-HEALTH-CHECK-SPEC.md +1317 -0
- package/infrastructure/domains/shared/resource-discovery.enhanced.test.js +306 -0
- package/infrastructure/domains/shared/resource-discovery.js +31 -2
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +1 -1
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +109 -5
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +310 -4
- package/infrastructure/domains/shared/validation/plugin-validator.js +187 -0
- package/infrastructure/domains/shared/validation/plugin-validator.test.js +323 -0
- package/infrastructure/infrastructure-composer.js +22 -0
- package/layers/prisma/.build-complete +3 -0
- package/package.json +18 -7
- package/management-ui/package-lock.json +0 -16517
|
@@ -0,0 +1,564 @@
|
|
|
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('../../infrastructure/domains/health/domain/value-objects/stack-identifier');
|
|
19
|
+
const RunHealthCheckUseCase = require('../../infrastructure/domains/health/application/use-cases/run-health-check-use-case');
|
|
20
|
+
const RepairViaImportUseCase = require('../../infrastructure/domains/health/application/use-cases/repair-via-import-use-case');
|
|
21
|
+
const ReconcilePropertiesUseCase = require('../../infrastructure/domains/health/application/use-cases/reconcile-properties-use-case');
|
|
22
|
+
const ExecuteResourceImportUseCase = require('../../infrastructure/domains/health/application/use-cases/execute-resource-import-use-case');
|
|
23
|
+
|
|
24
|
+
// Infrastructure Layer - AWS Adapters
|
|
25
|
+
const AWSStackRepository = require('../../infrastructure/domains/health/infrastructure/adapters/aws-stack-repository');
|
|
26
|
+
const AWSResourceDetector = require('../../infrastructure/domains/health/infrastructure/adapters/aws-resource-detector');
|
|
27
|
+
const AWSResourceImporter = require('../../infrastructure/domains/health/infrastructure/adapters/aws-resource-importer');
|
|
28
|
+
const AWSPropertyReconciler = require('../../infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler');
|
|
29
|
+
|
|
30
|
+
// Domain Services
|
|
31
|
+
const MismatchAnalyzer = require('../../infrastructure/domains/health/domain/services/mismatch-analyzer');
|
|
32
|
+
const HealthScoreCalculator = require('../../infrastructure/domains/health/domain/services/health-score-calculator');
|
|
33
|
+
const { TemplateParser } = require('../../infrastructure/domains/health/domain/services/template-parser');
|
|
34
|
+
const { ImportTemplateGenerator } = require('../../infrastructure/domains/health/domain/services/import-template-generator');
|
|
35
|
+
const { ImportProgressMonitor } = require('../../infrastructure/domains/health/domain/services/import-progress-monitor');
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create readline interface for user prompts
|
|
39
|
+
* @returns {readline.Interface}
|
|
40
|
+
*/
|
|
41
|
+
function createReadlineInterface() {
|
|
42
|
+
return readline.createInterface({
|
|
43
|
+
input: process.stdin,
|
|
44
|
+
output: process.stdout,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Prompt user for confirmation
|
|
50
|
+
* @param {string} question - Question to ask
|
|
51
|
+
* @returns {Promise<boolean>} User confirmed
|
|
52
|
+
*/
|
|
53
|
+
function confirm(question) {
|
|
54
|
+
const rl = createReadlineInterface();
|
|
55
|
+
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
rl.question(`${question} (y/N): `, (answer) => {
|
|
58
|
+
rl.close();
|
|
59
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Handle import repair operation using template comparison
|
|
66
|
+
* @param {StackIdentifier} stackIdentifier - Stack identifier
|
|
67
|
+
* @param {Object} report - Health check report
|
|
68
|
+
* @param {Object} options - Command options
|
|
69
|
+
*/
|
|
70
|
+
async function handleImportRepair(stackIdentifier, report, options) {
|
|
71
|
+
const orphanedResources = report.getOrphanedResources();
|
|
72
|
+
|
|
73
|
+
if (orphanedResources.length === 0) {
|
|
74
|
+
console.log('\n✓ No orphaned resources to import');
|
|
75
|
+
return { imported: 0, failed: 0 };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(`\n📦 Found ${orphanedResources.length} orphaned resource(s) to import:`);
|
|
79
|
+
orphanedResources.forEach((resource, idx) => {
|
|
80
|
+
console.log(` ${idx + 1}. ${resource.resourceType} - ${resource.physicalId}`);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Check for build template
|
|
84
|
+
const buildTemplatePath = TemplateParser.getBuildTemplatePath();
|
|
85
|
+
const buildTemplateExists = TemplateParser.buildTemplateExists();
|
|
86
|
+
|
|
87
|
+
if (!buildTemplateExists) {
|
|
88
|
+
console.log('\n⚠️ Build template not found. Generating sequential logical IDs (not recommended).');
|
|
89
|
+
console.log(` Run one of the following to generate build template:`);
|
|
90
|
+
console.log(` • serverless package`);
|
|
91
|
+
console.log(` • frigg build`);
|
|
92
|
+
console.log(` • frigg deploy --stage dev`);
|
|
93
|
+
console.log(` Then run 'frigg repair --import ${stackIdentifier.stackName}' again for correct logical IDs.\n`);
|
|
94
|
+
|
|
95
|
+
// Fallback to sequential IDs (old behavior)
|
|
96
|
+
const resourcesToImport = orphanedResources.map((resource, idx) => ({
|
|
97
|
+
logicalId: `ImportedResource${idx + 1}`,
|
|
98
|
+
physicalId: resource.physicalId,
|
|
99
|
+
resourceType: resource.resourceType,
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
if (!options.yes) {
|
|
103
|
+
const confirmed = await confirm(`\nImport ${orphanedResources.length} orphaned resource(s) with sequential IDs?`);
|
|
104
|
+
if (!confirmed) {
|
|
105
|
+
console.log('Import cancelled by user');
|
|
106
|
+
return { imported: 0, failed: 0, cancelled: true };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const resourceDetector = new AWSResourceDetector({ region: stackIdentifier.region });
|
|
111
|
+
const resourceImporter = new AWSResourceImporter({ region: stackIdentifier.region });
|
|
112
|
+
const repairUseCase = new RepairViaImportUseCase({ resourceDetector, resourceImporter });
|
|
113
|
+
|
|
114
|
+
console.log('\n🔧 Importing resources with sequential IDs...');
|
|
115
|
+
const importResult = await repairUseCase.importMultipleResources({
|
|
116
|
+
stackIdentifier,
|
|
117
|
+
resources: resourcesToImport,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (importResult.success) {
|
|
121
|
+
console.log(`\n✓ Successfully imported ${importResult.importedCount} resource(s)`);
|
|
122
|
+
} else {
|
|
123
|
+
console.log(`\n✗ Import failed: ${importResult.message}`);
|
|
124
|
+
if (importResult.validationErrors && importResult.validationErrors.length > 0) {
|
|
125
|
+
console.log('\nValidation errors:');
|
|
126
|
+
importResult.validationErrors.forEach((error) => {
|
|
127
|
+
console.log(` • ${error.logicalId}: ${error.reason}`);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
imported: importResult.importedCount,
|
|
134
|
+
failed: importResult.failedCount,
|
|
135
|
+
success: importResult.success,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Use template comparison to find correct logical IDs
|
|
140
|
+
console.log(`\n🔍 Analyzing templates to map orphaned resources to correct logical IDs...`);
|
|
141
|
+
console.log(` Build template: ${buildTemplatePath}`);
|
|
142
|
+
console.log(` Deployed template: CloudFormation (via AWS API)`);
|
|
143
|
+
|
|
144
|
+
// Wire up use case with template comparison
|
|
145
|
+
const stackRepository = new AWSStackRepository({ region: stackIdentifier.region });
|
|
146
|
+
const resourceDetector = new AWSResourceDetector({ region: stackIdentifier.region });
|
|
147
|
+
const resourceImporter = new AWSResourceImporter({ region: stackIdentifier.region });
|
|
148
|
+
const repairUseCase = new RepairViaImportUseCase({
|
|
149
|
+
resourceDetector,
|
|
150
|
+
resourceImporter,
|
|
151
|
+
stackRepository,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Execute logical ID mapping
|
|
155
|
+
const mappingResult = await repairUseCase.importWithLogicalIdMapping({
|
|
156
|
+
stackIdentifier,
|
|
157
|
+
orphanedResources,
|
|
158
|
+
buildTemplatePath,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (!mappingResult.success) {
|
|
162
|
+
console.log(`\n✗ Mapping failed: ${mappingResult.message}`);
|
|
163
|
+
return { imported: 0, failed: 0, success: false };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Display mapping results
|
|
167
|
+
console.log(`\n✅ Successfully mapped ${mappingResult.mappedCount} resource(s) to logical IDs:`);
|
|
168
|
+
mappingResult.mappings.forEach((mapping) => {
|
|
169
|
+
console.log(` • ${mapping.logicalId} ← ${mapping.physicalId} (${mapping.matchMethod}, ${mapping.confidence} confidence)`);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (mappingResult.unmappedCount > 0) {
|
|
173
|
+
console.log(`\n⚠️ Could not map ${mappingResult.unmappedCount} resource(s):`);
|
|
174
|
+
mappingResult.unmappedResources.forEach((resource) => {
|
|
175
|
+
console.log(` • ${resource.resourceType} - ${resource.physicalId}`);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Display warnings for multiple resources of same type
|
|
180
|
+
if (mappingResult.warnings && mappingResult.warnings.length > 0) {
|
|
181
|
+
console.log(`\n⚠️ Warnings:`);
|
|
182
|
+
mappingResult.warnings.forEach((warning) => {
|
|
183
|
+
console.log(` • ${warning.message}`);
|
|
184
|
+
if (warning.type === 'MULTIPLE_RESOURCES') {
|
|
185
|
+
warning.resources.forEach((res) => {
|
|
186
|
+
console.log(` - ${res.logicalId} ← ${res.physicalId} (${res.matchMethod}, ${res.confidence})`);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Confirm with user (unless --yes flag)
|
|
193
|
+
if (!options.yes) {
|
|
194
|
+
console.log(`\n📋 The following will be imported into CloudFormation:`);
|
|
195
|
+
mappingResult.resourcesToImport.forEach((resource) => {
|
|
196
|
+
console.log(` • ${resource.LogicalResourceId} (${resource.ResourceType})`);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const confirmed = await confirm(`\nProceed with import of ${mappingResult.mappedCount} resource(s)?`);
|
|
200
|
+
if (!confirmed) {
|
|
201
|
+
console.log('Import cancelled by user');
|
|
202
|
+
return { imported: 0, failed: 0, cancelled: true };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Execute actual CloudFormation import operation
|
|
207
|
+
console.log(`\n🔧 Preparing CloudFormation import operation...`);
|
|
208
|
+
|
|
209
|
+
// Wire up ExecuteResourceImportUseCase
|
|
210
|
+
const templateParser = new TemplateParser();
|
|
211
|
+
const importTemplateGenerator = new ImportTemplateGenerator({
|
|
212
|
+
stackRepository,
|
|
213
|
+
templateParser,
|
|
214
|
+
resourceDetector,
|
|
215
|
+
});
|
|
216
|
+
const importProgressMonitor = new ImportProgressMonitor({
|
|
217
|
+
cloudFormationRepository: stackRepository,
|
|
218
|
+
});
|
|
219
|
+
const executeImportUseCase = new ExecuteResourceImportUseCase({
|
|
220
|
+
importTemplateGenerator,
|
|
221
|
+
importProgressMonitor,
|
|
222
|
+
cloudFormationRepository: stackRepository,
|
|
223
|
+
stackRepository,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Convert mappings to resourcesToImport format
|
|
227
|
+
const resourcesToImport = mappingResult.mappings.map((mapping) => ({
|
|
228
|
+
logicalId: mapping.logicalId,
|
|
229
|
+
physicalId: mapping.physicalId,
|
|
230
|
+
resourceType: mapping.resourceType,
|
|
231
|
+
}));
|
|
232
|
+
|
|
233
|
+
// Execute import with progress reporting
|
|
234
|
+
const importResult = await executeImportUseCase.execute({
|
|
235
|
+
stackIdentifier,
|
|
236
|
+
resourcesToImport,
|
|
237
|
+
buildTemplatePath,
|
|
238
|
+
onProgress: (progress) => {
|
|
239
|
+
if (progress.step === 'generate_template' && progress.status === 'in_progress') {
|
|
240
|
+
console.log(' • Generating import template...');
|
|
241
|
+
} else if (progress.step === 'generate_template' && progress.status === 'complete') {
|
|
242
|
+
console.log(' ✓ Template generated');
|
|
243
|
+
} else if (progress.step === 'create_change_set' && progress.status === 'in_progress') {
|
|
244
|
+
console.log(' • Creating CloudFormation change set...');
|
|
245
|
+
} else if (progress.step === 'create_change_set' && progress.status === 'complete') {
|
|
246
|
+
console.log(` ✓ Change set created: ${progress.changeSetName}`);
|
|
247
|
+
} else if (progress.step === 'wait_change_set' && progress.status === 'in_progress') {
|
|
248
|
+
console.log(' • Waiting for change set...');
|
|
249
|
+
} else if (progress.step === 'wait_change_set' && progress.status === 'complete') {
|
|
250
|
+
console.log(' ✓ Change set ready');
|
|
251
|
+
} else if (progress.step === 'execute_import' && progress.status === 'in_progress') {
|
|
252
|
+
if (progress.resourceProgress) {
|
|
253
|
+
const { logicalId, status, progress: resourceProgress, total } = progress.resourceProgress;
|
|
254
|
+
console.log(` • Importing resource ${resourceProgress}/${total}: ${logicalId} (${status})`);
|
|
255
|
+
} else {
|
|
256
|
+
console.log(' • Executing import operation...');
|
|
257
|
+
}
|
|
258
|
+
} else if (progress.step === 'execute_import' && progress.status === 'complete') {
|
|
259
|
+
console.log(' ✓ Import operation complete');
|
|
260
|
+
} else if (progress.step === 'verify' && progress.status === 'in_progress') {
|
|
261
|
+
console.log(' • Verifying imported resources...');
|
|
262
|
+
} else if (progress.step === 'verify' && progress.status === 'complete') {
|
|
263
|
+
console.log(' ✓ Verification complete');
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
if (importResult.success) {
|
|
269
|
+
console.log(`\n✅ Successfully imported ${importResult.importedCount} resource(s) into CloudFormation!`);
|
|
270
|
+
console.log(` Stack status: ${importResult.stackStatus}`);
|
|
271
|
+
console.log(` Change set: ${importResult.changeSetName}`);
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
imported: importResult.importedCount,
|
|
275
|
+
failed: 0,
|
|
276
|
+
success: true,
|
|
277
|
+
};
|
|
278
|
+
} else {
|
|
279
|
+
console.error(`\n❌ Import operation failed: ${importResult.error}`);
|
|
280
|
+
console.error(` Failed at step: ${importResult.step}`);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
imported: 0,
|
|
284
|
+
failed: mappingResult.mappedCount,
|
|
285
|
+
success: false,
|
|
286
|
+
error: importResult.error,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Handle property reconciliation repair operation
|
|
293
|
+
* @param {StackIdentifier} stackIdentifier - Stack identifier
|
|
294
|
+
* @param {Object} report - Health check report
|
|
295
|
+
* @param {Object} options - Command options
|
|
296
|
+
*/
|
|
297
|
+
async function handleReconcileRepair(stackIdentifier, report, options) {
|
|
298
|
+
const driftedResources = report.getDriftedResources();
|
|
299
|
+
|
|
300
|
+
if (driftedResources.length === 0) {
|
|
301
|
+
console.log('\n✓ No property drift to reconcile');
|
|
302
|
+
return { reconciled: 0, failed: 0 };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Count total property mismatches
|
|
306
|
+
let totalMismatches = 0;
|
|
307
|
+
driftedResources.forEach((resource) => {
|
|
308
|
+
const issues = report.issues.filter(
|
|
309
|
+
(issue) => issue.type === 'PROPERTY_MISMATCH' && issue.resourceId === resource.physicalId
|
|
310
|
+
);
|
|
311
|
+
totalMismatches += issues.length;
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
console.log(`\n🔧 Found ${driftedResources.length} drifted resource(s) with ${totalMismatches} property mismatch(es):`);
|
|
315
|
+
driftedResources.forEach((resource) => {
|
|
316
|
+
const issues = report.issues.filter(
|
|
317
|
+
(issue) => issue.type === 'PROPERTY_MISMATCH' && issue.resourceId === resource.physicalId
|
|
318
|
+
);
|
|
319
|
+
console.log(` • ${resource.logicalId} (${resource.resourceType}): ${issues.length} mismatch(es)`);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Determine mode (template or resource)
|
|
323
|
+
const mode = options.mode || 'template';
|
|
324
|
+
const modeDescription = mode === 'template'
|
|
325
|
+
? 'Update CloudFormation template to match actual resource state'
|
|
326
|
+
: 'Update cloud resources to match CloudFormation template';
|
|
327
|
+
|
|
328
|
+
console.log(`\nReconciliation mode: ${mode}`);
|
|
329
|
+
console.log(` ${modeDescription}`);
|
|
330
|
+
|
|
331
|
+
// Confirm with user (unless --yes flag)
|
|
332
|
+
if (!options.yes) {
|
|
333
|
+
const confirmed = await confirm(`\nReconcile ${totalMismatches} property mismatch(es) in ${mode} mode?`);
|
|
334
|
+
if (!confirmed) {
|
|
335
|
+
console.log('Reconciliation cancelled by user');
|
|
336
|
+
return { reconciled: 0, failed: 0, cancelled: true };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Wire up use case with CloudFormation repository for monitoring
|
|
341
|
+
const stackRepository = new AWSStackRepository({ region: stackIdentifier.region });
|
|
342
|
+
const propertyReconciler = new AWSPropertyReconciler({
|
|
343
|
+
region: stackIdentifier.region,
|
|
344
|
+
cloudFormationRepository: stackRepository
|
|
345
|
+
});
|
|
346
|
+
const reconcileUseCase = new ReconcilePropertiesUseCase({ propertyReconciler });
|
|
347
|
+
|
|
348
|
+
// Execute reconciliation for each drifted resource
|
|
349
|
+
console.log('\n🔧 Reconciling property drift...');
|
|
350
|
+
let reconciledCount = 0;
|
|
351
|
+
let failedCount = 0;
|
|
352
|
+
let skippedImmutableCount = 0;
|
|
353
|
+
const immutableProperties = [];
|
|
354
|
+
|
|
355
|
+
for (const resource of driftedResources) {
|
|
356
|
+
// Get property mismatches for this resource
|
|
357
|
+
const resourceIssues = report.issues.filter(
|
|
358
|
+
(issue) => issue.type === 'PROPERTY_MISMATCH' && issue.resourceId === resource.physicalId
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
if (resourceIssues.length === 0) continue;
|
|
362
|
+
|
|
363
|
+
const mismatches = resourceIssues.map((issue) => issue.propertyMismatch);
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
const result = await reconcileUseCase.reconcileMultipleProperties({
|
|
367
|
+
stackIdentifier,
|
|
368
|
+
logicalId: resource.logicalId,
|
|
369
|
+
physicalId: resource.physicalId,
|
|
370
|
+
resourceType: resource.resourceType,
|
|
371
|
+
mismatches,
|
|
372
|
+
mode,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
reconciledCount += result.reconciledCount;
|
|
376
|
+
failedCount += result.failedCount;
|
|
377
|
+
skippedImmutableCount += result.skippedCount || 0;
|
|
378
|
+
|
|
379
|
+
// Track immutable properties for reporting
|
|
380
|
+
if (result.skippedCount > 0) {
|
|
381
|
+
const skippedMismatches = mismatches.filter(m => m.requiresReplacement());
|
|
382
|
+
skippedMismatches.forEach(m => {
|
|
383
|
+
immutableProperties.push({
|
|
384
|
+
logicalId: resource.logicalId,
|
|
385
|
+
resourceType: resource.resourceType,
|
|
386
|
+
physicalId: resource.physicalId,
|
|
387
|
+
propertyPath: m.propertyPath,
|
|
388
|
+
expectedValue: m.expectedValue,
|
|
389
|
+
actualValue: m.actualValue,
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
console.log(` ✓ ${resource.logicalId}: Reconciled ${result.reconciledCount} property(ies)`);
|
|
395
|
+
if (result.skippedCount > 0) {
|
|
396
|
+
console.log(` ⚠ Skipped ${result.skippedCount} immutable property(ies) - requires manual intervention`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Debug: Log full result if reconciledCount is 0 but we expected properties
|
|
400
|
+
if (process.env.DEBUG_RECONCILE && result.reconciledCount === 0 && mismatches.length > 0) {
|
|
401
|
+
console.log(` [DEBUG] Expected ${mismatches.length} mismatches, got result:`, JSON.stringify(result, null, 2));
|
|
402
|
+
}
|
|
403
|
+
} catch (error) {
|
|
404
|
+
// Count failed properties, not just the resource
|
|
405
|
+
failedCount += mismatches.length;
|
|
406
|
+
console.log(` ✗ ${resource.logicalId}: ${error.message}`);
|
|
407
|
+
|
|
408
|
+
// Debug: Log full error
|
|
409
|
+
if (process.env.DEBUG_RECONCILE) {
|
|
410
|
+
console.log(` [DEBUG] Error stack:`, error.stack);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Report results
|
|
416
|
+
console.log(''); // Blank line before summary
|
|
417
|
+
|
|
418
|
+
if (reconciledCount > 0) {
|
|
419
|
+
console.log(`✅ Reconciled ${reconciledCount} property(ies)`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (skippedImmutableCount > 0) {
|
|
423
|
+
console.log(`\n⚠ ${skippedImmutableCount} immutable property(ies) require manual intervention:`);
|
|
424
|
+
immutableProperties.forEach(prop => {
|
|
425
|
+
console.log(` • ${prop.logicalId}.${prop.propertyPath}`);
|
|
426
|
+
console.log(` Template: ${JSON.stringify(prop.expectedValue)}`);
|
|
427
|
+
console.log(` Actual: ${JSON.stringify(prop.actualValue)}`);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
console.log(`\n💡 To resolve immutable property drift:`);
|
|
431
|
+
console.log(` 1. These properties require resource replacement (cannot be updated in place)`);
|
|
432
|
+
console.log(` 2. Options:`);
|
|
433
|
+
console.log(` a) Accept the drift - update your local template to match actual values`);
|
|
434
|
+
console.log(` b) Replace the resource - delete and recreate via CloudFormation`);
|
|
435
|
+
console.log(` c) Use import workflow - remove from stack, then re-import with correct values`);
|
|
436
|
+
console.log(`\n For automated import workflow (coming soon):`);
|
|
437
|
+
console.log(` frigg repair --import-drift ${stackIdentifier.stackName}`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (failedCount === 0 && skippedImmutableCount === 0) {
|
|
441
|
+
console.log(`✓ Successfully reconciled all ${reconciledCount} property mismatch(es)`);
|
|
442
|
+
} else {
|
|
443
|
+
console.log(`\n⚠ Reconciled ${reconciledCount} property(ies), ${failedCount} failed`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return { reconciled: reconciledCount, failed: failedCount, success: failedCount === 0 };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Execute repair operations
|
|
451
|
+
* @param {string} stackName - CloudFormation stack name
|
|
452
|
+
* @param {Object} options - Command options
|
|
453
|
+
*/
|
|
454
|
+
async function repairCommand(stackName, options = {}) {
|
|
455
|
+
try {
|
|
456
|
+
// Validate required parameter
|
|
457
|
+
if (!stackName) {
|
|
458
|
+
console.error('Error: Stack name is required');
|
|
459
|
+
console.log('Usage: frigg repair [options] <stack-name>');
|
|
460
|
+
console.log('Options:');
|
|
461
|
+
console.log(' --import Import orphaned resources');
|
|
462
|
+
console.log(' --reconcile Reconcile property drift');
|
|
463
|
+
console.log(' --yes Skip confirmation prompts');
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Validate at least one repair operation is selected
|
|
468
|
+
if (!options.import && !options.reconcile) {
|
|
469
|
+
console.error('Error: At least one repair operation must be specified (--import or --reconcile)');
|
|
470
|
+
console.log('Usage: frigg repair [options] <stack-name>');
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Extract options with defaults
|
|
475
|
+
const region = options.region || process.env.AWS_REGION || 'us-east-1';
|
|
476
|
+
const verbose = options.verbose || false;
|
|
477
|
+
|
|
478
|
+
console.log(`\n🏥 Running Frigg Repair on stack: ${stackName} (${region})`);
|
|
479
|
+
|
|
480
|
+
// 1. Create stack identifier
|
|
481
|
+
const stackIdentifier = new StackIdentifier({ stackName, region });
|
|
482
|
+
|
|
483
|
+
// 2. Run health check first to identify issues
|
|
484
|
+
console.log('\n🔍 Running health check to identify issues...');
|
|
485
|
+
|
|
486
|
+
const stackRepository = new AWSStackRepository({ region });
|
|
487
|
+
const resourceDetector = new AWSResourceDetector({ region });
|
|
488
|
+
const mismatchAnalyzer = new MismatchAnalyzer();
|
|
489
|
+
const healthScoreCalculator = new HealthScoreCalculator();
|
|
490
|
+
|
|
491
|
+
const runHealthCheckUseCase = new RunHealthCheckUseCase({
|
|
492
|
+
stackRepository,
|
|
493
|
+
resourceDetector,
|
|
494
|
+
mismatchAnalyzer,
|
|
495
|
+
healthScoreCalculator,
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const report = await runHealthCheckUseCase.execute({ stackIdentifier });
|
|
499
|
+
|
|
500
|
+
console.log(`\nHealth Score: ${report.healthScore.value}/100 (${report.healthScore.qualitativeAssessment()})`);
|
|
501
|
+
console.log(`Issues: ${report.getIssueCount()} total (${report.getCriticalIssueCount()} critical)`);
|
|
502
|
+
|
|
503
|
+
// 3. Execute requested repair operations
|
|
504
|
+
const results = {
|
|
505
|
+
imported: 0,
|
|
506
|
+
reconciled: 0,
|
|
507
|
+
failed: 0,
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
if (options.import) {
|
|
511
|
+
const importResult = await handleImportRepair(stackIdentifier, report, options);
|
|
512
|
+
if (!importResult.cancelled) {
|
|
513
|
+
results.imported = importResult.imported || 0;
|
|
514
|
+
results.failed += importResult.failed || 0;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (options.reconcile) {
|
|
519
|
+
const reconcileResult = await handleReconcileRepair(stackIdentifier, report, options);
|
|
520
|
+
if (!reconcileResult.cancelled) {
|
|
521
|
+
results.reconciled = reconcileResult.reconciled || 0;
|
|
522
|
+
results.failed += reconcileResult.failed || 0;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// 4. Final summary
|
|
527
|
+
console.log('\n' + '═'.repeat(80));
|
|
528
|
+
console.log('Repair Summary:');
|
|
529
|
+
if (options.import) {
|
|
530
|
+
console.log(` Imported: ${results.imported} resource(s)`);
|
|
531
|
+
}
|
|
532
|
+
if (options.reconcile) {
|
|
533
|
+
console.log(` Reconciled: ${results.reconciled} property(ies)`);
|
|
534
|
+
}
|
|
535
|
+
console.log(` Failed: ${results.failed}`);
|
|
536
|
+
console.log('═'.repeat(80));
|
|
537
|
+
|
|
538
|
+
// Run health check again to verify repairs
|
|
539
|
+
console.log('\n🔍 Running health check to verify repairs...');
|
|
540
|
+
const verifyReport = await runHealthCheckUseCase.execute({ stackIdentifier });
|
|
541
|
+
console.log(`\nNew Health Score: ${verifyReport.healthScore.value}/100 (${verifyReport.healthScore.qualitativeAssessment()})`);
|
|
542
|
+
|
|
543
|
+
if (verifyReport.healthScore.value > report.healthScore.value) {
|
|
544
|
+
console.log(`\n✓ Health improved by ${verifyReport.healthScore.value - report.healthScore.value} points!`);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// 5. Exit with appropriate code
|
|
548
|
+
if (results.failed > 0) {
|
|
549
|
+
process.exit(1);
|
|
550
|
+
} else {
|
|
551
|
+
process.exit(0);
|
|
552
|
+
}
|
|
553
|
+
} catch (error) {
|
|
554
|
+
console.error(`\n✗ Repair failed: ${error.message}`);
|
|
555
|
+
|
|
556
|
+
if (options.verbose && error.stack) {
|
|
557
|
+
console.error(`\nStack trace:\n${error.stack}`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
module.exports = { repairCommand };
|