@friggframework/devtools 2.0.0--canary.487.63ed8db.0 → 2.0.0--canary.490.7d57f02.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.
@@ -111,6 +111,74 @@ class CloudFormationDiscovery {
111
111
  }
112
112
  }
113
113
 
114
+ /**
115
+ * Extract external resource references from stack resource properties
116
+ *
117
+ * When VPC/subnets/NAT are external, they're referenced in routing resources' properties.
118
+ * We query EC2 to get the actual VPC ID, NAT Gateway ID, and subnet IDs from the route table.
119
+ *
120
+ * @private
121
+ * @param {string} stackName - Stack name
122
+ * @param {Array} resources - CloudFormation stack resources
123
+ * @param {Object} discovered - Object to populate with discovered resources
124
+ */
125
+ async _extractExternalReferencesFromStackResources(stackName, resources, discovered) {
126
+ if (!this.provider || !this.provider.getEC2Client) {
127
+ console.log(' ℹ Skipping external reference extraction (EC2 client not available)');
128
+ return;
129
+ }
130
+
131
+ try {
132
+ // If we found a route table in the stack, query EC2 for its details
133
+ // This gives us VPC ID, NAT Gateway ID, and subnet IDs
134
+ if (discovered.routeTableId) {
135
+ try {
136
+ console.log(` ℹ Querying route table ${discovered.routeTableId} for external references...`);
137
+ const { DescribeRouteTablesCommand } = require('@aws-sdk/client-ec2');
138
+ const ec2 = this.provider.getEC2Client();
139
+ const rtResponse = await ec2.send(new DescribeRouteTablesCommand({
140
+ RouteTableIds: [discovered.routeTableId]
141
+ }));
142
+
143
+ if (rtResponse.RouteTables && rtResponse.RouteTables.length > 0) {
144
+ const routeTable = rtResponse.RouteTables[0];
145
+
146
+ // Extract VPC ID
147
+ if (routeTable.VpcId && !discovered.defaultVpcId) {
148
+ discovered.defaultVpcId = routeTable.VpcId;
149
+ console.log(` ✓ Extracted VPC ID from route table: ${routeTable.VpcId}`);
150
+ }
151
+
152
+ // Extract NAT Gateway ID from routes
153
+ const natRoute = routeTable.Routes?.find(r => r.NatGatewayId);
154
+ if (natRoute && natRoute.NatGatewayId && !discovered.natGatewayId) {
155
+ discovered.natGatewayId = natRoute.NatGatewayId;
156
+ discovered.existingNatGatewayId = natRoute.NatGatewayId;
157
+ console.log(` ✓ Extracted NAT Gateway ID from routes: ${natRoute.NatGatewayId}`);
158
+ }
159
+
160
+ // Extract subnet IDs from route table associations
161
+ const associations = routeTable.Associations || [];
162
+ const subnetAssociations = associations.filter(a => a.SubnetId);
163
+
164
+ if (subnetAssociations.length >= 1 && !discovered.privateSubnetId1) {
165
+ discovered.privateSubnetId1 = subnetAssociations[0].SubnetId;
166
+ console.log(` ✓ Extracted private subnet 1 from associations: ${subnetAssociations[0].SubnetId}`);
167
+ }
168
+ if (subnetAssociations.length >= 2 && !discovered.privateSubnetId2) {
169
+ discovered.privateSubnetId2 = subnetAssociations[1].SubnetId;
170
+ console.log(` ✓ Extracted private subnet 2 from associations: ${subnetAssociations[1].SubnetId}`);
171
+ }
172
+ }
173
+ } catch (error) {
174
+ console.warn(` ⚠️ Could not query route table for external references: ${error.message}`);
175
+ }
176
+ }
177
+ } catch (error) {
178
+ console.warn(` ⚠️ Error extracting external references: ${error.message}`);
179
+ }
180
+ }
181
+
114
182
  /**
115
183
  * Extract discovered resources from CloudFormation stack resources
116
184
  *
@@ -216,6 +284,27 @@ class CloudFormationDiscovery {
216
284
  discovered.natGatewayId = PhysicalResourceId;
217
285
  }
218
286
 
287
+ // Route Table (Lambda route table for external VPC pattern)
288
+ if (LogicalResourceId === 'FriggLambdaRouteTable' && ResourceType === 'AWS::EC2::RouteTable') {
289
+ discovered.routeTableId = PhysicalResourceId;
290
+ discovered.privateRouteTableId = PhysicalResourceId;
291
+ console.log(` ✓ Found route table in stack: ${PhysicalResourceId}`);
292
+ }
293
+
294
+ // NAT Route (proves NAT configuration exists)
295
+ if (LogicalResourceId === 'FriggNATRoute' && ResourceType === 'AWS::EC2::Route') {
296
+ discovered.natRoute = PhysicalResourceId;
297
+ console.log(` ✓ Found NAT route in stack`);
298
+ }
299
+
300
+ // Route Table Associations (links subnets to route table)
301
+ if (LogicalResourceId.includes('RouteAssociation') && ResourceType === 'AWS::EC2::SubnetRouteTableAssociation') {
302
+ if (!discovered.routeTableAssociations) {
303
+ discovered.routeTableAssociations = [];
304
+ }
305
+ discovered.routeTableAssociations.push(PhysicalResourceId);
306
+ }
307
+
219
308
  // VPC - direct extraction (primary method)
220
309
  if (LogicalResourceId === 'FriggVPC' && ResourceType === 'AWS::EC2::VPC') {
221
310
  discovered.defaultVpcId = PhysicalResourceId;
@@ -278,24 +367,53 @@ class CloudFormationDiscovery {
278
367
  discovered.vpcEndpointSecurityGroupId = PhysicalResourceId;
279
368
  }
280
369
 
281
- // VPC Endpoints
282
- if (LogicalResourceId === 'FriggS3VPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
370
+ // VPC Endpoints - support both old and new naming conventions
371
+ // Initialize vpcEndpoints object for structured access
372
+ if (!discovered.vpcEndpoints) {
373
+ discovered.vpcEndpoints = {};
374
+ }
375
+
376
+ // S3 Endpoint (both naming patterns)
377
+ if ((LogicalResourceId === 'FriggS3VPCEndpoint' || LogicalResourceId === 'VPCEndpointS3') &&
378
+ ResourceType === 'AWS::EC2::VPCEndpoint') {
283
379
  discovered.s3VpcEndpointId = PhysicalResourceId;
380
+ discovered.vpcEndpoints.s3 = PhysicalResourceId;
381
+ console.log(` ✓ Found S3 VPC endpoint in stack: ${PhysicalResourceId}`);
284
382
  }
285
- if (LogicalResourceId === 'FriggDynamoDBVPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
383
+
384
+ // DynamoDB Endpoint (both naming patterns)
385
+ if ((LogicalResourceId === 'FriggDynamoDBVPCEndpoint' || LogicalResourceId === 'VPCEndpointDynamoDB') &&
386
+ ResourceType === 'AWS::EC2::VPCEndpoint') {
286
387
  discovered.dynamoDbVpcEndpointId = PhysicalResourceId;
388
+ discovered.vpcEndpoints.dynamodb = PhysicalResourceId;
389
+ console.log(` ✓ Found DynamoDB VPC endpoint in stack: ${PhysicalResourceId}`);
287
390
  }
288
- if (LogicalResourceId === 'FriggKMSVPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
391
+
392
+ // KMS Endpoint (both naming patterns)
393
+ if ((LogicalResourceId === 'FriggKMSVPCEndpoint' || LogicalResourceId === 'VPCEndpointKMS') &&
394
+ ResourceType === 'AWS::EC2::VPCEndpoint') {
289
395
  discovered.kmsVpcEndpointId = PhysicalResourceId;
396
+ discovered.vpcEndpoints.kms = PhysicalResourceId;
397
+ console.log(` ✓ Found KMS VPC endpoint in stack: ${PhysicalResourceId}`);
290
398
  }
399
+
400
+ // Secrets Manager Endpoint
291
401
  if (LogicalResourceId === 'FriggSecretsManagerVPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
292
402
  discovered.secretsManagerVpcEndpointId = PhysicalResourceId;
403
+ discovered.vpcEndpoints.secretsManager = PhysicalResourceId;
293
404
  }
405
+
406
+ // SQS Endpoint
294
407
  if (LogicalResourceId === 'FriggSQSVPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
295
408
  discovered.sqsVpcEndpointId = PhysicalResourceId;
409
+ discovered.vpcEndpoints.sqs = PhysicalResourceId;
296
410
  }
297
411
  }
298
412
 
413
+ // Extract VPC ID and other external references from routing resource properties
414
+ // This handles the pattern where VPC is external but routing is in the stack
415
+ await this._extractExternalReferencesFromStackResources(stackName, resources, discovered);
416
+
299
417
  // If we have a VPC ID but no subnet IDs, query EC2 for Frigg-managed subnets
300
418
  if (discovered.defaultVpcId && this.provider &&
301
419
  !discovered.privateSubnetId1 && !discovered.publicSubnetId1) {
@@ -586,5 +586,113 @@ describe('CloudFormationDiscovery', () => {
586
586
  expect(mockProvider.describeKmsKey).toHaveBeenCalledWith('alias/test-service-dev-frigg-kms');
587
587
  });
588
588
  });
589
+
590
+ describe('External VPC with routing infrastructure pattern', () => {
591
+ it('should discover routing resources when VPC is external', async () => {
592
+ // This tests the Frontify pattern: external VPC/subnets/KMS,
593
+ // but stack creates routing infrastructure (route table, NAT route, VPC endpoints)
594
+ const mockStack = {
595
+ StackName: 'create-frigg-app-production',
596
+ Outputs: [],
597
+ };
598
+
599
+ const mockResources = [
600
+ {
601
+ LogicalResourceId: 'FriggLambdaRouteTable',
602
+ PhysicalResourceId: 'rtb-0b83aca77ccde20a6',
603
+ ResourceType: 'AWS::EC2::RouteTable',
604
+ ResourceStatus: 'UPDATE_COMPLETE',
605
+ },
606
+ {
607
+ LogicalResourceId: 'FriggNATRoute',
608
+ PhysicalResourceId: 'rtb-0b83aca77ccde20a6|0.0.0.0/0',
609
+ ResourceType: 'AWS::EC2::Route',
610
+ ResourceStatus: 'UPDATE_COMPLETE',
611
+ },
612
+ {
613
+ LogicalResourceId: 'FriggSubnet1RouteAssociation',
614
+ PhysicalResourceId: 'rtbassoc-07245da0b447ca469',
615
+ ResourceType: 'AWS::EC2::SubnetRouteTableAssociation',
616
+ ResourceStatus: 'CREATE_COMPLETE',
617
+ },
618
+ {
619
+ LogicalResourceId: 'FriggSubnet2RouteAssociation',
620
+ PhysicalResourceId: 'rtbassoc-0806f9783c4ea181f',
621
+ ResourceType: 'AWS::EC2::SubnetRouteTableAssociation',
622
+ ResourceStatus: 'CREATE_COMPLETE',
623
+ },
624
+ {
625
+ LogicalResourceId: 'VPCEndpointS3',
626
+ PhysicalResourceId: 'vpce-0352ceac2124c14be',
627
+ ResourceType: 'AWS::EC2::VPCEndpoint',
628
+ ResourceStatus: 'CREATE_COMPLETE',
629
+ },
630
+ {
631
+ LogicalResourceId: 'VPCEndpointDynamoDB',
632
+ PhysicalResourceId: 'vpce-0b06c4f631199ea68',
633
+ ResourceType: 'AWS::EC2::VPCEndpoint',
634
+ ResourceStatus: 'CREATE_COMPLETE',
635
+ },
636
+ ];
637
+
638
+ mockProvider.describeStack.mockResolvedValue(mockStack);
639
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
640
+
641
+ const result = await cfDiscovery.discoverFromStack('create-frigg-app-production');
642
+
643
+ // Verify routing infrastructure was discovered
644
+ expect(result.routeTableId).toBe('rtb-0b83aca77ccde20a6');
645
+ expect(result.privateRouteTableId).toBe('rtb-0b83aca77ccde20a6');
646
+ expect(result.natRoute).toBe('rtb-0b83aca77ccde20a6|0.0.0.0/0');
647
+ expect(result.routeTableAssociations).toEqual([
648
+ 'rtbassoc-07245da0b447ca469',
649
+ 'rtbassoc-0806f9783c4ea181f',
650
+ ]);
651
+
652
+ // Verify VPC endpoints were discovered (both naming conventions)
653
+ expect(result.vpcEndpoints).toBeDefined();
654
+ expect(result.vpcEndpoints.s3).toBe('vpce-0352ceac2124c14be');
655
+ expect(result.vpcEndpoints.dynamodb).toBe('vpce-0b06c4f631199ea68');
656
+ expect(result.s3VpcEndpointId).toBe('vpce-0352ceac2124c14be');
657
+ expect(result.dynamoDbVpcEndpointId).toBe('vpce-0b06c4f631199ea68');
658
+
659
+ // Verify NO VPC/KMS resources (they're external)
660
+ expect(result.defaultVpcId).toBeUndefined();
661
+ expect(result.defaultKmsKeyId).toBeUndefined();
662
+ });
663
+
664
+ it('should work with legacy VPC endpoint naming (FriggS3VPCEndpoint)', async () => {
665
+ const mockStack = {
666
+ StackName: 'test-stack',
667
+ Outputs: [],
668
+ };
669
+
670
+ const mockResources = [
671
+ {
672
+ LogicalResourceId: 'FriggS3VPCEndpoint',
673
+ PhysicalResourceId: 'vpce-legacy-s3',
674
+ ResourceType: 'AWS::EC2::VPCEndpoint',
675
+ ResourceStatus: 'CREATE_COMPLETE',
676
+ },
677
+ {
678
+ LogicalResourceId: 'FriggDynamoDBVPCEndpoint',
679
+ PhysicalResourceId: 'vpce-legacy-ddb',
680
+ ResourceType: 'AWS::EC2::VPCEndpoint',
681
+ ResourceStatus: 'CREATE_COMPLETE',
682
+ },
683
+ ];
684
+
685
+ mockProvider.describeStack.mockResolvedValue(mockStack);
686
+ mockProvider.listStackResources.mockResolvedValue(mockResources);
687
+
688
+ const result = await cfDiscovery.discoverFromStack('test-stack');
689
+
690
+ // Both naming conventions should work
691
+ expect(result.vpcEndpoints.s3).toBe('vpce-legacy-s3');
692
+ expect(result.vpcEndpoints.dynamodb).toBe('vpce-legacy-ddb');
693
+ expect(result.s3VpcEndpointId).toBe('vpce-legacy-s3');
694
+ expect(result.dynamoDbVpcEndpointId).toBe('vpce-legacy-ddb');
695
+ });
696
+ });
589
697
  });
590
698
 
@@ -536,6 +536,29 @@ class AWSProviderAdapter extends CloudProviderAdapter {
536
536
  return [];
537
537
  }
538
538
  }
539
+
540
+ /**
541
+ * Describe a specific stack resource to get its full details including properties
542
+ * @param {string} stackName - Stack name
543
+ * @param {string} logicalResourceId - Logical resource ID
544
+ * @returns {Promise<Object>} Resource details
545
+ */
546
+ async describeStackResource(stackName, logicalResourceId) {
547
+ const cf = this.getCloudFormationClient();
548
+
549
+ try {
550
+ const { DescribeStackResourceCommand } = require('@aws-sdk/client-cloudformation');
551
+ const response = await cf.send(new DescribeStackResourceCommand({
552
+ StackName: stackName,
553
+ LogicalResourceId: logicalResourceId,
554
+ }));
555
+
556
+ return response.StackResourceDetail || null;
557
+ } catch (error) {
558
+ console.warn(`Failed to describe stack resource ${logicalResourceId}:`, error.message);
559
+ return null;
560
+ }
561
+ }
539
562
  }
540
563
 
541
564
  module.exports = {
@@ -95,11 +95,23 @@ async function gatherDiscoveredResources(appDefinition) {
95
95
  const cfDiscovery = new CloudFormationDiscovery(provider, { serviceName, stage });
96
96
  const stackResources = await cfDiscovery.discoverFromStack(stackName);
97
97
 
98
- // Validate CF discovery results - only use if contains useful data
99
- const hasVpcData = stackResources?.defaultVpcId;
100
- const hasKmsData = stackResources?.defaultKmsKeyId;
101
- const hasAuroraData = stackResources?.auroraClusterId;
102
- const hasSomeUsefulData = hasVpcData || hasKmsData || hasAuroraData;
98
+ // Validate CF discovery results - check for ANY useful infrastructure
99
+ const hasVpcData = stackResources?.defaultVpcId; // VPC resource in stack
100
+ const hasKmsData = stackResources?.defaultKmsKeyId; // KMS resource in stack
101
+ const hasAuroraData = stackResources?.auroraClusterId; // Aurora in stack
102
+
103
+ // Check for routing infrastructure (proves VPC config exists even with external VPC)
104
+ const hasRoutingInfra = stackResources?.routeTableId || // FriggLambdaRouteTable
105
+ stackResources?.natRoute || // FriggNATRoute
106
+ stackResources?.vpcEndpoints?.s3 || // VPC endpoints
107
+ stackResources?.vpcEndpoints?.dynamodb;
108
+
109
+ // Stack is useful if it has EITHER actual resources OR routing infrastructure
110
+ const hasSomeUsefulData = hasVpcData || hasKmsData || hasAuroraData || hasRoutingInfra;
111
+
112
+ if (hasRoutingInfra && !hasVpcData) {
113
+ console.log(' ✓ Found VPC routing infrastructure in stack (external VPC pattern)');
114
+ }
103
115
 
104
116
  // Check if we're in isolated mode (each stage gets its own VPC/Aurora)
105
117
  const isIsolatedMode = appDefinition.managementMode === 'managed' &&
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.487.63ed8db.0",
4
+ "version": "2.0.0--canary.490.7d57f02.0",
5
5
  "bin": {
6
6
  "frigg": "./frigg-cli/index.js"
7
7
  },
@@ -16,9 +16,9 @@
16
16
  "@babel/eslint-parser": "^7.18.9",
17
17
  "@babel/parser": "^7.25.3",
18
18
  "@babel/traverse": "^7.25.3",
19
- "@friggframework/core": "2.0.0--canary.487.63ed8db.0",
20
- "@friggframework/schemas": "2.0.0--canary.487.63ed8db.0",
21
- "@friggframework/test": "2.0.0--canary.487.63ed8db.0",
19
+ "@friggframework/core": "2.0.0--canary.490.7d57f02.0",
20
+ "@friggframework/schemas": "2.0.0--canary.490.7d57f02.0",
21
+ "@friggframework/test": "2.0.0--canary.490.7d57f02.0",
22
22
  "@hapi/boom": "^10.0.1",
23
23
  "@inquirer/prompts": "^5.3.8",
24
24
  "axios": "^1.7.2",
@@ -46,8 +46,8 @@
46
46
  "validate-npm-package-name": "^5.0.0"
47
47
  },
48
48
  "devDependencies": {
49
- "@friggframework/eslint-config": "2.0.0--canary.487.63ed8db.0",
50
- "@friggframework/prettier-config": "2.0.0--canary.487.63ed8db.0",
49
+ "@friggframework/eslint-config": "2.0.0--canary.490.7d57f02.0",
50
+ "@friggframework/prettier-config": "2.0.0--canary.490.7d57f02.0",
51
51
  "aws-sdk-client-mock": "^4.1.0",
52
52
  "aws-sdk-client-mock-jest": "^4.1.0",
53
53
  "jest": "^30.1.3",
@@ -79,5 +79,5 @@
79
79
  "publishConfig": {
80
80
  "access": "public"
81
81
  },
82
- "gitHead": "63ed8db39a59f2be928d8255be6b4afde423123f"
82
+ "gitHead": "7d57f02625ea51ce9dadd5e1535aac1c4491b9da"
83
83
  }