@friggframework/devtools 2.0.0--canary.461.9cc128b.0 → 2.0.0--canary.461.fb1228a.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.
@@ -88,6 +88,10 @@ class AuroraBuilder extends InfrastructureBuilder {
88
88
  const globalMode = appDefinition.managementMode || 'discover';
89
89
  const vpcIsolation = appDefinition.vpcIsolation || 'shared';
90
90
 
91
+ // Debug logging
92
+ console.log(` 🔍 DEBUG: Aurora globalMode = '${globalMode}', vpcIsolation = '${vpcIsolation}'`);
93
+ console.log(` 🔍 DEBUG: Aurora discoveredResources.auroraClusterId = ${discoveredResources?.auroraClusterId}`);
94
+
91
95
  let management = dbConfig.management;
92
96
 
93
97
  if (globalMode === 'managed') {
@@ -106,6 +110,8 @@ class AuroraBuilder extends InfrastructureBuilder {
106
110
  const hasStackAurora = discoveredResources?.auroraClusterId &&
107
111
  typeof discoveredResources.auroraClusterId === 'string';
108
112
 
113
+ console.log(` 🔍 DEBUG: Aurora hasStackAurora = ${hasStackAurora}`);
114
+
109
115
  if (hasStackAurora) {
110
116
  // Stack has Aurora - reuse it (standard flow: stack → orphaned → create)
111
117
  management = 'discover';
@@ -131,7 +131,8 @@ class VpcBuilder extends InfrastructureBuilder {
131
131
 
132
132
  // Debug logging
133
133
  console.log(` 🔍 DEBUG: globalMode = '${globalMode}', vpcIsolation = '${vpcIsolation}'`);
134
- console.log(` 🔍 DEBUG: discoveredResources =`, JSON.stringify(discoveredResources, null, 2));
134
+ console.log(` 🔍 DEBUG: discoveredResources.defaultVpcId = ${discoveredResources?.defaultVpcId}`);
135
+ console.log(` 🔍 DEBUG: discoveredResources keys = ${Object.keys(discoveredResources || {}).join(', ')}`);
135
136
 
136
137
  let management = appDefinition.vpc.management;
137
138
 
@@ -861,23 +862,82 @@ class VpcBuilder extends InfrastructureBuilder {
861
862
  console.log(' ✅ Route table and subnet associations created');
862
863
  }
863
864
 
865
+ /**
866
+ * Ensure subnet associations with route table
867
+ * Called to heal missing associations when route table exists but associations don't
868
+ */
869
+ ensureSubnetAssociations(appDefinition, discoveredResources, result) {
870
+ // Skip if associations already created (by NAT Gateway routing)
871
+ if (result.resources.FriggPrivateSubnet1RouteTableAssociation) {
872
+ return; // Already handled by NAT Gateway routing
873
+ }
874
+
875
+ const routeTableId = discoveredResources.routeTableId || { Ref: 'FriggLambdaRouteTable' };
876
+ const subnet1Id = discoveredResources.privateSubnetId1 || { Ref: 'FriggPrivateSubnet1' };
877
+ const subnet2Id = discoveredResources.privateSubnetId2 || { Ref: 'FriggPrivateSubnet2' };
878
+
879
+ result.resources.FriggPrivateSubnet1RouteTableAssociation = {
880
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
881
+ Properties: {
882
+ SubnetId: subnet1Id,
883
+ RouteTableId: routeTableId,
884
+ },
885
+ };
886
+
887
+ result.resources.FriggPrivateSubnet2RouteTableAssociation = {
888
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
889
+ Properties: {
890
+ SubnetId: subnet2Id,
891
+ RouteTableId: routeTableId,
892
+ },
893
+ };
894
+
895
+ console.log(' ✓ Ensured subnet associations with route table');
896
+ }
897
+
864
898
  /**
865
899
  * Build VPC Endpoints for AWS services
866
900
  */
867
901
  buildVpcEndpoints(appDefinition, discoveredResources, result, existingEndpoints = {}) {
902
+ // Check if endpoints are from CloudFormation stack (string IDs)
903
+ // Stack-managed resources should be reused, not recreated
904
+ const stackManagedEndpoints = {
905
+ s3: discoveredResources.s3VpcEndpointId && typeof discoveredResources.s3VpcEndpointId === 'string',
906
+ dynamodb: discoveredResources.dynamoDbVpcEndpointId && typeof discoveredResources.dynamoDbVpcEndpointId === 'string',
907
+ kms: discoveredResources.kmsVpcEndpointId && typeof discoveredResources.kmsVpcEndpointId === 'string',
908
+ secretsManager: discoveredResources.secretsManagerVpcEndpointId && typeof discoveredResources.secretsManagerVpcEndpointId === 'string',
909
+ sqs: discoveredResources.sqsVpcEndpointId && typeof discoveredResources.sqsVpcEndpointId === 'string',
910
+ };
911
+
912
+ // Build list of what needs creation (not stack-managed, not existing elsewhere)
868
913
  const missing = [];
869
- if (!existingEndpoints.s3) missing.push('S3');
870
- if (!existingEndpoints.dynamodb) missing.push('DynamoDB');
871
- if (!existingEndpoints.kms && appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') missing.push('KMS');
872
- if (!existingEndpoints.secretsManager) missing.push('Secrets Manager');
914
+ if (!stackManagedEndpoints.s3 && !existingEndpoints.s3) missing.push('S3');
915
+ if (!stackManagedEndpoints.dynamodb && !existingEndpoints.dynamodb) missing.push('DynamoDB');
916
+ if (!stackManagedEndpoints.kms && !existingEndpoints.kms && appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') missing.push('KMS');
917
+ if (!stackManagedEndpoints.secretsManager && !existingEndpoints.secretsManager) missing.push('Secrets Manager');
873
918
  // SQS endpoint needed for database migrations (migration queue)
874
- if (!existingEndpoints.sqs && appDefinition.database?.postgres?.enable) missing.push('SQS');
919
+ if (!stackManagedEndpoints.sqs && !existingEndpoints.sqs && appDefinition.database?.postgres?.enable) missing.push('SQS');
920
+
921
+ // Log reused stack-managed endpoints
922
+ const reused = [];
923
+ if (stackManagedEndpoints.s3) reused.push('S3');
924
+ if (stackManagedEndpoints.dynamodb) reused.push('DynamoDB');
925
+ if (stackManagedEndpoints.kms) reused.push('KMS');
926
+ if (stackManagedEndpoints.secretsManager) reused.push('Secrets Manager');
927
+ if (stackManagedEndpoints.sqs) reused.push('SQS');
928
+
929
+ if (reused.length > 0) {
930
+ console.log(` ✓ Reusing stack-managed VPC endpoints: ${reused.join(', ')}`);
931
+ }
875
932
 
876
933
  if (missing.length > 0) {
877
934
  console.log(` Creating missing VPC Endpoints: ${missing.join(', ')}...`);
878
- } else {
935
+ } else if (reused.length === 0) {
879
936
  console.log(' All required VPC Endpoints already exist - skipping creation');
880
937
  return;
938
+ } else {
939
+ // All endpoints are stack-managed, no creation needed
940
+ return;
881
941
  }
882
942
 
883
943
  const vpcId = result.vpcId || discoveredResources.defaultVpcId;
@@ -897,8 +957,13 @@ class VpcBuilder extends InfrastructureBuilder {
897
957
  };
898
958
  }
899
959
 
900
- // S3 Gateway Endpoint (only if missing)
901
- if (!existingEndpoints.s3) {
960
+ // Ensure subnet associations exist (healing for VPC endpoints without NAT Gateway)
961
+ if (result.resources.FriggLambdaRouteTable || discoveredResources.routeTableId) {
962
+ this.ensureSubnetAssociations(appDefinition, discoveredResources, result);
963
+ }
964
+
965
+ // S3 Gateway Endpoint (only if not stack-managed and missing)
966
+ if (!stackManagedEndpoints.s3 && !existingEndpoints.s3) {
902
967
  result.resources.FriggS3VPCEndpoint = {
903
968
  Type: 'AWS::EC2::VPCEndpoint',
904
969
  Properties: {
@@ -910,8 +975,8 @@ class VpcBuilder extends InfrastructureBuilder {
910
975
  };
911
976
  }
912
977
 
913
- // DynamoDB Gateway Endpoint (only if missing)
914
- if (!existingEndpoints.dynamodb) {
978
+ // DynamoDB Gateway Endpoint (only if not stack-managed and missing)
979
+ if (!stackManagedEndpoints.dynamodb && !existingEndpoints.dynamodb) {
915
980
  result.resources.FriggDynamoDBVPCEndpoint = {
916
981
  Type: 'AWS::EC2::VPCEndpoint',
917
982
  Properties: {
@@ -923,8 +988,13 @@ class VpcBuilder extends InfrastructureBuilder {
923
988
  };
924
989
  }
925
990
 
926
- // VPC Endpoint Security Group (only if KMS, Secrets Manager, or SQS are missing)
927
- if (!existingEndpoints.kms || !existingEndpoints.secretsManager || (!existingEndpoints.sqs && appDefinition.database?.postgres?.enable)) {
991
+ // VPC Endpoint Security Group (only if KMS, Secrets Manager, or SQS are not stack-managed and missing)
992
+ const needsSecurityGroup =
993
+ (!stackManagedEndpoints.kms && !existingEndpoints.kms && appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') ||
994
+ (!stackManagedEndpoints.secretsManager && !existingEndpoints.secretsManager) ||
995
+ (!stackManagedEndpoints.sqs && !existingEndpoints.sqs && appDefinition.database?.postgres?.enable);
996
+
997
+ if (needsSecurityGroup) {
928
998
  result.resources.FriggVPCEndpointSecurityGroup = {
929
999
  Type: 'AWS::EC2::SecurityGroup',
930
1000
  Properties: {
@@ -947,8 +1017,8 @@ class VpcBuilder extends InfrastructureBuilder {
947
1017
  };
948
1018
  }
949
1019
 
950
- // KMS Interface Endpoint (only if missing AND KMS encryption is enabled)
951
- if (!existingEndpoints.kms && appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') {
1020
+ // KMS Interface Endpoint (only if not stack-managed, missing, AND KMS encryption is enabled)
1021
+ if (!stackManagedEndpoints.kms && !existingEndpoints.kms && appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') {
952
1022
  result.resources.FriggKMSVPCEndpoint = {
953
1023
  Type: 'AWS::EC2::VPCEndpoint',
954
1024
  Properties: {
@@ -962,8 +1032,8 @@ class VpcBuilder extends InfrastructureBuilder {
962
1032
  };
963
1033
  }
964
1034
 
965
- // Secrets Manager Interface Endpoint (only if missing)
966
- if (!existingEndpoints.secretsManager) {
1035
+ // Secrets Manager Interface Endpoint (only if not stack-managed and missing)
1036
+ if (!stackManagedEndpoints.secretsManager && !existingEndpoints.secretsManager) {
967
1037
  result.resources.FriggSecretsManagerVPCEndpoint = {
968
1038
  Type: 'AWS::EC2::VPCEndpoint',
969
1039
  Properties: {
@@ -977,8 +1047,8 @@ class VpcBuilder extends InfrastructureBuilder {
977
1047
  };
978
1048
  }
979
1049
 
980
- // SQS Interface Endpoint (only if missing AND database migrations are enabled)
981
- if (!existingEndpoints.sqs && appDefinition.database?.postgres?.enable) {
1050
+ // SQS Interface Endpoint (only if not stack-managed, missing, AND database migrations are enabled)
1051
+ if (!stackManagedEndpoints.sqs && !existingEndpoints.sqs && appDefinition.database?.postgres?.enable) {
982
1052
  result.resources.FriggSQSVPCEndpoint = {
983
1053
  Type: 'AWS::EC2::VPCEndpoint',
984
1054
  Properties: {
@@ -398,6 +398,59 @@ describe('VpcBuilder', () => {
398
398
  expect(result.resources.FriggS3VPCEndpoint.Properties.VpcId).toBe('vpc-123');
399
399
  });
400
400
 
401
+ it('should reuse stack-managed VPC endpoints without creating CloudFormation resources', async () => {
402
+ const appDefinition = {
403
+ vpc: { enable: true, enableVPCEndpoints: true },
404
+ encryption: { fieldLevelEncryptionMethod: 'kms' },
405
+ database: { postgres: { enable: true } },
406
+ };
407
+ const discoveredResources = {
408
+ defaultVpcId: 'vpc-123',
409
+ privateSubnetId1: 'subnet-1',
410
+ privateSubnetId2: 'subnet-2',
411
+ // VPC endpoints from CloudFormation stack (string IDs)
412
+ s3VpcEndpointId: 'vpce-s3-stack',
413
+ dynamoDbVpcEndpointId: 'vpce-ddb-stack',
414
+ kmsVpcEndpointId: 'vpce-kms-stack',
415
+ secretsManagerVpcEndpointId: 'vpce-sm-stack',
416
+ sqsVpcEndpointId: 'vpce-sqs-stack',
417
+ };
418
+
419
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
420
+
421
+ // Should NOT create CloudFormation resources (reuse stack endpoints)
422
+ expect(result.resources.FriggS3VPCEndpoint).toBeUndefined();
423
+ expect(result.resources.FriggDynamoDBVPCEndpoint).toBeUndefined();
424
+ expect(result.resources.FriggKMSVPCEndpoint).toBeUndefined();
425
+ expect(result.resources.FriggSecretsManagerVPCEndpoint).toBeUndefined();
426
+ expect(result.resources.FriggSQSVPCEndpoint).toBeUndefined();
427
+
428
+ // Should still NOT create VPC Endpoint Security Group
429
+ expect(result.resources.FriggVPCEndpointSecurityGroup).toBeUndefined();
430
+ });
431
+
432
+ it('should create VPC endpoints when discovered from AWS but not stack', async () => {
433
+ const appDefinition = {
434
+ vpc: { enable: true, enableVPCEndpoints: true, selfHeal: true },
435
+ encryption: { fieldLevelEncryptionMethod: 'kms' },
436
+ };
437
+ const discoveredResources = {
438
+ defaultVpcId: 'vpc-123',
439
+ privateSubnetId1: 'subnet-1',
440
+ privateSubnetId2: 'subnet-2',
441
+ // No VPC endpoints in stack (would be strings)
442
+ // existingEndpoints will be passed as empty
443
+ };
444
+
445
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
446
+
447
+ // Should create CloudFormation resources (not in stack)
448
+ expect(result.resources.FriggS3VPCEndpoint).toBeDefined();
449
+ expect(result.resources.FriggDynamoDBVPCEndpoint).toBeDefined();
450
+ expect(result.resources.FriggKMSVPCEndpoint).toBeDefined();
451
+ expect(result.resources.FriggSecretsManagerVPCEndpoint).toBeDefined();
452
+ });
453
+
401
454
  it('should skip VPC endpoints when disabled', async () => {
402
455
  const appDefinition = {
403
456
  vpc: {
@@ -418,6 +471,32 @@ describe('VpcBuilder', () => {
418
471
  expect(result.resources.FriggS3VPCEndpoint).toBeUndefined();
419
472
  });
420
473
 
474
+ it('should create route table associations when VPC endpoints exist but no NAT Gateway', async () => {
475
+ const appDefinition = {
476
+ vpc: { enable: true, enableVPCEndpoints: true, selfHeal: true },
477
+ };
478
+ const discoveredResources = {
479
+ defaultVpcId: 'vpc-123',
480
+ privateSubnetId1: 'subnet-1',
481
+ privateSubnetId2: 'subnet-2',
482
+ // No NAT Gateway, so associations won't be created by NAT Gateway routing
483
+ };
484
+
485
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
486
+
487
+ // Route table should be created for VPC endpoints
488
+ expect(result.resources.FriggLambdaRouteTable).toBeDefined();
489
+ expect(result.resources.FriggLambdaRouteTable.Type).toBe('AWS::EC2::RouteTable');
490
+
491
+ // Subnet associations should be created (healing)
492
+ expect(result.resources.FriggPrivateSubnet1RouteTableAssociation).toBeDefined();
493
+ expect(result.resources.FriggPrivateSubnet1RouteTableAssociation.Type).toBe('AWS::EC2::SubnetRouteTableAssociation');
494
+ expect(result.resources.FriggPrivateSubnet1RouteTableAssociation.Properties.SubnetId).toBe('subnet-1');
495
+
496
+ expect(result.resources.FriggPrivateSubnet2RouteTableAssociation).toBeDefined();
497
+ expect(result.resources.FriggPrivateSubnet2RouteTableAssociation.Properties.SubnetId).toBe('subnet-2');
498
+ });
499
+
421
500
  it('should include IAM permissions for VPC operations', async () => {
422
501
  const appDefinition = {
423
502
  vpc: {
@@ -894,8 +973,9 @@ describe('VpcBuilder', () => {
894
973
  };
895
974
 
896
975
  // No VPC in CloudFormation stack (fresh deployment)
976
+ // Default VPC might exist in AWS, but not stack-managed
897
977
  const discoveredResources = {
898
- defaultVpcId: 'vpc-default', // Only default VPC exists (not from stack)
978
+ // No defaultVpcId means no VPC in CloudFormation stack
899
979
  };
900
980
 
901
981
  const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
@@ -137,9 +137,38 @@ class CloudFormationDiscovery {
137
137
  }
138
138
  }
139
139
 
140
- // Aurora cluster
140
+ // Aurora cluster - query AWS to get endpoint details
141
141
  if (LogicalResourceId === 'FriggAuroraCluster' && ResourceType === 'AWS::RDS::DBCluster') {
142
142
  discovered.auroraClusterId = PhysicalResourceId;
143
+ console.log(` ✓ Found Aurora cluster in stack: ${PhysicalResourceId}`);
144
+
145
+ // Query RDS to get cluster endpoint
146
+ if (this.provider && !discovered.auroraClusterEndpoint) {
147
+ try {
148
+ console.log(` Querying RDS to get Aurora endpoint...`);
149
+ const { DescribeDBClustersCommand } = require('@aws-sdk/client-rds');
150
+ const { RDSClient } = require('@aws-sdk/client-rds');
151
+
152
+ const rdsClient = new RDSClient({ region: this.provider.region });
153
+ const clusterDetails = await rdsClient.send(
154
+ new DescribeDBClustersCommand({
155
+ DBClusterIdentifier: PhysicalResourceId
156
+ })
157
+ );
158
+
159
+ if (clusterDetails.DBClusters && clusterDetails.DBClusters.length > 0) {
160
+ const cluster = clusterDetails.DBClusters[0];
161
+ discovered.auroraClusterEndpoint = cluster.Endpoint;
162
+ discovered.auroraClusterPort = cluster.Port;
163
+ discovered.auroraClusterIdentifier = cluster.DBClusterIdentifier;
164
+ console.log(` ✓ Extracted Aurora endpoint: ${cluster.Endpoint}:${cluster.Port}`);
165
+ } else {
166
+ console.warn(` ⚠️ RDS cluster query returned no results`);
167
+ }
168
+ } catch (error) {
169
+ console.warn(` ⚠️ Could not get endpoint from Aurora cluster: ${error.message}`);
170
+ }
171
+ }
143
172
  }
144
173
 
145
174
  // Migration status bucket
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.9cc128b.0",
4
+ "version": "2.0.0--canary.461.fb1228a.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.9cc128b.0",
15
- "@friggframework/test": "2.0.0--canary.461.9cc128b.0",
14
+ "@friggframework/schemas": "2.0.0--canary.461.fb1228a.0",
15
+ "@friggframework/test": "2.0.0--canary.461.fb1228a.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.9cc128b.0",
38
- "@friggframework/prettier-config": "2.0.0--canary.461.9cc128b.0",
37
+ "@friggframework/eslint-config": "2.0.0--canary.461.fb1228a.0",
38
+ "@friggframework/prettier-config": "2.0.0--canary.461.fb1228a.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": "9cc128b2052ab29ee18f3aa04a34c200346a2a8f"
73
+ "gitHead": "fb1228abb7a26c860a458d4fbf27a70be53d1683"
74
74
  }