@friggframework/devtools 2.0.0--canary.474.6a0bba7.0 → 2.0.0--canary.474.ca45ad3.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.
@@ -0,0 +1,382 @@
1
+ /**
2
+ * Property Mutability Configuration
3
+ *
4
+ * Defines which CloudFormation resource properties are immutable (require replacement),
5
+ * mutable (can be updated), or conditional (depends on other properties).
6
+ *
7
+ * Based on AWS CloudFormation documentation "Update requires" behavior:
8
+ * - IMMUTABLE: "Replacement" - Cannot be changed without replacing the resource
9
+ * - MUTABLE: "No interruption" or "Some interruptions" - Can be updated in place
10
+ * - CONDITIONAL: Depends on other property values or specific conditions
11
+ *
12
+ * References:
13
+ * - AWS CloudFormation Template Reference: https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/
14
+ * - Each resource type has "Update requires" documentation for each property
15
+ */
16
+
17
+ const PropertyMutability = require('../value-objects/property-mutability');
18
+
19
+ /**
20
+ * Property mutability configuration by resource type
21
+ *
22
+ * Key: CloudFormation resource type (e.g., 'AWS::Lambda::Function')
23
+ * Value: Object mapping property paths to PropertyMutability instances
24
+ *
25
+ * Property paths match AWS drift detection format (without 'Properties.' prefix):
26
+ * - Simple: 'BucketName'
27
+ * - Nested: 'VpcConfig.SubnetIds'
28
+ */
29
+ const PROPERTY_MUTABILITY_CONFIG = {
30
+ //
31
+ // AWS::EC2::* Resources
32
+ //
33
+
34
+ 'AWS::EC2::VPC': {
35
+ // Immutable properties
36
+ 'CidrBlock': PropertyMutability.IMMUTABLE, // Replacement required
37
+ 'InstanceTenancy': PropertyMutability.IMMUTABLE, // Replacement required
38
+
39
+ // Mutable properties
40
+ 'EnableDnsSupport': PropertyMutability.MUTABLE, // No interruption
41
+ 'EnableDnsHostnames': PropertyMutability.MUTABLE, // No interruption
42
+ 'Tags': PropertyMutability.MUTABLE, // No interruption
43
+ },
44
+
45
+ 'AWS::EC2::Subnet': {
46
+ // Immutable properties
47
+ 'VpcId': PropertyMutability.IMMUTABLE, // Replacement required
48
+ 'CidrBlock': PropertyMutability.IMMUTABLE, // Replacement required
49
+ 'AvailabilityZone': PropertyMutability.IMMUTABLE, // Replacement required
50
+ 'AvailabilityZoneId': PropertyMutability.IMMUTABLE, // Replacement required
51
+ 'Ipv4IpamPoolId': PropertyMutability.IMMUTABLE, // Replacement required
52
+ 'Ipv4NetmaskLength': PropertyMutability.IMMUTABLE, // Replacement required
53
+ 'Ipv6IpamPoolId': PropertyMutability.IMMUTABLE, // Replacement required
54
+ 'Ipv6Native': PropertyMutability.IMMUTABLE, // Replacement required
55
+ 'Ipv6NetmaskLength': PropertyMutability.IMMUTABLE, // Replacement required
56
+ 'OutpostArn': PropertyMutability.IMMUTABLE, // Replacement required
57
+
58
+ // Mutable properties
59
+ 'AssignIpv6AddressOnCreation': PropertyMutability.MUTABLE, // No interruption
60
+ 'EnableDns64': PropertyMutability.MUTABLE, // No interruption
61
+ 'EnableLniAtDeviceIndex': PropertyMutability.MUTABLE, // No interruption
62
+ 'Ipv6CidrBlock': PropertyMutability.MUTABLE, // Some interruptions
63
+ 'MapPublicIpOnLaunch': PropertyMutability.MUTABLE, // No interruption
64
+ 'PrivateDnsNameOptionsOnLaunch': PropertyMutability.MUTABLE, // No interruption
65
+ 'Tags': PropertyMutability.MUTABLE, // No interruption
66
+ },
67
+
68
+ 'AWS::EC2::SecurityGroup': {
69
+ // Immutable properties
70
+ 'VpcId': PropertyMutability.IMMUTABLE, // Replacement required
71
+ 'GroupName': PropertyMutability.IMMUTABLE, // Replacement required
72
+
73
+ // Mutable properties
74
+ 'GroupDescription': PropertyMutability.MUTABLE, // No interruption
75
+ 'SecurityGroupIngress': PropertyMutability.MUTABLE, // No interruption
76
+ 'SecurityGroupEgress': PropertyMutability.MUTABLE, // No interruption
77
+ 'Tags': PropertyMutability.MUTABLE, // No interruption
78
+ },
79
+
80
+ 'AWS::EC2::RouteTable': {
81
+ // Immutable properties
82
+ 'VpcId': PropertyMutability.IMMUTABLE, // Replacement required
83
+
84
+ // Mutable properties
85
+ 'Tags': PropertyMutability.MUTABLE, // No interruption
86
+ },
87
+
88
+ 'AWS::EC2::Instance': {
89
+ // Immutable properties
90
+ 'ImageId': PropertyMutability.IMMUTABLE, // Replacement required
91
+ 'InstanceType': PropertyMutability.IMMUTABLE, // Replacement required
92
+ 'KeyName': PropertyMutability.IMMUTABLE, // Replacement required
93
+ 'AvailabilityZone': PropertyMutability.IMMUTABLE, // Replacement required
94
+ 'PlacementGroupName': PropertyMutability.IMMUTABLE, // Replacement required
95
+ 'PrivateIpAddress': PropertyMutability.IMMUTABLE, // Replacement required
96
+ 'SubnetId': PropertyMutability.IMMUTABLE, // Replacement required
97
+
98
+ // Mutable properties
99
+ 'SecurityGroupIds': PropertyMutability.MUTABLE, // No interruption (VPC instances)
100
+ 'SecurityGroups': PropertyMutability.MUTABLE, // No interruption
101
+ 'UserData': PropertyMutability.MUTABLE, // Some interruptions
102
+ 'IamInstanceProfile': PropertyMutability.MUTABLE, // No interruption
103
+ 'Monitoring': PropertyMutability.MUTABLE, // No interruption
104
+ 'Tags': PropertyMutability.MUTABLE, // No interruption
105
+ 'DisableApiTermination': PropertyMutability.MUTABLE, // No interruption
106
+ 'EbsOptimized': PropertyMutability.MUTABLE, // Some interruptions
107
+ },
108
+
109
+ //
110
+ // AWS::Lambda::* Resources
111
+ //
112
+
113
+ 'AWS::Lambda::Function': {
114
+ // Immutable properties
115
+ 'FunctionName': PropertyMutability.IMMUTABLE, // Replacement required
116
+
117
+ // Mutable properties
118
+ 'Code': PropertyMutability.MUTABLE, // No interruption
119
+ 'Runtime': PropertyMutability.MUTABLE, // No interruption
120
+ 'Handler': PropertyMutability.MUTABLE, // No interruption
121
+ 'Role': PropertyMutability.MUTABLE, // No interruption
122
+ 'Description': PropertyMutability.MUTABLE, // No interruption
123
+ 'Timeout': PropertyMutability.MUTABLE, // No interruption
124
+ 'MemorySize': PropertyMutability.MUTABLE, // No interruption
125
+ 'Environment': PropertyMutability.MUTABLE, // No interruption
126
+ 'Environment.Variables': PropertyMutability.MUTABLE, // No interruption
127
+ 'VpcConfig': PropertyMutability.MUTABLE, // No interruption
128
+ 'VpcConfig.SubnetIds': PropertyMutability.MUTABLE, // No interruption
129
+ 'VpcConfig.SecurityGroupIds': PropertyMutability.MUTABLE, // No interruption
130
+ 'VpcConfig.Ipv6AllowedForDualStack': PropertyMutability.MUTABLE, // No interruption
131
+ 'DeadLetterConfig': PropertyMutability.MUTABLE, // No interruption
132
+ 'TracingConfig': PropertyMutability.MUTABLE, // No interruption
133
+ 'KMSKeyArn': PropertyMutability.MUTABLE, // No interruption
134
+ 'Layers': PropertyMutability.MUTABLE, // No interruption
135
+ 'ReservedConcurrentExecutions': PropertyMutability.MUTABLE, // No interruption
136
+ 'Tags': PropertyMutability.MUTABLE, // No interruption
137
+ 'FileSystemConfigs': PropertyMutability.MUTABLE, // No interruption
138
+ 'Architectures': PropertyMutability.MUTABLE, // No interruption
139
+ 'EphemeralStorage': PropertyMutability.MUTABLE, // No interruption
140
+ 'SnapStart': PropertyMutability.MUTABLE, // No interruption
141
+ 'RuntimeManagementConfig': PropertyMutability.MUTABLE, // No interruption
142
+ 'LoggingConfig': PropertyMutability.MUTABLE, // No interruption
143
+ },
144
+
145
+ //
146
+ // AWS::RDS::* Resources
147
+ //
148
+
149
+ 'AWS::RDS::DBInstance': {
150
+ // Immutable properties
151
+ 'DBInstanceIdentifier': PropertyMutability.IMMUTABLE, // Replacement required
152
+ 'Engine': PropertyMutability.IMMUTABLE, // Replacement required
153
+ 'DBName': PropertyMutability.IMMUTABLE, // Replacement required (for some engines)
154
+ 'AvailabilityZone': PropertyMutability.IMMUTABLE, // Replacement required
155
+
156
+ // Mutable properties
157
+ 'AllocatedStorage': PropertyMutability.MUTABLE, // No interruption
158
+ 'DBInstanceClass': PropertyMutability.MUTABLE, // Some interruptions
159
+ 'EngineVersion': PropertyMutability.MUTABLE, // Some interruptions
160
+ 'MasterUsername': PropertyMutability.MUTABLE, // No interruption (can't actually change)
161
+ 'MasterUserPassword': PropertyMutability.MUTABLE, // No interruption
162
+ 'BackupRetentionPeriod': PropertyMutability.MUTABLE, // No interruption
163
+ 'DBSecurityGroups': PropertyMutability.MUTABLE, // No interruption
164
+ 'VPCSecurityGroups': PropertyMutability.MUTABLE, // No interruption
165
+ 'MultiAZ': PropertyMutability.MUTABLE, // No interruption
166
+ 'PubliclyAccessible': PropertyMutability.MUTABLE, // No interruption
167
+ 'StorageEncrypted': PropertyMutability.MUTABLE, // Some interruptions
168
+ 'Tags': PropertyMutability.MUTABLE, // No interruption
169
+ },
170
+
171
+ 'AWS::RDS::DBCluster': {
172
+ // Immutable properties
173
+ 'DBClusterIdentifier': PropertyMutability.IMMUTABLE, // Replacement required
174
+ 'Engine': PropertyMutability.IMMUTABLE, // Replacement required
175
+ 'DatabaseName': PropertyMutability.IMMUTABLE, // Replacement required
176
+
177
+ // Mutable properties
178
+ 'EngineVersion': PropertyMutability.MUTABLE, // No interruption
179
+ 'MasterUsername': PropertyMutability.MUTABLE, // No interruption (can't actually change)
180
+ 'MasterUserPassword': PropertyMutability.MUTABLE, // No interruption
181
+ 'BackupRetentionPeriod': PropertyMutability.MUTABLE, // No interruption
182
+ 'PreferredBackupWindow': PropertyMutability.MUTABLE, // No interruption
183
+ 'PreferredMaintenanceWindow': PropertyMutability.MUTABLE, // No interruption
184
+ 'VpcSecurityGroupIds': PropertyMutability.MUTABLE, // No interruption
185
+ 'DBSubnetGroupName': PropertyMutability.MUTABLE, // Some interruptions
186
+ 'StorageEncrypted': PropertyMutability.MUTABLE, // Some interruptions
187
+ 'KmsKeyId': PropertyMutability.MUTABLE, // Some interruptions
188
+ 'Tags': PropertyMutability.MUTABLE, // No interruption
189
+ },
190
+
191
+ //
192
+ // AWS::S3::* Resources
193
+ //
194
+
195
+ 'AWS::S3::Bucket': {
196
+ // Immutable properties
197
+ 'BucketName': PropertyMutability.IMMUTABLE, // Replacement required
198
+
199
+ // Mutable properties
200
+ 'AccelerateConfiguration': PropertyMutability.MUTABLE, // No interruption
201
+ 'AccessControl': PropertyMutability.MUTABLE, // No interruption
202
+ 'AnalyticsConfigurations': PropertyMutability.MUTABLE, // No interruption
203
+ 'BucketEncryption': PropertyMutability.MUTABLE, // No interruption
204
+ 'CorsConfiguration': PropertyMutability.MUTABLE, // No interruption
205
+ 'IntelligentTieringConfigurations': PropertyMutability.MUTABLE, // No interruption
206
+ 'InventoryConfigurations': PropertyMutability.MUTABLE, // No interruption
207
+ 'LifecycleConfiguration': PropertyMutability.MUTABLE, // No interruption
208
+ 'LoggingConfiguration': PropertyMutability.MUTABLE, // No interruption
209
+ 'MetricsConfigurations': PropertyMutability.MUTABLE, // No interruption
210
+ 'NotificationConfiguration': PropertyMutability.MUTABLE, // No interruption
211
+ 'ObjectLockConfiguration': PropertyMutability.MUTABLE, // No interruption
212
+ 'ObjectLockEnabled': PropertyMutability.MUTABLE, // No interruption
213
+ 'OwnershipControls': PropertyMutability.MUTABLE, // No interruption
214
+ 'PublicAccessBlockConfiguration': PropertyMutability.MUTABLE, // No interruption
215
+ 'ReplicationConfiguration': PropertyMutability.MUTABLE, // No interruption
216
+ 'Tags': PropertyMutability.MUTABLE, // No interruption
217
+ 'VersioningConfiguration': PropertyMutability.MUTABLE, // No interruption
218
+ 'WebsiteConfiguration': PropertyMutability.MUTABLE, // No interruption
219
+ },
220
+
221
+ //
222
+ // AWS::KMS::* Resources
223
+ //
224
+
225
+ 'AWS::KMS::Key': {
226
+ // All KMS key properties are mutable (updates don't require replacement)
227
+ 'Description': PropertyMutability.MUTABLE, // No interruption
228
+ 'Enabled': PropertyMutability.MUTABLE, // No interruption
229
+ 'EnableKeyRotation': PropertyMutability.MUTABLE, // No interruption
230
+ 'KeyPolicy': PropertyMutability.MUTABLE, // No interruption
231
+ 'KeyUsage': PropertyMutability.MUTABLE, // No interruption
232
+ 'MultiRegion': PropertyMutability.MUTABLE, // No interruption
233
+ 'PendingWindowInDays': PropertyMutability.MUTABLE, // No interruption
234
+ 'Tags': PropertyMutability.MUTABLE, // No interruption
235
+ },
236
+
237
+ //
238
+ // AWS::DynamoDB::* Resources
239
+ //
240
+
241
+ 'AWS::DynamoDB::Table': {
242
+ // Immutable properties
243
+ 'TableName': PropertyMutability.IMMUTABLE, // Replacement required
244
+ 'KeySchema': PropertyMutability.IMMUTABLE, // Replacement required
245
+ 'AttributeDefinitions': PropertyMutability.CONDITIONAL, // Some changes require replacement
246
+
247
+ // Mutable properties
248
+ 'BillingMode': PropertyMutability.MUTABLE, // No interruption
249
+ 'ProvisionedThroughput': PropertyMutability.MUTABLE, // No interruption
250
+ 'GlobalSecondaryIndexes': PropertyMutability.MUTABLE, // No interruption
251
+ 'LocalSecondaryIndexes': PropertyMutability.IMMUTABLE, // Replacement required
252
+ 'StreamSpecification': PropertyMutability.MUTABLE, // No interruption
253
+ 'SSESpecification': PropertyMutability.MUTABLE, // No interruption
254
+ 'Tags': PropertyMutability.MUTABLE, // No interruption
255
+ 'TimeToLiveSpecification': PropertyMutability.MUTABLE, // No interruption
256
+ 'PointInTimeRecoverySpecification': PropertyMutability.MUTABLE, // No interruption
257
+ 'ContributorInsightsSpecification': PropertyMutability.MUTABLE, // No interruption
258
+ 'KinesisStreamSpecification': PropertyMutability.MUTABLE, // No interruption
259
+ },
260
+
261
+ //
262
+ // AWS::SQS::* Resources
263
+ //
264
+
265
+ 'AWS::SQS::Queue': {
266
+ // Immutable properties
267
+ 'QueueName': PropertyMutability.IMMUTABLE, // Replacement required
268
+ 'FifoQueue': PropertyMutability.IMMUTABLE, // Replacement required
269
+
270
+ // Mutable properties
271
+ 'ContentBasedDeduplication': PropertyMutability.MUTABLE, // No interruption
272
+ 'DeduplicationScope': PropertyMutability.MUTABLE, // No interruption
273
+ 'DelaySeconds': PropertyMutability.MUTABLE, // No interruption
274
+ 'FifoThroughputLimit': PropertyMutability.MUTABLE, // No interruption
275
+ 'KmsMasterKeyId': PropertyMutability.MUTABLE, // No interruption
276
+ 'KmsDataKeyReusePeriodSeconds': PropertyMutability.MUTABLE, // No interruption
277
+ 'MaximumMessageSize': PropertyMutability.MUTABLE, // No interruption
278
+ 'MessageRetentionPeriod': PropertyMutability.MUTABLE, // No interruption
279
+ 'ReceiveMessageWaitTimeSeconds': PropertyMutability.MUTABLE, // No interruption
280
+ 'RedrivePolicy': PropertyMutability.MUTABLE, // No interruption
281
+ 'RedriveAllowPolicy': PropertyMutability.MUTABLE, // No interruption
282
+ 'SqsManagedSseEnabled': PropertyMutability.MUTABLE, // No interruption
283
+ 'Tags': PropertyMutability.MUTABLE, // No interruption
284
+ 'VisibilityTimeout': PropertyMutability.MUTABLE, // No interruption
285
+ },
286
+
287
+ //
288
+ // AWS::SNS::* Resources
289
+ //
290
+
291
+ 'AWS::SNS::Topic': {
292
+ // Immutable properties
293
+ 'TopicName': PropertyMutability.IMMUTABLE, // Replacement required
294
+ 'FifoTopic': PropertyMutability.IMMUTABLE, // Replacement required
295
+
296
+ // Mutable properties
297
+ 'ContentBasedDeduplication': PropertyMutability.MUTABLE, // No interruption
298
+ 'DataProtectionPolicy': PropertyMutability.MUTABLE, // No interruption
299
+ 'DisplayName': PropertyMutability.MUTABLE, // No interruption
300
+ 'KmsMasterKeyId': PropertyMutability.MUTABLE, // No interruption
301
+ 'SignatureVersion': PropertyMutability.MUTABLE, // No interruption
302
+ 'Subscription': PropertyMutability.MUTABLE, // No interruption
303
+ 'Tags': PropertyMutability.MUTABLE, // No interruption
304
+ 'TracingConfig': PropertyMutability.MUTABLE, // No interruption
305
+ },
306
+
307
+ //
308
+ // AWS::IAM::* Resources
309
+ //
310
+
311
+ 'AWS::IAM::Role': {
312
+ // Immutable properties
313
+ 'RoleName': PropertyMutability.IMMUTABLE, // Replacement required
314
+
315
+ // Mutable properties
316
+ 'AssumeRolePolicyDocument': PropertyMutability.MUTABLE, // No interruption
317
+ 'Description': PropertyMutability.MUTABLE, // No interruption
318
+ 'ManagedPolicyArns': PropertyMutability.MUTABLE, // No interruption
319
+ 'MaxSessionDuration': PropertyMutability.MUTABLE, // No interruption
320
+ 'Path': PropertyMutability.MUTABLE, // No interruption
321
+ 'PermissionsBoundary': PropertyMutability.MUTABLE, // No interruption
322
+ 'Policies': PropertyMutability.MUTABLE, // No interruption
323
+ 'Tags': PropertyMutability.MUTABLE, // No interruption
324
+ },
325
+ };
326
+
327
+ /**
328
+ * Get property mutability for a given resource type and property path
329
+ *
330
+ * @param {string} resourceType - CloudFormation resource type (e.g., 'AWS::Lambda::Function')
331
+ * @param {string} propertyPath - Property path from drift detection (e.g., 'VpcConfig.SubnetIds')
332
+ * @returns {PropertyMutability} Property mutability (defaults to MUTABLE if not found)
333
+ */
334
+ function getPropertyMutability(resourceType, propertyPath) {
335
+ // Check if resource type has configuration
336
+ if (!PROPERTY_MUTABILITY_CONFIG[resourceType]) {
337
+ // Unknown resource type - default to MUTABLE (safe, allows reconciliation attempt)
338
+ return PropertyMutability.MUTABLE;
339
+ }
340
+
341
+ const resourceConfig = PROPERTY_MUTABILITY_CONFIG[resourceType];
342
+
343
+ // Check exact property path match
344
+ if (resourceConfig[propertyPath]) {
345
+ return resourceConfig[propertyPath];
346
+ }
347
+
348
+ // Check parent property for nested paths (e.g., 'VpcConfig' for 'VpcConfig.SubnetIds')
349
+ const parentPath = propertyPath.split('.')[0];
350
+ if (resourceConfig[parentPath]) {
351
+ return resourceConfig[parentPath];
352
+ }
353
+
354
+ // Property not found in config - default to MUTABLE
355
+ return PropertyMutability.MUTABLE;
356
+ }
357
+
358
+ /**
359
+ * Check if a resource type is supported in the configuration
360
+ *
361
+ * @param {string} resourceType - CloudFormation resource type
362
+ * @returns {boolean} True if resource type is configured
363
+ */
364
+ function isResourceTypeConfigured(resourceType) {
365
+ return resourceType in PROPERTY_MUTABILITY_CONFIG;
366
+ }
367
+
368
+ /**
369
+ * Get all configured resource types
370
+ *
371
+ * @returns {string[]} Array of resource type names
372
+ */
373
+ function getConfiguredResourceTypes() {
374
+ return Object.keys(PROPERTY_MUTABILITY_CONFIG);
375
+ }
376
+
377
+ module.exports = {
378
+ PROPERTY_MUTABILITY_CONFIG,
379
+ getPropertyMutability,
380
+ isResourceTypeConfigured,
381
+ getConfiguredResourceTypes,
382
+ };
@@ -0,0 +1,192 @@
1
+ /**
2
+ * UpdateProgressMonitor - Monitor CloudFormation Update Operation Progress
3
+ *
4
+ * Domain Layer - Service
5
+ *
6
+ * Monitors CloudFormation UPDATE operations by polling stack events and tracking
7
+ * resource update progress. Provides real-time progress callbacks and detects
8
+ * failures, rollbacks, and timeouts.
9
+ *
10
+ * Responsibilities:
11
+ * - Poll CloudFormation stack events during update
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 UpdateProgressMonitor {
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 update operation progress
34
+ *
35
+ * Polls CloudFormation stack events every 2 seconds to track resource update 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>} Update result
44
+ */
45
+ async monitorUpdate({ stackIdentifier, resourceLogicalIds, onProgress }) {
46
+ // If no resources to track, return immediately
47
+ if (!resourceLogicalIds || resourceLogicalIds.length === 0) {
48
+ return {
49
+ success: true,
50
+ updatedCount: 0,
51
+ failedCount: 0,
52
+ failedResources: [],
53
+ };
54
+ }
55
+
56
+ const updatedResources = new Set();
57
+ const failedResources = [];
58
+ const processedEvents = new Set(); // Track processed events by timestamp + logicalId
59
+ let elapsedTime = 0; // Track elapsed time manually for fake timers compatibility
60
+ const TIMEOUT_MS = 300000; // 5 minutes
61
+ const POLL_INTERVAL_MS = 2000; // 2 seconds
62
+
63
+ // Continue polling until all resources are complete or failed
64
+ while (
65
+ updatedResources.size + failedResources.length <
66
+ resourceLogicalIds.length
67
+ ) {
68
+ // Wait 2 seconds before polling
69
+ await this._delay(POLL_INTERVAL_MS);
70
+ elapsedTime += POLL_INTERVAL_MS;
71
+
72
+ // Check for timeout
73
+ if (elapsedTime > TIMEOUT_MS) {
74
+ throw new Error('Update operation timed out');
75
+ }
76
+
77
+ // Get stack events
78
+ const events = await this.cfRepo.getStackEvents({
79
+ stackIdentifier,
80
+ });
81
+
82
+ // Sort events by timestamp (oldest first) for consistent processing
83
+ const sortedEvents = [...events].sort(
84
+ (a, b) => new Date(a.Timestamp) - new Date(b.Timestamp)
85
+ );
86
+
87
+ // Process events for tracked resources
88
+ for (const event of sortedEvents) {
89
+ const logicalId = event.LogicalResourceId;
90
+
91
+ // Skip if not a tracked resource
92
+ if (!resourceLogicalIds.includes(logicalId)) {
93
+ continue;
94
+ }
95
+
96
+ // Create unique event key to avoid duplicate processing
97
+ const eventKey = `${event.Timestamp.toISOString()}_${logicalId}_${event.ResourceStatus}`;
98
+
99
+ // Skip if already processed
100
+ if (processedEvents.has(eventKey)) {
101
+ continue;
102
+ }
103
+
104
+ processedEvents.add(eventKey);
105
+
106
+ // Handle different resource statuses
107
+ if (event.ResourceStatus === 'UPDATE_IN_PROGRESS') {
108
+ // Call progress callback with IN_PROGRESS status
109
+ if (onProgress) {
110
+ onProgress({
111
+ logicalId,
112
+ status: 'IN_PROGRESS',
113
+ });
114
+ }
115
+ } else if (event.ResourceStatus === 'UPDATE_COMPLETE') {
116
+ // Mark resource as updated
117
+ updatedResources.add(logicalId);
118
+
119
+ // Call progress callback with COMPLETE status
120
+ if (onProgress) {
121
+ onProgress({
122
+ logicalId,
123
+ status: 'COMPLETE',
124
+ progress: updatedResources.size,
125
+ total: resourceLogicalIds.length,
126
+ });
127
+ }
128
+ } else if (event.ResourceStatus === 'UPDATE_FAILED') {
129
+ // Add to failed resources
130
+ const reason = event.ResourceStatusReason || 'Unknown error';
131
+ failedResources.push({
132
+ logicalId,
133
+ reason,
134
+ });
135
+
136
+ // Call progress callback with FAILED status
137
+ if (onProgress) {
138
+ onProgress({
139
+ logicalId,
140
+ status: 'FAILED',
141
+ reason,
142
+ });
143
+ }
144
+ }
145
+ }
146
+
147
+ // Check if all resources are now accounted for
148
+ const allResourcesProcessed =
149
+ updatedResources.size + failedResources.length >=
150
+ resourceLogicalIds.length;
151
+
152
+ // If all resources processed, exit loop to return result
153
+ if (allResourcesProcessed) {
154
+ break;
155
+ }
156
+
157
+ // Check stack status AFTER processing events - if rollback in progress, throw
158
+ const stackStatus = await this.cfRepo.getStackStatus(stackIdentifier);
159
+ if (
160
+ stackStatus.includes('ROLLBACK') &&
161
+ stackStatus !== 'UPDATE_ROLLBACK_COMPLETE'
162
+ ) {
163
+ throw new Error('Update operation failed and rolled back');
164
+ }
165
+ }
166
+
167
+ // Check final stack status before returning
168
+ const finalStackStatus = await this.cfRepo.getStackStatus(stackIdentifier);
169
+
170
+ // Return result (success = no failures)
171
+ const success = failedResources.length === 0;
172
+ return {
173
+ success,
174
+ updatedCount: updatedResources.size,
175
+ failedCount: failedResources.length,
176
+ failedResources,
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Delay helper for polling intervals
182
+ *
183
+ * @param {number} ms - Milliseconds to delay
184
+ * @returns {Promise<void>}
185
+ * @private
186
+ */
187
+ async _delay(ms) {
188
+ return new Promise((resolve) => setTimeout(resolve, ms));
189
+ }
190
+ }
191
+
192
+ module.exports = { UpdateProgressMonitor };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/devtools",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.474.6a0bba7.0",
4
+ "version": "2.0.0--canary.474.ca45ad3.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-ec2": "^3.835.0",
7
7
  "@aws-sdk/client-kms": "^3.835.0",
@@ -11,8 +11,8 @@
11
11
  "@babel/eslint-parser": "^7.18.9",
12
12
  "@babel/parser": "^7.25.3",
13
13
  "@babel/traverse": "^7.25.3",
14
- "@friggframework/schemas": "2.0.0--canary.474.6a0bba7.0",
15
- "@friggframework/test": "2.0.0--canary.474.6a0bba7.0",
14
+ "@friggframework/schemas": "2.0.0--canary.474.ca45ad3.0",
15
+ "@friggframework/test": "2.0.0--canary.474.ca45ad3.0",
16
16
  "@hapi/boom": "^10.0.1",
17
17
  "@inquirer/prompts": "^5.3.8",
18
18
  "axios": "^1.7.2",
@@ -34,8 +34,8 @@
34
34
  "serverless-http": "^2.7.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@friggframework/eslint-config": "2.0.0--canary.474.6a0bba7.0",
38
- "@friggframework/prettier-config": "2.0.0--canary.474.6a0bba7.0",
37
+ "@friggframework/eslint-config": "2.0.0--canary.474.ca45ad3.0",
38
+ "@friggframework/prettier-config": "2.0.0--canary.474.ca45ad3.0",
39
39
  "aws-sdk-client-mock": "^4.1.0",
40
40
  "aws-sdk-client-mock-jest": "^4.1.0",
41
41
  "jest": "^30.1.3",
@@ -67,5 +67,5 @@
67
67
  "publishConfig": {
68
68
  "access": "public"
69
69
  },
70
- "gitHead": "6a0bba7b2074339687448fa7eba7d377c3870bea"
70
+ "gitHead": "ca45ad3b737f85241453177ba87eb05fba6d389e"
71
71
  }