@friggframework/devtools 2.0.0--canary.474.d64c550.0 → 2.0.0--canary.474.082077e.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,386 @@
1
+ /**
2
+ * AWSStackRepository - AWS CloudFormation Stack Adapter
3
+ *
4
+ * Infrastructure Adapter - Hexagonal Architecture
5
+ *
6
+ * Implements IStackRepository port for AWS CloudFormation.
7
+ * Handles CloudFormation API operations using AWS SDK v3.
8
+ *
9
+ * Lazy-loads AWS SDK to minimize cold start time and memory usage.
10
+ */
11
+
12
+ const IStackRepository = require('../../application/ports/IStackRepository');
13
+ const yaml = require('js-yaml');
14
+
15
+ // Lazy-loaded AWS SDK CloudFormation client
16
+ let CloudFormationClient,
17
+ DescribeStacksCommand,
18
+ ListStackResourcesCommand,
19
+ DescribeStackResourcesCommand,
20
+ DescribeStackResourceCommand,
21
+ GetTemplateCommand,
22
+ DetectStackDriftCommand,
23
+ DescribeStackDriftDetectionStatusCommand,
24
+ DescribeStackResourceDriftsCommand;
25
+
26
+ /**
27
+ * Lazy load CloudFormation SDK
28
+ */
29
+ function loadCloudFormation() {
30
+ if (!CloudFormationClient) {
31
+ const cfModule = require('@aws-sdk/client-cloudformation');
32
+ CloudFormationClient = cfModule.CloudFormationClient;
33
+ DescribeStacksCommand = cfModule.DescribeStacksCommand;
34
+ ListStackResourcesCommand = cfModule.ListStackResourcesCommand;
35
+ DescribeStackResourcesCommand = cfModule.DescribeStackResourcesCommand;
36
+ DescribeStackResourceCommand = cfModule.DescribeStackResourceCommand;
37
+ GetTemplateCommand = cfModule.GetTemplateCommand;
38
+ DetectStackDriftCommand = cfModule.DetectStackDriftCommand;
39
+ DescribeStackDriftDetectionStatusCommand =
40
+ cfModule.DescribeStackDriftDetectionStatusCommand;
41
+ DescribeStackResourceDriftsCommand = cfModule.DescribeStackResourceDriftsCommand;
42
+ }
43
+ }
44
+
45
+ class AWSStackRepository extends IStackRepository {
46
+ /**
47
+ * Create AWS Stack Repository
48
+ *
49
+ * @param {Object} [config={}]
50
+ * @param {string} [config.region] - AWS region (defaults to AWS_REGION env var)
51
+ */
52
+ constructor(config = {}) {
53
+ super();
54
+ this.region = config.region || process.env.AWS_REGION || 'us-east-1';
55
+ this.client = null;
56
+ }
57
+
58
+ /**
59
+ * Get or create CloudFormation client
60
+ * @private
61
+ */
62
+ _getClient() {
63
+ if (!this.client) {
64
+ loadCloudFormation();
65
+ this.client = new CloudFormationClient({ region: this.region });
66
+ }
67
+ return this.client;
68
+ }
69
+
70
+ /**
71
+ * Get stack information by identifier
72
+ */
73
+ async getStack(identifier) {
74
+ const client = this._getClient();
75
+
76
+ try {
77
+ const command = new DescribeStacksCommand({
78
+ StackName: identifier.stackName,
79
+ });
80
+
81
+ const response = await client.send(command);
82
+
83
+ if (!response.Stacks || response.Stacks.length === 0) {
84
+ throw new Error(
85
+ `Stack ${identifier.stackName} does not exist in region ${this.region}`
86
+ );
87
+ }
88
+
89
+ const stack = response.Stacks[0];
90
+
91
+ // Parse ARN to get account ID
92
+ const arnMatch = stack.StackId.match(/:(\d{12}):/);
93
+ const accountId = arnMatch ? arnMatch[1] : identifier.accountId;
94
+
95
+ return {
96
+ stackName: stack.StackName,
97
+ region: this.region,
98
+ accountId,
99
+ stackId: stack.StackId,
100
+ status: stack.StackStatus,
101
+ creationTime: stack.CreationTime,
102
+ lastUpdatedTime: stack.LastUpdatedTime,
103
+ parameters: this._parseParameters(stack.Parameters),
104
+ outputs: this._parseOutputs(stack.Outputs),
105
+ tags: this._parseTags(stack.Tags),
106
+ };
107
+ } catch (error) {
108
+ if (error.name === 'ValidationError' || error.message?.includes('does not exist')) {
109
+ throw new Error(
110
+ `Stack ${identifier.stackName} does not exist in region ${this.region}`
111
+ );
112
+ }
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * List all resources in a stack
119
+ */
120
+ async listResources(identifier) {
121
+ const client = this._getClient();
122
+ const resources = [];
123
+ let nextToken = null;
124
+
125
+ do {
126
+ const command = new ListStackResourcesCommand({
127
+ StackName: identifier.stackName,
128
+ NextToken: nextToken,
129
+ });
130
+
131
+ const response = await client.send(command);
132
+
133
+ if (response.StackResourceSummaries) {
134
+ for (const resource of response.StackResourceSummaries) {
135
+ resources.push({
136
+ logicalId: resource.LogicalResourceId,
137
+ physicalId: resource.PhysicalResourceId,
138
+ resourceType: resource.ResourceType,
139
+ status: resource.ResourceStatus,
140
+ lastUpdatedTime: resource.LastUpdatedTimestamp,
141
+ driftStatus: resource.DriftInformation?.StackResourceDriftStatus || 'NOT_CHECKED',
142
+ });
143
+ }
144
+ }
145
+
146
+ nextToken = response.NextToken;
147
+ } while (nextToken);
148
+
149
+ return resources;
150
+ }
151
+
152
+ /**
153
+ * Get resource details from stack
154
+ */
155
+ async getResource(identifier, logicalId) {
156
+ const client = this._getClient();
157
+
158
+ const command = new DescribeStackResourceCommand({
159
+ StackName: identifier.stackName,
160
+ LogicalResourceId: logicalId,
161
+ });
162
+
163
+ const response = await client.send(command);
164
+ const resource = response.StackResourceDetail;
165
+
166
+ return {
167
+ logicalId: resource.LogicalResourceId,
168
+ physicalId: resource.PhysicalResourceId,
169
+ resourceType: resource.ResourceType,
170
+ status: resource.ResourceStatus,
171
+ properties: {}, // CloudFormation doesn't return properties directly
172
+ metadata: this._parseMetadata(resource.Metadata),
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Get the CloudFormation template for a stack
178
+ */
179
+ async getTemplate(identifier) {
180
+ const client = this._getClient();
181
+
182
+ const command = new GetTemplateCommand({
183
+ StackName: identifier.stackName,
184
+ TemplateStage: 'Original',
185
+ });
186
+
187
+ const response = await client.send(command);
188
+ const templateBody = response.TemplateBody;
189
+
190
+ // Try to parse as JSON first
191
+ try {
192
+ return JSON.parse(templateBody);
193
+ } catch {
194
+ // If not JSON, try YAML
195
+ try {
196
+ return yaml.load(templateBody);
197
+ } catch {
198
+ throw new Error('Failed to parse template body as JSON or YAML');
199
+ }
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Check if a stack exists
205
+ */
206
+ async exists(identifier) {
207
+ try {
208
+ const stack = await this.getStack(identifier);
209
+
210
+ // Check if stack is in a deleted state
211
+ const deletedStates = ['DELETE_COMPLETE', 'DELETE_IN_PROGRESS'];
212
+ return !deletedStates.includes(stack.status);
213
+ } catch (error) {
214
+ if (error.message?.includes('does not exist')) {
215
+ return false;
216
+ }
217
+ throw error;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Detect drift for the entire stack
223
+ */
224
+ async detectStackDrift(identifier) {
225
+ const client = this._getClient();
226
+
227
+ // Initiate drift detection
228
+ const detectCommand = new DetectStackDriftCommand({
229
+ StackName: identifier.stackName,
230
+ });
231
+
232
+ const detectResponse = await client.send(detectCommand);
233
+ const driftDetectionId = detectResponse.StackDriftDetectionId;
234
+
235
+ // Poll for detection completion
236
+ let detectionStatus = 'DETECTION_IN_PROGRESS';
237
+ let statusResponse;
238
+
239
+ while (detectionStatus === 'DETECTION_IN_PROGRESS') {
240
+ await this._sleep(1000); // Wait 1 second between polls
241
+
242
+ const statusCommand = new DescribeStackDriftDetectionStatusCommand({
243
+ StackDriftDetectionId: driftDetectionId,
244
+ });
245
+
246
+ statusResponse = await client.send(statusCommand);
247
+ detectionStatus = statusResponse.DetectionStatus;
248
+ }
249
+
250
+ if (detectionStatus !== 'DETECTION_COMPLETE') {
251
+ throw new Error(`Drift detection failed with status: ${detectionStatus}`);
252
+ }
253
+
254
+ return {
255
+ stackDriftStatus: statusResponse.StackDriftStatus,
256
+ driftedResourceCount: statusResponse.DriftedStackResourceCount || 0,
257
+ detectionTime: statusResponse.Timestamp,
258
+ };
259
+ }
260
+
261
+ /**
262
+ * Get drift details for a specific resource
263
+ */
264
+ async getResourceDrift(identifier, logicalId) {
265
+ const client = this._getClient();
266
+
267
+ const command = new DescribeStackResourceDriftsCommand({
268
+ StackName: identifier.stackName,
269
+ StackResourceDriftStatusFilters: ['MODIFIED', 'DELETED', 'IN_SYNC'],
270
+ });
271
+
272
+ const response = await client.send(command);
273
+
274
+ // Find the specific resource
275
+ const resourceDrift = response.StackResourceDrifts?.find(
276
+ (drift) => drift.LogicalResourceId === logicalId
277
+ );
278
+
279
+ if (!resourceDrift) {
280
+ throw new Error(
281
+ `No drift information found for resource ${logicalId} in stack ${identifier.stackName}`
282
+ );
283
+ }
284
+
285
+ return {
286
+ driftStatus: resourceDrift.StackResourceDriftStatus,
287
+ expectedProperties: this._parseProperties(resourceDrift.ExpectedProperties),
288
+ actualProperties: this._parseProperties(resourceDrift.ActualProperties),
289
+ propertyDifferences: resourceDrift.PropertyDifferences || [],
290
+ };
291
+ }
292
+
293
+ // ========================================
294
+ // Private Helper Methods
295
+ // ========================================
296
+
297
+ /**
298
+ * Parse CloudFormation parameters to key-value object
299
+ * @private
300
+ */
301
+ _parseParameters(parameters) {
302
+ if (!parameters || parameters.length === 0) {
303
+ return {};
304
+ }
305
+
306
+ const result = {};
307
+ for (const param of parameters) {
308
+ result[param.ParameterKey] = param.ParameterValue;
309
+ }
310
+ return result;
311
+ }
312
+
313
+ /**
314
+ * Parse CloudFormation outputs to key-value object
315
+ * @private
316
+ */
317
+ _parseOutputs(outputs) {
318
+ if (!outputs || outputs.length === 0) {
319
+ return {};
320
+ }
321
+
322
+ const result = {};
323
+ for (const output of outputs) {
324
+ result[output.OutputKey] = output.OutputValue;
325
+ }
326
+ return result;
327
+ }
328
+
329
+ /**
330
+ * Parse CloudFormation tags to key-value object
331
+ * @private
332
+ */
333
+ _parseTags(tags) {
334
+ if (!tags || tags.length === 0) {
335
+ return {};
336
+ }
337
+
338
+ const result = {};
339
+ for (const tag of tags) {
340
+ result[tag.Key] = tag.Value;
341
+ }
342
+ return result;
343
+ }
344
+
345
+ /**
346
+ * Parse resource metadata
347
+ * @private
348
+ */
349
+ _parseMetadata(metadata) {
350
+ if (!metadata) {
351
+ return {};
352
+ }
353
+
354
+ try {
355
+ return JSON.parse(metadata);
356
+ } catch {
357
+ return {};
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Parse property JSON string
363
+ * @private
364
+ */
365
+ _parseProperties(propertiesString) {
366
+ if (!propertiesString) {
367
+ return {};
368
+ }
369
+
370
+ try {
371
+ return JSON.parse(propertiesString);
372
+ } catch {
373
+ return {};
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Sleep for specified milliseconds
379
+ * @private
380
+ */
381
+ _sleep(ms) {
382
+ return new Promise((resolve) => setTimeout(resolve, ms));
383
+ }
384
+ }
385
+
386
+ module.exports = AWSStackRepository;