@friggframework/devtools 2.0.0--canary.461.77c8d12.0 → 2.0.0--canary.461.5ebf96a.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.
@@ -233,7 +233,14 @@ class VpcBuilder extends InfrastructureBuilder {
233
233
  existingEndpoints.kms || existingEndpoints.secretsManager || existingEndpoints.sqs;
234
234
 
235
235
  if (appDefinition.vpc.enableVPCEndpoints !== false) {
236
- if (vpcManagement === 'create-new') {
236
+ // Check if resources came from CloudFormation stack
237
+ const fromCfStack = discoveredResources.fromCloudFormationStack === true;
238
+ const existingLogicalIds = discoveredResources.existingLogicalIds || [];
239
+
240
+ if (fromCfStack && existingLogicalIds.length > 0 && allEndpointsExist) {
241
+ console.log(' All VPC endpoints exist in CloudFormation stack - skipping creation');
242
+ // Skip VPC endpoint creation entirely
243
+ } else if (vpcManagement === 'create-new') {
237
244
  // Always create in create-new mode
238
245
  this.buildVpcEndpoints(appDefinition, discoveredResources, result, existingEndpoints);
239
246
  } else if (vpcManagement === 'discover') {
@@ -418,6 +425,27 @@ class VpcBuilder extends InfrastructureBuilder {
418
425
 
419
426
  result.vpcId = discoveredResources.defaultVpcId;
420
427
 
428
+ // Check if resources came from CloudFormation stack
429
+ const fromCfStack = discoveredResources.fromCloudFormationStack === true;
430
+ const existingLogicalIds = discoveredResources.existingLogicalIds || [];
431
+
432
+ if (fromCfStack && existingLogicalIds.length > 0) {
433
+ console.log(` ✓ VPC discovered from CloudFormation stack: ${discoveredResources.stackName}`);
434
+ console.log(` ✓ Found ${existingLogicalIds.length} existing resources in stack`);
435
+ console.log(' ℹ Skipping resource creation - will reuse existing CloudFormation resources');
436
+
437
+ // Set security group IDs from discovered resources
438
+ if (discoveredResources.securityGroupId) {
439
+ result.vpcConfig.securityGroupIds = [discoveredResources.securityGroupId];
440
+ }
441
+
442
+ // Don't create any new resources - they already exist in the CF stack
443
+ return;
444
+ }
445
+
446
+ // VPC discovered from AWS API (not from CF stack) - create needed resources
447
+ console.log(' ℹ VPC discovered from AWS API - will create Lambda security group');
448
+
421
449
  // Create a Lambda security group in the discovered VPC
422
450
  // This is needed even in discover mode so other resources can reference it
423
451
  result.resources.FriggLambdaSecurityGroup = {
@@ -695,6 +723,15 @@ class VpcBuilder extends InfrastructureBuilder {
695
723
 
696
724
  console.log(` NAT Gateway Management: ${natManagement}`);
697
725
 
726
+ // Check if resources came from CloudFormation stack
727
+ const fromCfStack = discoveredResources.fromCloudFormationStack === true;
728
+ const existingLogicalIds = discoveredResources.existingLogicalIds || [];
729
+
730
+ if (fromCfStack && existingLogicalIds.length > 0) {
731
+ console.log(' Skipping NAT Gateway - will reuse from CloudFormation stack');
732
+ return;
733
+ }
734
+
698
735
  // Check if we should create NAT Gateway
699
736
  const needsNatGateway = natManagement === 'createAndManage' ||
700
737
  discoveredResources.needsNewNatGateway === true;
@@ -22,7 +22,7 @@ class CloudFormationDiscovery {
22
22
 
23
23
  /**
24
24
  * Discover resources from an existing CloudFormation stack
25
- *
25
+ *
26
26
  * @param {string} stackName - Name of the CloudFormation stack
27
27
  * @returns {Promise<Object|null>} Discovered resources or null if stack doesn't exist
28
28
  */
@@ -35,7 +35,12 @@ class CloudFormationDiscovery {
35
35
  const resources = await this.provider.listStackResources(stackName);
36
36
 
37
37
  // Extract discovered resources from outputs and resources
38
- const discovered = {};
38
+ const discovered = {
39
+ // Metadata to indicate resources came from CloudFormation stack
40
+ fromCloudFormationStack: true,
41
+ stackName: stackName,
42
+ existingLogicalIds: []
43
+ };
39
44
 
40
45
  // Extract from outputs
41
46
  if (stack.Outputs && stack.Outputs.length > 0) {
@@ -46,6 +51,11 @@ class CloudFormationDiscovery {
46
51
  // Always call this even if resources is empty, as it may query AWS for resources
47
52
  await this._extractFromResources(resources || [], discovered);
48
53
 
54
+ // Clean up metadata if no resources were discovered
55
+ if (discovered.existingLogicalIds.length === 0) {
56
+ delete discovered.existingLogicalIds;
57
+ }
58
+
49
59
  return discovered;
50
60
  } catch (error) {
51
61
  // Stack doesn't exist - return null to trigger fallback discovery
@@ -103,16 +113,26 @@ class CloudFormationDiscovery {
103
113
 
104
114
  /**
105
115
  * Extract discovered resources from CloudFormation stack resources
106
- *
116
+ *
107
117
  * @private
108
118
  * @param {Array} resources - CloudFormation stack resources
109
119
  * @param {Object} discovered - Object to populate with discovered resources
110
120
  */
111
121
  async _extractFromResources(resources, discovered) {
112
122
  console.log(` DEBUG: Processing ${resources.length} CloudFormation resources...`);
123
+
124
+ // Initialize existingLogicalIds array if not present
125
+ if (!discovered.existingLogicalIds) {
126
+ discovered.existingLogicalIds = [];
127
+ }
113
128
  for (const resource of resources) {
114
129
  const { LogicalResourceId, PhysicalResourceId, ResourceType } = resource;
115
130
 
131
+ // Track Frigg-managed resources by logical ID
132
+ if (LogicalResourceId.startsWith('Frigg') || LogicalResourceId.includes('Migration')) {
133
+ discovered.existingLogicalIds.push(LogicalResourceId);
134
+ }
135
+
116
136
  // Debug Aurora detection
117
137
  if (LogicalResourceId.includes('Aurora')) {
118
138
  console.log(` DEBUG: Found Aurora resource: ${LogicalResourceId} (${ResourceType})`);
@@ -122,18 +142,21 @@ class CloudFormationDiscovery {
122
142
  if (LogicalResourceId === 'FriggLambdaSecurityGroup' && ResourceType === 'AWS::EC2::SecurityGroup') {
123
143
  discovered.securityGroupId = PhysicalResourceId;
124
144
  console.log(` ✓ Found security group in stack: ${PhysicalResourceId}`);
125
- // Query security group to get VPC ID
126
- if (this.provider && !discovered.defaultVpcId) {
145
+
146
+ // Query security group to get VPC ID (required because SG resource doesn't include VPC ID)
147
+ if (this.provider && this.provider.getEC2Client && !discovered.defaultVpcId) {
127
148
  try {
128
- console.log(` Querying security group to get VPC ID...`);
149
+ console.log(` Querying EC2 to get VPC ID from security group...`);
129
150
  const { DescribeSecurityGroupsCommand } = require('@aws-sdk/client-ec2');
130
- const sgDetails = await this.provider.getEC2Client().send(
151
+ const ec2Client = this.provider.getEC2Client();
152
+ const sgDetails = await ec2Client.send(
131
153
  new DescribeSecurityGroupsCommand({
132
154
  GroupIds: [PhysicalResourceId]
133
155
  })
134
156
  );
157
+
135
158
  if (sgDetails.SecurityGroups && sgDetails.SecurityGroups.length > 0) {
136
- discovered.defaultVpcId = sgDetails.SecurityGroups[0].VpcId; // VpcBuilder expects 'defaultVpcId'
159
+ discovered.defaultVpcId = sgDetails.SecurityGroups[0].VpcId;
137
160
  console.log(` ✓ Extracted VPC ID from security group: ${discovered.defaultVpcId}`);
138
161
  } else {
139
162
  console.warn(` ⚠️ Security group query returned no results`);
@@ -193,6 +216,12 @@ class CloudFormationDiscovery {
193
216
  discovered.natGatewayId = PhysicalResourceId;
194
217
  }
195
218
 
219
+ // VPC - direct extraction (primary method)
220
+ if (LogicalResourceId === 'FriggVPC' && ResourceType === 'AWS::EC2::VPC') {
221
+ discovered.defaultVpcId = PhysicalResourceId;
222
+ console.log(` ✓ Found VPC in stack: ${PhysicalResourceId}`);
223
+ }
224
+
196
225
  // KMS Key (alternative to output)
197
226
  if (LogicalResourceId === 'FriggKMSKey' && ResourceType === 'AWS::KMS::Key') {
198
227
  // Note: For KMS, we prefer the ARN from outputs, but this is a fallback
@@ -253,6 +253,30 @@ describe('CloudFormationDiscovery', () => {
253
253
  });
254
254
  });
255
255
 
256
+ it('should extract VPC directly from stack resources', async () => {
257
+ const mockStack = {
258
+ StackName: 'test-stack',
259
+ Outputs: [],
260
+ };
261
+
262
+ const mockResources = [
263
+ {
264
+ LogicalResourceId: 'FriggVPC',
265
+ PhysicalResourceId: 'vpc-037ec55fe87aec1e7',
266
+ ResourceType: 'AWS::EC2::VPC',
267
+ },
268
+ ];
269
+
270
+ mockProvider.describeStack.mockResolvedValue(mockStack);
271
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
272
+
273
+ const result = await cfDiscovery.discoverFromStack('test-stack');
274
+
275
+ expect(result).toEqual({
276
+ defaultVpcId: 'vpc-037ec55fe87aec1e7',
277
+ });
278
+ });
279
+
256
280
  it('should combine outputs and resources correctly', async () => {
257
281
  const mockStack = {
258
282
  StackName: 'test-stack',
@@ -85,19 +85,26 @@ async function gatherDiscoveredResources(appDefinition) {
85
85
  const hasAuroraData = stackResources?.auroraClusterId;
86
86
  const hasSomeUsefulData = hasVpcData || hasKmsData || hasAuroraData;
87
87
 
88
+ // Check if we're in isolated mode (each stage gets its own VPC/Aurora)
89
+ const isIsolatedMode = appDefinition.managementMode === 'managed' &&
90
+ appDefinition.vpcIsolation === 'isolated';
91
+
88
92
  if (stackResources && hasSomeUsefulData) {
89
93
  console.log(' ✓ Discovered resources from existing CloudFormation stack');
90
94
  console.log('✅ Cloud resource discovery completed successfully!');
91
95
  return stackResources;
92
96
  }
93
97
 
94
- // In isolated mode, ONLY use CloudFormation discovery for VPC/Aurora
95
- // But still discover KMS (encryption keys can be safely shared across stages)
96
- if (appDefinition.managementMode === 'managed' && appDefinition.vpcIsolation === 'isolated') {
97
- console.log(' ℹ Isolated mode: discovering KMS (shareable) but not VPC/Aurora (isolated)');
98
+ // In isolated mode, NEVER fall back to AWS discovery for VPC/Aurora
99
+ // These resources must be isolated per stage, so we either:
100
+ // 1. Use resources from THIS stage's CloudFormation stack (handled above)
101
+ // 2. Return empty to CREATE fresh isolated resources for this stage
102
+ if (isIsolatedMode) {
103
+ console.log(' ℹ Isolated mode: No CloudFormation stack or no VPC/Aurora in stack');
104
+ console.log(' ℹ Will create fresh isolated VPC/Aurora for this stage');
105
+ console.log(' ℹ Checking for shared KMS key...');
98
106
 
99
- // Still run KMS discovery - encryption keys are safe to share
100
- // Pass serviceName and stage to search for stage-specific alias
107
+ // KMS keys CAN be shared across stages (encryption keys are safe to reuse)
101
108
  const kmsDiscovery = new KmsDiscovery(provider);
102
109
  const kmsConfig = {
103
110
  serviceName: appDefinition.name || 'create-frigg-app',
@@ -108,12 +115,12 @@ async function gatherDiscoveredResources(appDefinition) {
108
115
 
109
116
  if (kmsResult?.defaultKmsKeyId) {
110
117
  console.log(' ✓ Found shared KMS key (can be reused across stages)');
111
- console.log('✅ Cloud resource discovery completed successfully!');
118
+ console.log('✅ Cloud resource discovery completed - will create isolated VPC/Aurora!');
112
119
  return kmsResult;
113
120
  }
114
121
 
115
122
  console.log(' ℹ No existing KMS key found - will create new one');
116
- console.log('✅ Cloud resource discovery completed successfully!');
123
+ console.log('✅ Cloud resource discovery completed - will create fresh isolated resources!');
117
124
  return {};
118
125
  }
119
126
 
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.461.77c8d12.0",
4
+ "version": "2.0.0--canary.461.5ebf96a.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.461.77c8d12.0",
15
- "@friggframework/test": "2.0.0--canary.461.77c8d12.0",
14
+ "@friggframework/schemas": "2.0.0--canary.461.5ebf96a.0",
15
+ "@friggframework/test": "2.0.0--canary.461.5ebf96a.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.461.77c8d12.0",
38
- "@friggframework/prettier-config": "2.0.0--canary.461.77c8d12.0",
37
+ "@friggframework/eslint-config": "2.0.0--canary.461.5ebf96a.0",
38
+ "@friggframework/prettier-config": "2.0.0--canary.461.5ebf96a.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",
@@ -70,5 +70,5 @@
70
70
  "publishConfig": {
71
71
  "access": "public"
72
72
  },
73
- "gitHead": "77c8d12f6a33bb2d66e19b43b6d96ee27d6f5e06"
73
+ "gitHead": "5ebf96a4b08d18a165c8451176b92705a1dcbb4f"
74
74
  }