@friggframework/devtools 2.0.0--canary.461.41b7566.0 → 2.0.0--canary.461.d1a7bbb.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.
@@ -430,13 +430,26 @@ class VpcBuilder extends InfrastructureBuilder {
430
430
  */
431
431
  async buildSubnets(appDefinition, discoveredResources, result, vpcManagement) {
432
432
  // Default subnet management depends on context:
433
+ // - Stack-managed subnets discovered: discover (reuse existing)
433
434
  // - use-existing mode with subnet IDs provided: use-existing
434
435
  // - create-new mode: create
435
- // - discover mode: create (for stage isolation)
436
+ // - discover mode without stack subnets: create (for stage isolation)
436
437
  let defaultSubnetManagement = 'create';
437
- if (vpcManagement === 'use-existing' && appDefinition.vpc.subnets?.ids?.length >= 2) {
438
+
439
+ // Check if stack-managed subnets were discovered from CloudFormation
440
+ // Only reuse if they're actual subnet IDs (strings), not CloudFormation Refs (objects)
441
+ const hasStackManagedSubnets =
442
+ discoveredResources?.privateSubnetId1 &&
443
+ discoveredResources?.privateSubnetId2 &&
444
+ typeof discoveredResources.privateSubnetId1 === 'string' &&
445
+ typeof discoveredResources.privateSubnetId2 === 'string';
446
+
447
+ if (hasStackManagedSubnets) {
448
+ defaultSubnetManagement = 'discover';
449
+ } else if (vpcManagement === 'use-existing' && appDefinition.vpc.subnets?.ids?.length >= 2) {
438
450
  defaultSubnetManagement = 'use-existing';
439
451
  }
452
+
440
453
  const subnetManagement = appDefinition.vpc.subnets?.management || defaultSubnetManagement;
441
454
 
442
455
  console.log(` Subnet Management Mode: ${subnetManagement} (default: ${defaultSubnetManagement}, explicit: ${appDefinition.vpc.subnets?.management})`);
@@ -241,7 +241,36 @@ describe('VpcBuilder', () => {
241
241
  });
242
242
 
243
243
  describe('build() - discover mode', () => {
244
- it('should use discovered VPC but create stage-specific subnets by default', async () => {
244
+ it('should reuse stack-managed subnets when discovered from CloudFormation', async () => {
245
+ const appDefinition = {
246
+ vpc: { enable: true },
247
+ };
248
+
249
+ const discoveredResources = {
250
+ defaultVpcId: 'vpc-discovered',
251
+ privateSubnetId1: 'subnet-stack-private-1',
252
+ privateSubnetId2: 'subnet-stack-private-2',
253
+ publicSubnetId1: 'subnet-stack-public-1',
254
+ publicSubnetId2: 'subnet-stack-public-2',
255
+ };
256
+
257
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
258
+
259
+ // Should use discovered VPC
260
+ expect(result.vpcId).toBe('vpc-discovered');
261
+
262
+ // Should reuse stack-managed subnets (not create new ones)
263
+ expect(result.vpcConfig.subnetIds).toEqual([
264
+ 'subnet-stack-private-1',
265
+ 'subnet-stack-private-2',
266
+ ]);
267
+
268
+ // Should NOT create new subnet resources
269
+ expect(result.resources.FriggPrivateSubnet1).toBeUndefined();
270
+ expect(result.resources.FriggPrivateSubnet2).toBeUndefined();
271
+ });
272
+
273
+ it('should use discovered VPC but create stage-specific subnets when no stack subnets exist', async () => {
245
274
  const appDefinition = {
246
275
  vpc: {
247
276
  enable: true,
@@ -251,15 +280,13 @@ describe('VpcBuilder', () => {
251
280
 
252
281
  const discoveredResources = {
253
282
  defaultVpcId: 'vpc-discovered',
254
- // Even though subnets are discovered, we should create new ones for stage isolation
255
- privateSubnetId1: 'subnet-private1',
256
- privateSubnetId2: 'subnet-private2',
283
+ // No stack-managed subnets, so create new ones for stage isolation
257
284
  defaultSecurityGroupId: 'sg-discovered',
258
285
  };
259
286
 
260
287
  const result = await vpcBuilder.build(appDefinition, discoveredResources);
261
288
 
262
- // NEW BEHAVIOR: Create stage-specific subnets for isolation (prevent route table conflicts)
289
+ // Should create new stage-specific subnets for isolation (prevent route table conflicts)
263
290
  expect(result.vpcConfig.subnetIds).toEqual([
264
291
  { Ref: 'FriggPrivateSubnet1' },
265
292
  { Ref: 'FriggPrivateSubnet2' },
@@ -164,6 +164,47 @@ class CloudFormationDiscovery {
164
164
  discovered.defaultKmsKeyId = PhysicalResourceId;
165
165
  }
166
166
  }
167
+
168
+ // Subnets
169
+ if (LogicalResourceId === 'FriggPrivateSubnet1' && ResourceType === 'AWS::EC2::Subnet') {
170
+ discovered.privateSubnetId1 = PhysicalResourceId;
171
+ }
172
+ if (LogicalResourceId === 'FriggPrivateSubnet2' && ResourceType === 'AWS::EC2::Subnet') {
173
+ discovered.privateSubnetId2 = PhysicalResourceId;
174
+ }
175
+ if (LogicalResourceId === 'FriggPublicSubnet' && ResourceType === 'AWS::EC2::Subnet') {
176
+ discovered.publicSubnetId1 = PhysicalResourceId;
177
+ }
178
+ if (LogicalResourceId === 'FriggPublicSubnet2' && ResourceType === 'AWS::EC2::Subnet') {
179
+ discovered.publicSubnetId2 = PhysicalResourceId;
180
+ }
181
+
182
+ // Route Tables
183
+ if (LogicalResourceId === 'FriggLambdaRouteTable' && ResourceType === 'AWS::EC2::RouteTable') {
184
+ discovered.routeTableId = PhysicalResourceId;
185
+ }
186
+
187
+ // VPC Endpoint Security Group
188
+ if (LogicalResourceId === 'FriggVPCEndpointSecurityGroup' && ResourceType === 'AWS::EC2::SecurityGroup') {
189
+ discovered.vpcEndpointSecurityGroupId = PhysicalResourceId;
190
+ }
191
+
192
+ // VPC Endpoints
193
+ if (LogicalResourceId === 'FriggS3VPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
194
+ discovered.s3VpcEndpointId = PhysicalResourceId;
195
+ }
196
+ if (LogicalResourceId === 'FriggDynamoDBVPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
197
+ discovered.dynamoDbVpcEndpointId = PhysicalResourceId;
198
+ }
199
+ if (LogicalResourceId === 'FriggKMSVPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
200
+ discovered.kmsVpcEndpointId = PhysicalResourceId;
201
+ }
202
+ if (LogicalResourceId === 'FriggSecretsManagerVPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
203
+ discovered.secretsManagerVpcEndpointId = PhysicalResourceId;
204
+ }
205
+ if (LogicalResourceId === 'FriggSQSVPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
206
+ discovered.sqsVpcEndpointId = PhysicalResourceId;
207
+ }
167
208
  }
168
209
  }
169
210
  }
@@ -73,6 +73,50 @@ describe('CloudFormationDiscovery', () => {
73
73
  });
74
74
  });
75
75
 
76
+ it('should extract VPC subnets from stack resources', async () => {
77
+ const mockStack = { StackName: 'test-stack', Outputs: [] };
78
+ const mockResources = [
79
+ { LogicalResourceId: 'FriggPrivateSubnet1', PhysicalResourceId: 'subnet-priv-1', ResourceType: 'AWS::EC2::Subnet' },
80
+ { LogicalResourceId: 'FriggPrivateSubnet2', PhysicalResourceId: 'subnet-priv-2', ResourceType: 'AWS::EC2::Subnet' },
81
+ { LogicalResourceId: 'FriggPublicSubnet', PhysicalResourceId: 'subnet-pub-1', ResourceType: 'AWS::EC2::Subnet' },
82
+ { LogicalResourceId: 'FriggPublicSubnet2', PhysicalResourceId: 'subnet-pub-2', ResourceType: 'AWS::EC2::Subnet' },
83
+ ];
84
+
85
+ mockProvider.describeStack.mockResolvedValue(mockStack);
86
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
87
+
88
+ const result = await cfDiscovery.discoverFromStack('test-stack');
89
+
90
+ expect(result.privateSubnetId1).toBe('subnet-priv-1');
91
+ expect(result.privateSubnetId2).toBe('subnet-priv-2');
92
+ expect(result.publicSubnetId1).toBe('subnet-pub-1');
93
+ expect(result.publicSubnetId2).toBe('subnet-pub-2');
94
+ });
95
+
96
+ it('should extract route tables and VPC endpoints from stack resources', async () => {
97
+ const mockStack = { StackName: 'test-stack', Outputs: [] };
98
+ const mockResources = [
99
+ { LogicalResourceId: 'FriggLambdaRouteTable', PhysicalResourceId: 'rtb-123', ResourceType: 'AWS::EC2::RouteTable' },
100
+ { LogicalResourceId: 'FriggVPCEndpointSecurityGroup', PhysicalResourceId: 'sg-vpce-123', ResourceType: 'AWS::EC2::SecurityGroup' },
101
+ { LogicalResourceId: 'FriggS3VPCEndpoint', PhysicalResourceId: 'vpce-s3-123', ResourceType: 'AWS::EC2::VPCEndpoint' },
102
+ { LogicalResourceId: 'FriggDynamoDBVPCEndpoint', PhysicalResourceId: 'vpce-ddb-123', ResourceType: 'AWS::EC2::VPCEndpoint' },
103
+ { LogicalResourceId: 'FriggKMSVPCEndpoint', PhysicalResourceId: 'vpce-kms-123', ResourceType: 'AWS::EC2::VPCEndpoint' },
104
+ { LogicalResourceId: 'FriggSecretsManagerVPCEndpoint', PhysicalResourceId: 'vpce-sm-123', ResourceType: 'AWS::EC2::VPCEndpoint' },
105
+ ];
106
+
107
+ mockProvider.describeStack.mockResolvedValue(mockStack);
108
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
109
+
110
+ const result = await cfDiscovery.discoverFromStack('test-stack');
111
+
112
+ expect(result.routeTableId).toBe('rtb-123');
113
+ expect(result.vpcEndpointSecurityGroupId).toBe('sg-vpce-123');
114
+ expect(result.s3VpcEndpointId).toBe('vpce-s3-123');
115
+ expect(result.dynamoDbVpcEndpointId).toBe('vpce-ddb-123');
116
+ expect(result.kmsVpcEndpointId).toBe('vpce-kms-123');
117
+ expect(result.secretsManagerVpcEndpointId).toBe('vpce-sm-123');
118
+ });
119
+
76
120
  it('should extract Aurora cluster from stack resources', async () => {
77
121
  const mockStack = {
78
122
  StackName: 'test-stack',
@@ -97,6 +141,46 @@ describe('CloudFormationDiscovery', () => {
97
141
  });
98
142
  });
99
143
 
144
+ it('should extract subnets from stack resources', async () => {
145
+ const mockStack = {
146
+ StackName: 'test-stack',
147
+ Outputs: [],
148
+ };
149
+
150
+ const mockResources = [
151
+ {
152
+ LogicalResourceId: 'FriggPrivateSubnet1',
153
+ PhysicalResourceId: 'subnet-private-1',
154
+ ResourceType: 'AWS::EC2::Subnet',
155
+ },
156
+ {
157
+ LogicalResourceId: 'FriggPrivateSubnet2',
158
+ PhysicalResourceId: 'subnet-private-2',
159
+ ResourceType: 'AWS::EC2::Subnet',
160
+ },
161
+ {
162
+ LogicalResourceId: 'FriggPublicSubnet',
163
+ PhysicalResourceId: 'subnet-public-1',
164
+ ResourceType: 'AWS::EC2::Subnet',
165
+ },
166
+ {
167
+ LogicalResourceId: 'FriggPublicSubnet2',
168
+ PhysicalResourceId: 'subnet-public-2',
169
+ ResourceType: 'AWS::EC2::Subnet',
170
+ },
171
+ ];
172
+
173
+ mockProvider.describeStack.mockResolvedValue(mockStack);
174
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
175
+
176
+ const result = await cfDiscovery.discoverFromStack('test-stack');
177
+
178
+ expect(result.privateSubnetId1).toBe('subnet-private-1');
179
+ expect(result.privateSubnetId2).toBe('subnet-private-2');
180
+ expect(result.publicSubnetId1).toBe('subnet-public-1');
181
+ expect(result.publicSubnetId2).toBe('subnet-public-2');
182
+ });
183
+
100
184
  it('should extract S3 migration bucket from stack resources', async () => {
101
185
  const mockStack = {
102
186
  StackName: 'test-stack',
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.41b7566.0",
4
+ "version": "2.0.0--canary.461.d1a7bbb.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.41b7566.0",
15
- "@friggframework/test": "2.0.0--canary.461.41b7566.0",
14
+ "@friggframework/schemas": "2.0.0--canary.461.d1a7bbb.0",
15
+ "@friggframework/test": "2.0.0--canary.461.d1a7bbb.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.41b7566.0",
38
- "@friggframework/prettier-config": "2.0.0--canary.461.41b7566.0",
37
+ "@friggframework/eslint-config": "2.0.0--canary.461.d1a7bbb.0",
38
+ "@friggframework/prettier-config": "2.0.0--canary.461.d1a7bbb.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": "41b7566abaa46939705d52e01904022e52cdd06a"
73
+ "gitHead": "d1a7bbb21e49abe96c803b1f3433e8d3bbeaa478"
74
74
  }