@friggframework/devtools 2.0.0--canary.474.898a56c.0 → 2.0.0--canary.474.a794ea3.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__/execute-resource-import-use-case.test.js +679 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +397 -29
- 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 +6 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +162 -9
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +19 -1
- 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__/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 +55 -28
- package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +419 -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 +21 -6
- package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
- package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -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-stack-repository.js +392 -1
- package/package.json +6 -6
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImportProgressMonitor - Monitor CloudFormation Import Operation Progress
|
|
3
|
+
*
|
|
4
|
+
* Domain Layer - Service
|
|
5
|
+
*
|
|
6
|
+
* Monitors CloudFormation import operations by polling stack events and tracking
|
|
7
|
+
* resource import progress. Provides real-time progress callbacks and detects
|
|
8
|
+
* failures, rollbacks, and timeouts.
|
|
9
|
+
*
|
|
10
|
+
* Responsibilities:
|
|
11
|
+
* - Poll CloudFormation stack events during import
|
|
12
|
+
* - Track progress per resource (IN_PROGRESS, COMPLETE, FAILED)
|
|
13
|
+
* - Detect stack rollback states
|
|
14
|
+
* - Timeout after 5 minutes
|
|
15
|
+
* - Provide progress callbacks for UI updates
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
class ImportProgressMonitor {
|
|
19
|
+
/**
|
|
20
|
+
* Create progress monitor with CloudFormation repository dependency
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} params
|
|
23
|
+
* @param {Object} params.cloudFormationRepository - CloudFormation operations
|
|
24
|
+
*/
|
|
25
|
+
constructor({ cloudFormationRepository }) {
|
|
26
|
+
if (!cloudFormationRepository) {
|
|
27
|
+
throw new Error('cloudFormationRepository is required');
|
|
28
|
+
}
|
|
29
|
+
this.cfRepo = cloudFormationRepository;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Monitor import operation progress
|
|
34
|
+
*
|
|
35
|
+
* Polls CloudFormation stack events every 2 seconds to track resource import progress.
|
|
36
|
+
* Calls onProgress callback with status updates for each resource.
|
|
37
|
+
* Detects failures, rollbacks, and timeouts.
|
|
38
|
+
*
|
|
39
|
+
* @param {Object} params
|
|
40
|
+
* @param {Object} params.stackIdentifier - Stack identifier { stackName, region }
|
|
41
|
+
* @param {Array<string>} params.resourceLogicalIds - Logical IDs to track
|
|
42
|
+
* @param {Function} params.onProgress - Progress callback function
|
|
43
|
+
* @returns {Promise<Object>} Import result
|
|
44
|
+
*/
|
|
45
|
+
async monitorImport({ stackIdentifier, resourceLogicalIds, onProgress }) {
|
|
46
|
+
const importedResources = new Set();
|
|
47
|
+
const failedResources = [];
|
|
48
|
+
const processedEvents = new Set(); // Track processed events by timestamp + logicalId
|
|
49
|
+
let elapsedTime = 0; // Track elapsed time manually for fake timers compatibility
|
|
50
|
+
const TIMEOUT_MS = 300000; // 5 minutes
|
|
51
|
+
const POLL_INTERVAL_MS = 2000; // 2 seconds
|
|
52
|
+
|
|
53
|
+
// Continue polling until all resources are complete or failed
|
|
54
|
+
while (
|
|
55
|
+
importedResources.size + failedResources.length <
|
|
56
|
+
resourceLogicalIds.length
|
|
57
|
+
) {
|
|
58
|
+
// Wait 2 seconds before polling
|
|
59
|
+
await this._delay(POLL_INTERVAL_MS);
|
|
60
|
+
elapsedTime += POLL_INTERVAL_MS;
|
|
61
|
+
|
|
62
|
+
// Check for timeout
|
|
63
|
+
if (elapsedTime > TIMEOUT_MS) {
|
|
64
|
+
throw new Error('Import operation timed out');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Get stack events
|
|
68
|
+
const events = await this.cfRepo.getStackEvents({
|
|
69
|
+
stackIdentifier,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Sort events by timestamp (oldest first) for consistent processing
|
|
73
|
+
const sortedEvents = [...events].sort(
|
|
74
|
+
(a, b) => new Date(a.Timestamp) - new Date(b.Timestamp)
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Track if we processed any new events in this iteration
|
|
78
|
+
let processedNewEvents = false;
|
|
79
|
+
|
|
80
|
+
// Process events for tracked resources
|
|
81
|
+
for (const event of sortedEvents) {
|
|
82
|
+
const logicalId = event.LogicalResourceId;
|
|
83
|
+
|
|
84
|
+
// Skip if not a tracked resource
|
|
85
|
+
if (!resourceLogicalIds.includes(logicalId)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Create unique event key to avoid duplicate processing
|
|
90
|
+
const eventKey = `${event.Timestamp.toISOString()}_${logicalId}_${event.ResourceStatus}`;
|
|
91
|
+
|
|
92
|
+
// Skip if already processed
|
|
93
|
+
if (processedEvents.has(eventKey)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
processedEvents.add(eventKey);
|
|
98
|
+
processedNewEvents = true;
|
|
99
|
+
|
|
100
|
+
// Handle different resource statuses
|
|
101
|
+
if (event.ResourceStatus === 'IMPORT_IN_PROGRESS') {
|
|
102
|
+
// Call progress callback with IN_PROGRESS status
|
|
103
|
+
if (onProgress) {
|
|
104
|
+
onProgress({
|
|
105
|
+
logicalId,
|
|
106
|
+
status: 'IN_PROGRESS',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
} else if (event.ResourceStatus === 'IMPORT_COMPLETE' || event.ResourceStatus === 'UPDATE_COMPLETE') {
|
|
110
|
+
// Mark resource as imported
|
|
111
|
+
// Note: CloudFormation sends IMPORT_COMPLETE then UPDATE_COMPLETE (for tagging)
|
|
112
|
+
// We count either as successfully imported
|
|
113
|
+
importedResources.add(logicalId);
|
|
114
|
+
|
|
115
|
+
// Call progress callback with COMPLETE status
|
|
116
|
+
if (onProgress) {
|
|
117
|
+
onProgress({
|
|
118
|
+
logicalId,
|
|
119
|
+
status: 'COMPLETE',
|
|
120
|
+
progress: importedResources.size,
|
|
121
|
+
total: resourceLogicalIds.length,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
} else if (event.ResourceStatus === 'IMPORT_FAILED') {
|
|
125
|
+
// Add to failed resources
|
|
126
|
+
const reason = event.ResourceStatusReason || 'Unknown error';
|
|
127
|
+
failedResources.push({
|
|
128
|
+
logicalId,
|
|
129
|
+
reason,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Call progress callback with FAILED status
|
|
133
|
+
if (onProgress) {
|
|
134
|
+
onProgress({
|
|
135
|
+
logicalId,
|
|
136
|
+
status: 'FAILED',
|
|
137
|
+
reason,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check if all resources are now accounted for
|
|
144
|
+
const allResourcesProcessed =
|
|
145
|
+
importedResources.size + failedResources.length >=
|
|
146
|
+
resourceLogicalIds.length;
|
|
147
|
+
|
|
148
|
+
// If all resources processed, exit loop to return result
|
|
149
|
+
if (allResourcesProcessed) {
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check stack status AFTER processing events - if rollback in progress, throw
|
|
154
|
+
const stackStatus = await this.cfRepo.getStackStatus(stackIdentifier);
|
|
155
|
+
if (stackStatus.includes('ROLLBACK') && stackStatus !== 'IMPORT_ROLLBACK_COMPLETE') {
|
|
156
|
+
throw new Error('Import operation failed and rolled back');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check final stack status before returning
|
|
161
|
+
const finalStackStatus = await this.cfRepo.getStackStatus(stackIdentifier);
|
|
162
|
+
|
|
163
|
+
// If stack rolled back completely and monitoring just finished,
|
|
164
|
+
// check if we should throw or return
|
|
165
|
+
if (finalStackStatus.includes('ROLLBACK')) {
|
|
166
|
+
// If we have any imported resources, return result (partial success)
|
|
167
|
+
// Otherwise, throw error (complete failure)
|
|
168
|
+
if (importedResources.size === 0 && failedResources.length > 0) {
|
|
169
|
+
throw new Error('Import operation failed and rolled back');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Return result
|
|
174
|
+
const success = failedResources.length === 0;
|
|
175
|
+
return {
|
|
176
|
+
success,
|
|
177
|
+
importedCount: importedResources.size,
|
|
178
|
+
failedCount: failedResources.length,
|
|
179
|
+
failedResources,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Delay helper for polling intervals
|
|
185
|
+
*
|
|
186
|
+
* @param {number} ms - Milliseconds to delay
|
|
187
|
+
* @returns {Promise<void>}
|
|
188
|
+
* @private
|
|
189
|
+
*/
|
|
190
|
+
async _delay(ms) {
|
|
191
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = { ImportProgressMonitor };
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImportTemplateGenerator - Generate CloudFormation import templates
|
|
3
|
+
*
|
|
4
|
+
* Purpose: Generate CloudFormation templates for importing existing resources
|
|
5
|
+
* by resolving intrinsic functions (!Ref, !Sub, !GetAtt) with actual AWS values
|
|
6
|
+
* and merging with current stack template.
|
|
7
|
+
*
|
|
8
|
+
* Domain Layer - Service
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
class ImportTemplateGenerator {
|
|
12
|
+
/**
|
|
13
|
+
* Create ImportTemplateGenerator instance
|
|
14
|
+
* @param {object} dependencies - Service dependencies
|
|
15
|
+
* @param {object} dependencies.templateParser - Template parsing service
|
|
16
|
+
* @param {object} dependencies.resourceDetector - AWS resource detection service
|
|
17
|
+
* @param {object} dependencies.stackRepository - CloudFormation stack repository
|
|
18
|
+
*/
|
|
19
|
+
constructor({ templateParser, resourceDetector, stackRepository }) {
|
|
20
|
+
this.templateParser = templateParser;
|
|
21
|
+
this.resourceDetector = resourceDetector;
|
|
22
|
+
this.stackRepository = stackRepository;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generate import template by merging build template with AWS state
|
|
27
|
+
*
|
|
28
|
+
* Process:
|
|
29
|
+
* 1. Parse build template to get resource definitions with Refs
|
|
30
|
+
* 2. Get current stack template (if exists)
|
|
31
|
+
* 3. For each resource to import:
|
|
32
|
+
* - Get AWS resource properties via resourceDetector
|
|
33
|
+
* - Generate resource definition with resolved intrinsics
|
|
34
|
+
* - Create resource identifier for import operation
|
|
35
|
+
* 4. Merge with current template (preserve existing resources)
|
|
36
|
+
*
|
|
37
|
+
* @param {object} params - Generation parameters
|
|
38
|
+
* @param {Array} params.resourcesToImport - Resources to import
|
|
39
|
+
* @param {string} params.buildTemplatePath - Path to build template
|
|
40
|
+
* @param {object} params.stackIdentifier - Target stack identifier
|
|
41
|
+
* @returns {Promise<object>} Import template and resource identifiers
|
|
42
|
+
*/
|
|
43
|
+
async generateImportTemplate({
|
|
44
|
+
resourcesToImport,
|
|
45
|
+
buildTemplatePath,
|
|
46
|
+
stackIdentifier,
|
|
47
|
+
}) {
|
|
48
|
+
// 1. Parse build template
|
|
49
|
+
const buildTemplate = this.templateParser.parseTemplate(buildTemplatePath);
|
|
50
|
+
|
|
51
|
+
// 2. Get current stack template (if exists)
|
|
52
|
+
let currentTemplate;
|
|
53
|
+
try {
|
|
54
|
+
currentTemplate = await this.stackRepository.getTemplate(stackIdentifier);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
// Stack might not exist yet
|
|
57
|
+
currentTemplate = { Resources: {} };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 3. For each resource to import, get AWS properties
|
|
61
|
+
const resourceDefinitions = {};
|
|
62
|
+
const resourceIdentifiers = [];
|
|
63
|
+
|
|
64
|
+
for (const resource of resourcesToImport) {
|
|
65
|
+
const { logicalId, physicalId, resourceType } = resource;
|
|
66
|
+
|
|
67
|
+
// Get current resource state from AWS
|
|
68
|
+
const awsResourceDetails =
|
|
69
|
+
await this.resourceDetector.getResourceDetails({
|
|
70
|
+
resourceType,
|
|
71
|
+
physicalId,
|
|
72
|
+
region: stackIdentifier.region,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Generate CloudFormation resource definition
|
|
76
|
+
const resourceDef = this._generateResourceDefinition({
|
|
77
|
+
logicalId,
|
|
78
|
+
resourceType,
|
|
79
|
+
physicalId,
|
|
80
|
+
buildTemplate,
|
|
81
|
+
awsResourceDetails,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
resourceDefinitions[logicalId] = resourceDef;
|
|
85
|
+
|
|
86
|
+
// Generate resource identifier for import
|
|
87
|
+
resourceIdentifiers.push({
|
|
88
|
+
ResourceType: resourceType,
|
|
89
|
+
LogicalResourceId: logicalId,
|
|
90
|
+
ResourceIdentifier: this._getResourceIdentifier(
|
|
91
|
+
resourceType,
|
|
92
|
+
physicalId
|
|
93
|
+
),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 4. Merge with current template (keep existing resources)
|
|
98
|
+
const importTemplate = {
|
|
99
|
+
...currentTemplate,
|
|
100
|
+
Resources: {
|
|
101
|
+
...currentTemplate.Resources,
|
|
102
|
+
...resourceDefinitions,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// DEBUG: Write template to file for inspection
|
|
107
|
+
if (process.env.DEBUG_IMPORT_TEMPLATE === 'true') {
|
|
108
|
+
const fs = require('fs');
|
|
109
|
+
const path = require('path');
|
|
110
|
+
const debugDir = path.join(__dirname, '../../debug');
|
|
111
|
+
|
|
112
|
+
// Ensure debug directory exists
|
|
113
|
+
if (!fs.existsSync(debugDir)) {
|
|
114
|
+
fs.mkdirSync(debugDir, { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
118
|
+
const debugFile = path.join(debugDir, `import-template-${timestamp}.json`);
|
|
119
|
+
|
|
120
|
+
const debugData = {
|
|
121
|
+
stackIdentifier,
|
|
122
|
+
resourcesToImport,
|
|
123
|
+
resourceIdentifiers,
|
|
124
|
+
template: importTemplate,
|
|
125
|
+
templateSize: JSON.stringify(importTemplate).length,
|
|
126
|
+
resourceCount: Object.keys(importTemplate.Resources || {}).length,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
fs.writeFileSync(debugFile, JSON.stringify(debugData, null, 2));
|
|
130
|
+
console.log(`[DEBUG] Template written to: ${debugFile}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
template: importTemplate,
|
|
135
|
+
resourceIdentifiers,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generate CloudFormation resource definition from AWS state
|
|
141
|
+
*
|
|
142
|
+
* Takes build template definition and resolves all intrinsics
|
|
143
|
+
* with actual AWS values to create import-ready definition.
|
|
144
|
+
*
|
|
145
|
+
* @private
|
|
146
|
+
* @param {object} params - Generation parameters
|
|
147
|
+
* @param {string} params.logicalId - CloudFormation logical ID
|
|
148
|
+
* @param {string} params.resourceType - AWS resource type
|
|
149
|
+
* @param {string} params.physicalId - AWS physical resource ID
|
|
150
|
+
* @param {object} params.buildTemplate - Build template with Refs
|
|
151
|
+
* @param {object} params.awsResourceDetails - Current AWS resource state
|
|
152
|
+
* @returns {object} CloudFormation resource definition
|
|
153
|
+
* @throws {Error} If logical ID not found in build template
|
|
154
|
+
*/
|
|
155
|
+
_generateResourceDefinition({
|
|
156
|
+
logicalId,
|
|
157
|
+
resourceType,
|
|
158
|
+
physicalId,
|
|
159
|
+
buildTemplate,
|
|
160
|
+
awsResourceDetails,
|
|
161
|
+
}) {
|
|
162
|
+
// Start with build template definition if available
|
|
163
|
+
const buildResource = buildTemplate.resources?.[logicalId];
|
|
164
|
+
|
|
165
|
+
if (!buildResource) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Logical ID ${logicalId} not found in build template. ` +
|
|
168
|
+
`Cannot generate import definition without template reference.`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Resolve intrinsics with actual AWS values
|
|
173
|
+
const resolvedProperties = this._resolveIntrinsics({
|
|
174
|
+
properties: buildResource.Properties,
|
|
175
|
+
awsResourceDetails,
|
|
176
|
+
resourceType,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
Type: resourceType,
|
|
181
|
+
Properties: resolvedProperties,
|
|
182
|
+
DeletionPolicy: 'Retain', // Required for CloudFormation IMPORT operations
|
|
183
|
+
UpdateReplacePolicy: 'Retain', // Protects old resources during stack updates
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Resolve CloudFormation intrinsics in properties
|
|
189
|
+
*
|
|
190
|
+
* Processes all properties and resolves intrinsic functions
|
|
191
|
+
* (!Ref, !Sub, !GetAtt) with actual AWS values.
|
|
192
|
+
*
|
|
193
|
+
* @private
|
|
194
|
+
* @param {object} params - Resolution parameters
|
|
195
|
+
* @param {object} params.properties - CloudFormation properties
|
|
196
|
+
* @param {object} params.awsResourceDetails - AWS resource state
|
|
197
|
+
* @param {string} params.resourceType - AWS resource type
|
|
198
|
+
* @returns {object} Resolved properties
|
|
199
|
+
*/
|
|
200
|
+
_resolveIntrinsics({ properties, awsResourceDetails, resourceType }) {
|
|
201
|
+
const resolved = {};
|
|
202
|
+
|
|
203
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
204
|
+
resolved[key] = this._resolveValue(
|
|
205
|
+
value,
|
|
206
|
+
awsResourceDetails,
|
|
207
|
+
resourceType
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return resolved;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Recursively resolve a property value
|
|
216
|
+
*
|
|
217
|
+
* Handles:
|
|
218
|
+
* - Intrinsic functions (!Ref, !Sub, !GetAtt)
|
|
219
|
+
* - Nested objects
|
|
220
|
+
* - Arrays
|
|
221
|
+
* - Literal values
|
|
222
|
+
*
|
|
223
|
+
* @private
|
|
224
|
+
* @param {*} value - Property value to resolve
|
|
225
|
+
* @param {object} awsResourceDetails - AWS resource state
|
|
226
|
+
* @param {string} resourceType - AWS resource type
|
|
227
|
+
* @returns {*} Resolved value
|
|
228
|
+
*/
|
|
229
|
+
_resolveValue(value, awsResourceDetails, resourceType) {
|
|
230
|
+
// Handle intrinsic functions
|
|
231
|
+
if (typeof value === 'object' && value !== null) {
|
|
232
|
+
// !Ref
|
|
233
|
+
if (value.Ref) {
|
|
234
|
+
return this._resolveRef(value.Ref, awsResourceDetails, resourceType);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// !Sub
|
|
238
|
+
if (value['Fn::Sub']) {
|
|
239
|
+
return this._resolveSub(value['Fn::Sub'], awsResourceDetails);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// !GetAtt
|
|
243
|
+
if (value['Fn::GetAtt']) {
|
|
244
|
+
return this._resolveGetAtt(value['Fn::GetAtt'], awsResourceDetails);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Recursively process nested objects
|
|
248
|
+
if (Array.isArray(value)) {
|
|
249
|
+
return value.map((v) =>
|
|
250
|
+
this._resolveValue(v, awsResourceDetails, resourceType)
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Recursively process object properties
|
|
255
|
+
const resolved = {};
|
|
256
|
+
for (const [k, v] of Object.entries(value)) {
|
|
257
|
+
resolved[k] = this._resolveValue(v, awsResourceDetails, resourceType);
|
|
258
|
+
}
|
|
259
|
+
return resolved;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Literal value
|
|
263
|
+
return value;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Resolve !Ref to actual AWS value
|
|
268
|
+
*
|
|
269
|
+
* Maps common parameter names and resource references to AWS property values.
|
|
270
|
+
* Supports:
|
|
271
|
+
* - Parameter names (VpcCidr, VpcId, etc.)
|
|
272
|
+
* - Resource logical IDs (FriggVPC, FriggLambdaSecurityGroup, etc.)
|
|
273
|
+
* - Direct property references (Subnet1, Subnet2, etc.)
|
|
274
|
+
*
|
|
275
|
+
* @private
|
|
276
|
+
* @param {string} refName - Reference name
|
|
277
|
+
* @param {object} awsResourceDetails - AWS resource state
|
|
278
|
+
* @param {string} resourceType - AWS resource type
|
|
279
|
+
* @returns {*} Resolved value
|
|
280
|
+
*/
|
|
281
|
+
_resolveRef(refName, awsResourceDetails, resourceType) {
|
|
282
|
+
// Map common parameters to AWS properties
|
|
283
|
+
const refMap = {
|
|
284
|
+
VpcCidr: awsResourceDetails.properties?.CidrBlock,
|
|
285
|
+
VpcId: awsResourceDetails.properties?.VpcId,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
if (refMap[refName] !== undefined) {
|
|
289
|
+
return refMap[refName];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// First, try direct property lookup
|
|
293
|
+
// This handles cases like { Ref: 'Subnet1' } where properties.Subnet1 = 'subnet-111'
|
|
294
|
+
const directValue = awsResourceDetails.properties?.[refName];
|
|
295
|
+
if (directValue !== undefined) {
|
|
296
|
+
return directValue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Check if refName is a resource logical ID that references another resource
|
|
300
|
+
// In this case, the AWS properties will already have the resolved value
|
|
301
|
+
// For example: { Ref: 'FriggVPC' } should resolve to VpcId from properties
|
|
302
|
+
// For example: { Ref: 'FriggLambdaSecurityGroup' } should resolve to GroupId from properties
|
|
303
|
+
if (refName.includes('VPC') && !refName.includes('Endpoint')) {
|
|
304
|
+
// VPC resource reference
|
|
305
|
+
return awsResourceDetails.properties?.VpcId || refName;
|
|
306
|
+
} else if (refName.includes('Subnet')) {
|
|
307
|
+
// Subnet resource reference
|
|
308
|
+
return awsResourceDetails.properties?.SubnetId || refName;
|
|
309
|
+
} else if (refName.includes('SecurityGroup')) {
|
|
310
|
+
// Security Group resource reference
|
|
311
|
+
return awsResourceDetails.properties?.GroupId || refName;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Fallback: return the ref name itself if not found
|
|
315
|
+
return refName;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Resolve !Sub to actual string
|
|
320
|
+
*
|
|
321
|
+
* Replaces CloudFormation pseudo parameters and variables
|
|
322
|
+
* with actual values from AWS resource state.
|
|
323
|
+
*
|
|
324
|
+
* Supports:
|
|
325
|
+
* - ${AWS::StackName}
|
|
326
|
+
* - Custom variables from tags
|
|
327
|
+
*
|
|
328
|
+
* @private
|
|
329
|
+
* @param {string|object} subValue - Sub expression
|
|
330
|
+
* @param {object} awsResourceDetails - AWS resource state
|
|
331
|
+
* @returns {string|object} Resolved value
|
|
332
|
+
*/
|
|
333
|
+
_resolveSub(subValue, awsResourceDetails) {
|
|
334
|
+
if (typeof subValue !== 'string') {
|
|
335
|
+
return subValue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Replace ${AWS::StackName} with actual stack name
|
|
339
|
+
let resolved = subValue.replace(
|
|
340
|
+
/\$\{AWS::StackName\}/g,
|
|
341
|
+
awsResourceDetails.stackName || ''
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
// Replace other variables if present in tags
|
|
345
|
+
if (awsResourceDetails.tags) {
|
|
346
|
+
for (const [key, value] of Object.entries(awsResourceDetails.tags)) {
|
|
347
|
+
resolved = resolved.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), value);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return resolved;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Resolve !GetAtt to actual attribute value
|
|
356
|
+
*
|
|
357
|
+
* Gets attribute value from AWS resource properties.
|
|
358
|
+
* When the attribute refers to a different resource's property
|
|
359
|
+
* (e.g., FriggVPC.CidrBlock from a Subnet), we need to look at
|
|
360
|
+
* the actual AWS values in the current resource's properties.
|
|
361
|
+
*
|
|
362
|
+
* The AWS resource details from the detector already contain
|
|
363
|
+
* resolved values. For example, a Subnet may have Tags that
|
|
364
|
+
* include the VPC's CIDR, or properties that include the VPC ID.
|
|
365
|
+
*
|
|
366
|
+
* @private
|
|
367
|
+
* @param {Array} getAttValue - GetAtt expression [ResourceName, AttributeName]
|
|
368
|
+
* @param {object} awsResourceDetails - AWS resource state
|
|
369
|
+
* @returns {*} Attribute value or null
|
|
370
|
+
*/
|
|
371
|
+
_resolveGetAtt([resourceName, attributeName], awsResourceDetails) {
|
|
372
|
+
// First try to get directly from properties
|
|
373
|
+
const directValue = awsResourceDetails.properties?.[attributeName];
|
|
374
|
+
if (directValue !== undefined && directValue !== null) {
|
|
375
|
+
// Check if this is the actual value we want
|
|
376
|
+
// For cross-resource references, we need to be more careful
|
|
377
|
+
// If resourceName refers to a different resource (e.g., FriggVPC from a Subnet),
|
|
378
|
+
// the property might not be the right one
|
|
379
|
+
|
|
380
|
+
// If getting VPC.CidrBlock, but we have Subnet.CidrBlock,
|
|
381
|
+
// we need to look elsewhere
|
|
382
|
+
if (resourceName.includes('VPC') && attributeName === 'CidrBlock') {
|
|
383
|
+
// Check if this is actually a subnet's CIDR (starts with same first two octets + .X.0/24 pattern)
|
|
384
|
+
// If so, we need to look at Tags for VpcCidr
|
|
385
|
+
const tags = awsResourceDetails.properties?.Tags;
|
|
386
|
+
if (Array.isArray(tags)) {
|
|
387
|
+
const vpcCidrTag = tags.find(t => t.Key === 'VpcCidr');
|
|
388
|
+
if (vpcCidrTag) {
|
|
389
|
+
return vpcCidrTag.Value;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return directValue;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Check Tags as fallback
|
|
398
|
+
const tags = awsResourceDetails.properties?.Tags;
|
|
399
|
+
if (Array.isArray(tags)) {
|
|
400
|
+
const tag = tags.find(t => t.Key === attributeName || t.Key === `${resourceName}${attributeName}`);
|
|
401
|
+
if (tag) {
|
|
402
|
+
return tag.Value;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Get resource identifier for import operation
|
|
411
|
+
*
|
|
412
|
+
* Maps resource type to CloudFormation import identifier format.
|
|
413
|
+
* Each AWS resource type has a specific identifier property.
|
|
414
|
+
*
|
|
415
|
+
* @private
|
|
416
|
+
* @param {string} resourceType - AWS resource type
|
|
417
|
+
* @param {string} physicalId - AWS physical resource ID
|
|
418
|
+
* @returns {object} Resource identifier for import
|
|
419
|
+
*/
|
|
420
|
+
_getResourceIdentifier(resourceType, physicalId) {
|
|
421
|
+
const identifierMap = {
|
|
422
|
+
'AWS::EC2::VPC': { VpcId: physicalId },
|
|
423
|
+
'AWS::EC2::Subnet': { SubnetId: physicalId },
|
|
424
|
+
'AWS::EC2::SecurityGroup': { Id: physicalId },
|
|
425
|
+
'AWS::EC2::InternetGateway': { InternetGatewayId: physicalId },
|
|
426
|
+
'AWS::EC2::NatGateway': { NatGatewayId: physicalId },
|
|
427
|
+
'AWS::EC2::RouteTable': { RouteTableId: physicalId },
|
|
428
|
+
'AWS::EC2::VPCEndpoint': { VpcEndpointId: physicalId },
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
return identifierMap[resourceType] || { Id: physicalId };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
module.exports = { ImportTemplateGenerator };
|
|
@@ -33,7 +33,9 @@ class LogicalIdMapper {
|
|
|
33
33
|
|
|
34
34
|
for (const orphan of orphanedResources) {
|
|
35
35
|
// Strategy 1: Check CloudFormation tags for logical ID
|
|
36
|
-
|
|
36
|
+
// Tags are stored in orphan.properties.tags (Resource entity structure)
|
|
37
|
+
const tags = orphan.properties?.tags || orphan.tags; // Support both formats
|
|
38
|
+
const logicalIdFromTag = this._getLogicalIdFromTags(tags);
|
|
37
39
|
|
|
38
40
|
if (logicalIdFromTag) {
|
|
39
41
|
mappings.push({
|
|
@@ -116,15 +118,28 @@ class LogicalIdMapper {
|
|
|
116
118
|
|
|
117
119
|
/**
|
|
118
120
|
* Extract logical ID from CloudFormation tags
|
|
121
|
+
* Supports both formats:
|
|
122
|
+
* - AWS array format: [{Key: 'aws:cloudformation:logical-id', Value: 'FriggVPC'}]
|
|
123
|
+
* - Parsed object format: {'aws:cloudformation:logical-id': 'FriggVPC'}
|
|
119
124
|
* @private
|
|
120
125
|
*/
|
|
121
126
|
_getLogicalIdFromTags(tags) {
|
|
122
|
-
if (!tags
|
|
127
|
+
if (!tags) return null;
|
|
128
|
+
|
|
129
|
+
// Handle AWS array format [{Key, Value}]
|
|
130
|
+
if (Array.isArray(tags)) {
|
|
131
|
+
const logicalIdTag = tags.find(
|
|
132
|
+
(t) => t.Key === 'aws:cloudformation:logical-id'
|
|
133
|
+
);
|
|
134
|
+
return logicalIdTag ? logicalIdTag.Value : null;
|
|
135
|
+
}
|
|
123
136
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
137
|
+
// Handle parsed object format {key: value}
|
|
138
|
+
if (typeof tags === 'object') {
|
|
139
|
+
return tags['aws:cloudformation:logical-id'] || null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return null;
|
|
128
143
|
}
|
|
129
144
|
|
|
130
145
|
/**
|