@friggframework/devtools 2.0.0--canary.490.c5ad260.0 → 2.0.0--canary.492.669f13d.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.
@@ -149,7 +149,7 @@ class VpcBuilder extends InfrastructureBuilder {
149
149
  physicalId = flatDiscovery.defaultVpcId;
150
150
  } else if (logicalId === 'FriggLambdaSecurityGroup') {
151
151
  resourceType = 'AWS::EC2::SecurityGroup';
152
- physicalId = flatDiscovery.lambdaSecurityGroupId || flatDiscovery.defaultSecurityGroupId || flatDiscovery.securityGroupId;
152
+ physicalId = flatDiscovery.defaultSecurityGroupId || flatDiscovery.securityGroupId;
153
153
  } else if (logicalId === 'FriggPrivateSubnet1') {
154
154
  resourceType = 'AWS::EC2::Subnet';
155
155
  physicalId = flatDiscovery.privateSubnetId1;
@@ -23,7 +23,6 @@ class VpcResourceResolver extends BaseResourceResolver {
23
23
  */
24
24
  resolveVpc(appDefinition, discovery) {
25
25
  const userIntent = appDefinition.vpc?.ownership?.vpc || 'auto';
26
- const vpcManagement = appDefinition.vpc?.management; // Legacy config
27
26
 
28
27
  // Explicit external
29
28
  if (userIntent === 'external') {
@@ -44,35 +43,20 @@ class VpcResourceResolver extends BaseResourceResolver {
44
43
  }
45
44
 
46
45
  // Auto-decide
47
- const decision = this.resolveResourceOwnership(
46
+ return this.resolveResourceOwnership(
48
47
  'auto',
49
48
  'FriggVPC',
50
49
  'AWS::EC2::VPC',
51
50
  discovery
52
51
  );
53
-
54
- // CRITICAL: If auto-resolution wants to create a VPC but management mode is 'discover' (or undefined),
55
- // throw an error instead. Creating a VPC is expensive and should be explicit.
56
- if (decision.ownership === ResourceOwnership.STACK &&
57
- !decision.physicalId &&
58
- vpcManagement !== 'create-new' &&
59
- userIntent === 'auto') {
60
- throw new Error(
61
- 'VPC discovery failed: No VPC found. ' +
62
- 'Either set vpc.management to "create-new" or provide vpc.vpcId with vpc.management "use-existing".'
63
- );
64
- }
65
-
66
- return decision;
67
52
  }
68
53
 
69
54
  /**
70
55
  * Resolve Security Group ownership
71
56
  *
72
- * Logic:
73
- * - If FriggLambdaSecurityGroup exists in stack STACK (keep it)
74
- * - If default SG discovered from VPC EXTERNAL (use it)
75
- * - Otherwise → STACK (create FriggLambdaSecurityGroup)
57
+ * Special logic: We ALWAYS create our own FriggLambdaSecurityGroup with specific
58
+ * rules unless the user explicitly provides external SG IDs. The discovered
59
+ * defaultSecurityGroupId is the VPC's default SG, but we need our own Lambda SG.
76
60
  *
77
61
  * @param {Object} appDefinition - App definition
78
62
  * @param {Object} discovery - Discovery result
@@ -81,7 +65,7 @@ class VpcResourceResolver extends BaseResourceResolver {
81
65
  resolveSecurityGroup(appDefinition, discovery) {
82
66
  const userIntent = appDefinition.vpc?.ownership?.securityGroup || 'auto';
83
67
 
84
- // Explicit external - use provided SG IDs
68
+ // Explicit external - only use external SGs if user explicitly provides them
85
69
  if (userIntent === 'external') {
86
70
  this.requireExternalIds(
87
71
  appDefinition.vpc?.external?.securityGroupIds,
@@ -93,52 +77,21 @@ class VpcResourceResolver extends BaseResourceResolver {
93
77
  );
94
78
  }
95
79
 
96
- // Explicit stack - always create FriggLambdaSecurityGroup
97
- if (userIntent === 'stack') {
98
- const inStack = this.findInStack('FriggLambdaSecurityGroup', discovery);
99
- return this.createStackDecision(
100
- inStack?.physicalId,
101
- inStack
102
- ? 'Found FriggLambdaSecurityGroup in CloudFormation stack'
103
- : 'User specified ownership=stack - will create FriggLambdaSecurityGroup'
104
- );
105
- }
106
-
107
- // Auto mode: Check stack first, then check for discovered default SG
80
+ // For stack or auto: check if FriggLambdaSecurityGroup exists in stack
81
+ // If it does, reuse it. If not, create it. Never use discovered default SG.
108
82
  const inStack = this.findInStack('FriggLambdaSecurityGroup', discovery);
109
83
 
110
84
  if (inStack) {
111
85
  return this.createStackDecision(
112
86
  inStack.physicalId,
113
- 'Found FriggLambdaSecurityGroup in CloudFormation stack - must keep in template'
114
- );
115
- }
116
-
117
- // Also check flat discovery for lambdaSecurityGroupId (from CloudFormation extraction)
118
- const structured = discovery._structured || discovery;
119
- const lambdaSgId = structured.lambdaSecurityGroupId || discovery.lambdaSecurityGroupId;
120
-
121
- if (lambdaSgId) {
122
- return this.createStackDecision(
123
- lambdaSgId,
124
- 'Found FriggLambdaSecurityGroup in CloudFormation stack - must keep in template'
125
- );
126
- }
127
-
128
- // Check for discovered default security group (from external VPC pattern)
129
- const defaultSgId = structured.defaultSecurityGroupId || discovery.defaultSecurityGroupId;
130
-
131
- if (defaultSgId) {
132
- return this.createExternalDecision(
133
- [defaultSgId],
134
- 'Found default security group via discovery - will reuse (matches canary behavior)'
87
+ 'Found FriggLambdaSecurityGroup in CloudFormation stack'
135
88
  );
136
89
  }
137
90
 
138
- // No SG found anywhere - create new FriggLambdaSecurityGroup
91
+ // Create new FriggLambdaSecurityGroup in stack
139
92
  return this.createStackDecision(
140
93
  null,
141
- 'No security group found - will create FriggLambdaSecurityGroup in stack'
94
+ 'No existing FriggLambdaSecurityGroup - will create in stack'
142
95
  );
143
96
  }
144
97
 
@@ -109,46 +109,6 @@ describe('VpcResourceResolver', () => {
109
109
  expect(decision.physicalId).toBeUndefined();
110
110
  expect(decision.reason).toContain('No existing resource found');
111
111
  });
112
-
113
- it('should throw error when auto mode finds no VPC and management is not create-new', () => {
114
- const appDefinition = {
115
- vpc: {
116
- enable: true,
117
- // No management specified - defaults to discover
118
- // No ownership specified - defaults to auto
119
- }
120
- };
121
- const discovery = {
122
- stackManaged: [],
123
- external: [],
124
- fromCloudFormation: false
125
- };
126
-
127
- // Should throw error instead of trying to create VPC
128
- expect(() => resolver.resolveVpc(appDefinition, discovery)).toThrow(
129
- 'VPC discovery failed: No VPC found'
130
- );
131
- });
132
-
133
- it('should allow creating VPC when management is create-new', () => {
134
- const appDefinition = {
135
- vpc: {
136
- enable: true,
137
- management: 'create-new'
138
- }
139
- };
140
- const discovery = {
141
- stackManaged: [],
142
- external: [],
143
- fromCloudFormation: false
144
- };
145
-
146
- // Should NOT throw - create-new explicitly allows VPC creation
147
- const decision = resolver.resolveVpc(appDefinition, discovery);
148
-
149
- expect(decision.ownership).toBe(ResourceOwnership.STACK);
150
- expect(decision.physicalId).toBeNull();
151
- });
152
112
  });
153
113
 
154
114
  describe('resolveSecurityGroup', () => {
@@ -28,9 +28,6 @@ class CloudFormationDiscovery {
28
28
  */
29
29
  async discoverFromStack(stackName) {
30
30
  try {
31
- // Store stack name for use in helper methods
32
- this.currentStackName = stackName;
33
-
34
31
  // Try to get the stack
35
32
  const stack = await this.provider.describeStack(stackName);
36
33
 
@@ -114,93 +111,6 @@ class CloudFormationDiscovery {
114
111
  }
115
112
  }
116
113
 
117
- /**
118
- * Extract external resource references from stack resource properties
119
- *
120
- * When VPC/subnets/NAT are external, they're referenced in routing resources' properties.
121
- * We query EC2 to get the actual VPC ID, NAT Gateway ID, and subnet IDs from the route table.
122
- *
123
- * @private
124
- * @param {Array} resources - CloudFormation stack resources
125
- * @param {Object} discovered - Object to populate with discovered resources
126
- */
127
- async _extractExternalReferencesFromStackResources(resources, discovered) {
128
- if (!this.provider || !this.provider.getEC2Client) {
129
- console.log(' ℹ Skipping external reference extraction (EC2 client not available)');
130
- return;
131
- }
132
-
133
- try {
134
- // If we found a route table in the stack, query EC2 for its details
135
- // This gives us VPC ID, NAT Gateway ID, and subnet IDs
136
- if (discovered.routeTableId) {
137
- try {
138
- console.log(` ℹ Querying route table ${discovered.routeTableId} for external references...`);
139
- const { DescribeRouteTablesCommand } = require('@aws-sdk/client-ec2');
140
- const ec2 = this.provider.getEC2Client();
141
- const rtResponse = await ec2.send(new DescribeRouteTablesCommand({
142
- RouteTableIds: [discovered.routeTableId]
143
- }));
144
-
145
- if (rtResponse.RouteTables && rtResponse.RouteTables.length > 0) {
146
- const routeTable = rtResponse.RouteTables[0];
147
-
148
- // Extract VPC ID
149
- if (routeTable.VpcId && !discovered.defaultVpcId) {
150
- discovered.defaultVpcId = routeTable.VpcId;
151
- console.log(` ✓ Extracted VPC ID from route table: ${routeTable.VpcId}`);
152
- }
153
-
154
- // Extract NAT Gateway ID from routes
155
- const natRoute = routeTable.Routes?.find(r => r.NatGatewayId);
156
- if (natRoute && natRoute.NatGatewayId && !discovered.natGatewayId) {
157
- discovered.natGatewayId = natRoute.NatGatewayId;
158
- discovered.existingNatGatewayId = natRoute.NatGatewayId;
159
- console.log(` ✓ Extracted NAT Gateway ID from routes: ${natRoute.NatGatewayId}`);
160
- }
161
-
162
- // Extract subnet IDs from route table associations
163
- const associations = routeTable.Associations || [];
164
- const subnetAssociations = associations.filter(a => a.SubnetId);
165
-
166
- if (subnetAssociations.length >= 1 && !discovered.privateSubnetId1) {
167
- discovered.privateSubnetId1 = subnetAssociations[0].SubnetId;
168
- console.log(` ✓ Extracted private subnet 1 from associations: ${subnetAssociations[0].SubnetId}`);
169
- }
170
- if (subnetAssociations.length >= 2 && !discovered.privateSubnetId2) {
171
- discovered.privateSubnetId2 = subnetAssociations[1].SubnetId;
172
- console.log(` ✓ Extracted private subnet 2 from associations: ${subnetAssociations[1].SubnetId}`);
173
- }
174
-
175
- // Query for default security group in the VPC (matches canary behavior)
176
- if (routeTable.VpcId && !discovered.defaultSecurityGroupId) {
177
- try {
178
- const { DescribeSecurityGroupsCommand } = require('@aws-sdk/client-ec2');
179
- const sgResponse = await ec2.send(new DescribeSecurityGroupsCommand({
180
- Filters: [
181
- { Name: 'vpc-id', Values: [routeTable.VpcId] },
182
- { Name: 'group-name', Values: ['default'] }
183
- ]
184
- }));
185
-
186
- if (sgResponse.SecurityGroups && sgResponse.SecurityGroups.length > 0) {
187
- discovered.defaultSecurityGroupId = sgResponse.SecurityGroups[0].GroupId;
188
- console.log(` ✓ Extracted default security group: ${discovered.defaultSecurityGroupId}`);
189
- }
190
- } catch (error) {
191
- console.warn(` ⚠️ Could not query default security group: ${error.message}`);
192
- }
193
- }
194
- }
195
- } catch (error) {
196
- console.warn(` ⚠️ Could not query route table for external references: ${error.message}`);
197
- }
198
- }
199
- } catch (error) {
200
- console.warn(` ⚠️ Error extracting external references: ${error.message}`);
201
- }
202
- }
203
-
204
114
  /**
205
115
  * Extract discovered resources from CloudFormation stack resources
206
116
  *
@@ -306,30 +216,6 @@ class CloudFormationDiscovery {
306
216
  discovered.natGatewayId = PhysicalResourceId;
307
217
  }
308
218
 
309
- // Route Table (Lambda route table for external VPC pattern)
310
- if (LogicalResourceId === 'FriggLambdaRouteTable' && ResourceType === 'AWS::EC2::RouteTable') {
311
- discovered.routeTableId = PhysicalResourceId;
312
- discovered.privateRouteTableId = PhysicalResourceId;
313
- console.log(` ✓ Found route table in stack: ${PhysicalResourceId}`);
314
- }
315
-
316
- // NAT Route (proves NAT configuration exists) - support both naming patterns
317
- if ((LogicalResourceId === 'FriggNATRoute' || LogicalResourceId === 'FriggPrivateRoute') &&
318
- ResourceType === 'AWS::EC2::Route') {
319
- discovered.natRoute = PhysicalResourceId;
320
- console.log(` ✓ Found NAT route in stack: ${LogicalResourceId}`);
321
- }
322
-
323
- // Route Table Associations (links subnets to route table)
324
- if (LogicalResourceId.includes('RouteAssociation') &&
325
- ResourceType === 'AWS::EC2::SubnetRouteTableAssociation') {
326
- if (!discovered.routeTableAssociations) {
327
- discovered.routeTableAssociations = [];
328
- }
329
- discovered.routeTableAssociations.push(PhysicalResourceId);
330
- console.log(` ✓ Found route table association: ${LogicalResourceId}`);
331
- }
332
-
333
219
  // VPC - direct extraction (primary method)
334
220
  if (LogicalResourceId === 'FriggVPC' && ResourceType === 'AWS::EC2::VPC') {
335
221
  discovered.defaultVpcId = PhysicalResourceId;
@@ -390,64 +276,26 @@ class CloudFormationDiscovery {
390
276
  // VPC Endpoint Security Group
391
277
  if (LogicalResourceId === 'FriggVPCEndpointSecurityGroup' && ResourceType === 'AWS::EC2::SecurityGroup') {
392
278
  discovered.vpcEndpointSecurityGroupId = PhysicalResourceId;
393
- console.log(` ✓ Found VPC endpoint security group in stack: ${PhysicalResourceId}`);
394
- }
395
-
396
- // Lambda Security Group (if created in stack)
397
- if (LogicalResourceId === 'FriggLambdaSecurityGroup' && ResourceType === 'AWS::EC2::SecurityGroup') {
398
- discovered.lambdaSecurityGroupId = PhysicalResourceId;
399
- // Also set as defaultSecurityGroupId so converter recognizes it
400
- discovered.defaultSecurityGroupId = PhysicalResourceId;
401
- console.log(` ✓ Found Lambda security group in stack: ${PhysicalResourceId}`);
402
- }
403
-
404
- // VPC Endpoints - support both old and new naming conventions
405
- // Initialize vpcEndpoints object for structured access
406
- if (!discovered.vpcEndpoints) {
407
- discovered.vpcEndpoints = {};
408
279
  }
409
280
 
410
- // S3 Endpoint (both naming patterns)
411
- if ((LogicalResourceId === 'FriggS3VPCEndpoint' || LogicalResourceId === 'VPCEndpointS3') &&
412
- ResourceType === 'AWS::EC2::VPCEndpoint') {
281
+ // VPC Endpoints
282
+ if (LogicalResourceId === 'FriggS3VPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
413
283
  discovered.s3VpcEndpointId = PhysicalResourceId;
414
- discovered.vpcEndpoints.s3 = PhysicalResourceId;
415
- console.log(` ✓ Found S3 VPC endpoint in stack: ${PhysicalResourceId}`);
416
284
  }
417
-
418
- // DynamoDB Endpoint (both naming patterns)
419
- if ((LogicalResourceId === 'FriggDynamoDBVPCEndpoint' || LogicalResourceId === 'VPCEndpointDynamoDB') &&
420
- ResourceType === 'AWS::EC2::VPCEndpoint') {
285
+ if (LogicalResourceId === 'FriggDynamoDBVPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
421
286
  discovered.dynamoDbVpcEndpointId = PhysicalResourceId;
422
- discovered.vpcEndpoints.dynamodb = PhysicalResourceId;
423
- console.log(` ✓ Found DynamoDB VPC endpoint in stack: ${PhysicalResourceId}`);
424
287
  }
425
-
426
- // KMS Endpoint (both naming patterns)
427
- if ((LogicalResourceId === 'FriggKMSVPCEndpoint' || LogicalResourceId === 'VPCEndpointKMS') &&
428
- ResourceType === 'AWS::EC2::VPCEndpoint') {
288
+ if (LogicalResourceId === 'FriggKMSVPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
429
289
  discovered.kmsVpcEndpointId = PhysicalResourceId;
430
- discovered.vpcEndpoints.kms = PhysicalResourceId;
431
- console.log(` ✓ Found KMS VPC endpoint in stack: ${PhysicalResourceId}`);
432
290
  }
433
-
434
- // Secrets Manager Endpoint
435
291
  if (LogicalResourceId === 'FriggSecretsManagerVPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
436
292
  discovered.secretsManagerVpcEndpointId = PhysicalResourceId;
437
- discovered.vpcEndpoints.secretsManager = PhysicalResourceId;
438
293
  }
439
-
440
- // SQS Endpoint
441
294
  if (LogicalResourceId === 'FriggSQSVPCEndpoint' && ResourceType === 'AWS::EC2::VPCEndpoint') {
442
295
  discovered.sqsVpcEndpointId = PhysicalResourceId;
443
- discovered.vpcEndpoints.sqs = PhysicalResourceId;
444
296
  }
445
297
  }
446
298
 
447
- // Extract VPC ID and other external references from routing resource properties
448
- // This handles the pattern where VPC is external but routing is in the stack
449
- await this._extractExternalReferencesFromStackResources(resources, discovered);
450
-
451
299
  // If we have a VPC ID but no subnet IDs, query EC2 for Frigg-managed subnets
452
300
  if (discovered.defaultVpcId && this.provider &&
453
301
  !discovered.privateSubnetId1 && !discovered.publicSubnetId1) {
@@ -586,223 +586,5 @@ 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
-
697
- it('should extract FriggLambdaSecurityGroup from stack', async () => {
698
- const mockStack = {
699
- StackName: 'test-stack',
700
- Outputs: [],
701
- };
702
-
703
- const mockResources = [
704
- {
705
- LogicalResourceId: 'FriggLambdaSecurityGroup',
706
- PhysicalResourceId: 'sg-01002240c6a446202',
707
- ResourceType: 'AWS::EC2::SecurityGroup',
708
- ResourceStatus: 'UPDATE_COMPLETE',
709
- },
710
- {
711
- LogicalResourceId: 'FriggLambdaRouteTable',
712
- PhysicalResourceId: 'rtb-08af43bbf0775602d',
713
- ResourceType: 'AWS::EC2::RouteTable',
714
- ResourceStatus: 'UPDATE_COMPLETE',
715
- },
716
- ];
717
-
718
- mockProvider.describeStack.mockResolvedValue(mockStack);
719
- mockProvider.listStackResources.mockResolvedValue(mockResources);
720
-
721
- const result = await cfDiscovery.discoverFromStack('test-stack');
722
-
723
- // Lambda security group should be extracted
724
- expect(result.lambdaSecurityGroupId).toBe('sg-01002240c6a446202');
725
- expect(result.defaultSecurityGroupId).toBe('sg-01002240c6a446202');
726
- expect(result.existingLogicalIds).toContain('FriggLambdaSecurityGroup');
727
- });
728
-
729
- it('should support FriggPrivateRoute naming for NAT routes', async () => {
730
- const mockStack = {
731
- StackName: 'test-stack',
732
- Outputs: [],
733
- };
734
-
735
- const mockResources = [
736
- {
737
- LogicalResourceId: 'FriggLambdaRouteTable',
738
- PhysicalResourceId: 'rtb-123',
739
- ResourceType: 'AWS::EC2::RouteTable',
740
- ResourceStatus: 'UPDATE_COMPLETE',
741
- },
742
- {
743
- LogicalResourceId: 'FriggPrivateRoute',
744
- PhysicalResourceId: 'rtb-123|0.0.0.0/0',
745
- ResourceType: 'AWS::EC2::Route',
746
- ResourceStatus: 'UPDATE_COMPLETE',
747
- },
748
- ];
749
-
750
- mockProvider.describeStack.mockResolvedValue(mockStack);
751
- mockProvider.listStackResources.mockResolvedValue(mockResources);
752
-
753
- const result = await cfDiscovery.discoverFromStack('test-stack');
754
-
755
- // Both FriggNATRoute and FriggPrivateRoute should be recognized
756
- expect(result.natRoute).toBe('rtb-123|0.0.0.0/0');
757
- expect(result.routeTableId).toBe('rtb-123');
758
- });
759
-
760
- it('should extract external references from route table without stackName error', async () => {
761
- const mockStack = {
762
- StackName: 'test-stack',
763
- Outputs: [],
764
- };
765
-
766
- const mockResources = [
767
- {
768
- LogicalResourceId: 'FriggLambdaRouteTable',
769
- PhysicalResourceId: 'rtb-real-id',
770
- ResourceType: 'AWS::EC2::RouteTable',
771
- ResourceStatus: 'UPDATE_COMPLETE',
772
- },
773
- ];
774
-
775
- mockProvider.describeStack.mockResolvedValue(mockStack);
776
- mockProvider.listStackResources.mockResolvedValue(mockResources);
777
-
778
- // Mock EC2 DescribeRouteTables to return route table with VPC info
779
- mockProvider.getEC2Client = jest.fn().mockReturnValue({
780
- send: jest.fn().mockResolvedValue({
781
- RouteTables: [{
782
- RouteTableId: 'rtb-real-id',
783
- VpcId: 'vpc-extracted',
784
- Routes: [
785
- { NatGatewayId: 'nat-extracted', DestinationCidrBlock: '0.0.0.0/0' }
786
- ],
787
- Associations: [
788
- { SubnetId: 'subnet-1' },
789
- { SubnetId: 'subnet-2' }
790
- ]
791
- }]
792
- })
793
- });
794
-
795
- const result = await cfDiscovery.discoverFromStack('test-stack');
796
-
797
- // Should extract VPC, NAT, and subnets from route table
798
- expect(result.defaultVpcId).toBe('vpc-extracted');
799
- expect(result.existingNatGatewayId).toBe('nat-extracted');
800
- expect(result.privateSubnetId1).toBe('subnet-1');
801
- expect(result.privateSubnetId2).toBe('subnet-2');
802
-
803
- // Should NOT throw 'stackName is not defined' error
804
- expect(result).toBeDefined();
805
- });
806
- });
807
589
  });
808
590
 
@@ -536,29 +536,6 @@ 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
- }
562
539
  }
563
540
 
564
541
  module.exports = {
@@ -95,23 +95,11 @@ 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 - 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
- }
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;
115
103
 
116
104
  // Check if we're in isolated mode (each stage gets its own VPC/Aurora)
117
105
  const isIsolatedMode = appDefinition.managementMode === 'managed' &&
@@ -415,42 +415,6 @@ describe('Resource Discovery', () => {
415
415
  delete process.env.SLS_STAGE;
416
416
  });
417
417
 
418
- it('should recognize routing infrastructure as useful data', async () => {
419
- const appDefinition = {
420
- name: 'test-app',
421
- vpc: { enable: true },
422
- };
423
-
424
- process.env.SLS_STAGE = 'production';
425
-
426
- // Mock CloudFormation discovery to return routing infrastructure but no VPC resource
427
- const mockCloudFormationDiscovery = {
428
- discoverFromStack: jest.fn().mockResolvedValue({
429
- fromCloudFormationStack: true,
430
- routeTableId: 'rtb-123',
431
- natRoute: 'rtb-123|0.0.0.0/0',
432
- vpcEndpoints: {
433
- s3: 'vpce-s3',
434
- dynamodb: 'vpce-ddb'
435
- },
436
- existingLogicalIds: ['FriggLambdaRouteTable', 'FriggNATRoute']
437
- // NO defaultVpcId, NO defaultKmsKeyId, NO auroraClusterId
438
- })
439
- };
440
-
441
- const { CloudFormationDiscovery } = require('./cloudformation-discovery');
442
- CloudFormationDiscovery.mockImplementation(() => mockCloudFormationDiscovery);
443
-
444
- const result = await gatherDiscoveredResources(appDefinition);
445
-
446
- // Should use CloudFormation data without falling back to AWS API
447
- expect(result.routeTableId).toBe('rtb-123');
448
- expect(result.vpcEndpoints.s3).toBe('vpce-s3');
449
-
450
- // Should NOT call AWS API discovery
451
- expect(mockVpcDiscovery.discover).not.toHaveBeenCalled();
452
- });
453
-
454
418
  it('should include secrets in SSM discovery by default', async () => {
455
419
  const appDefinition = {
456
420
  ssm: { enable: true },
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.490.c5ad260.0",
4
+ "version": "2.0.0--canary.492.669f13d.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.490.c5ad260.0",
20
- "@friggframework/schemas": "2.0.0--canary.490.c5ad260.0",
21
- "@friggframework/test": "2.0.0--canary.490.c5ad260.0",
19
+ "@friggframework/core": "2.0.0--canary.492.669f13d.0",
20
+ "@friggframework/schemas": "2.0.0--canary.492.669f13d.0",
21
+ "@friggframework/test": "2.0.0--canary.492.669f13d.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.490.c5ad260.0",
50
- "@friggframework/prettier-config": "2.0.0--canary.490.c5ad260.0",
49
+ "@friggframework/eslint-config": "2.0.0--canary.492.669f13d.0",
50
+ "@friggframework/prettier-config": "2.0.0--canary.492.669f13d.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": "c5ad26056632a30d7ce772e3af8209e0b2d2014e"
82
+ "gitHead": "669f13d026454d149c314188e3271d4307751c48"
83
83
  }