@friggframework/devtools 2.0.0--canary.461.c930ee6.0 → 2.0.0--canary.461.ec1ad4e.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';
@@ -862,23 +862,82 @@ class VpcBuilder extends InfrastructureBuilder {
862
862
  console.log(' ✅ Route table and subnet associations created');
863
863
  }
864
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
+
865
898
  /**
866
899
  * Build VPC Endpoints for AWS services
867
900
  */
868
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)
869
913
  const missing = [];
870
- if (!existingEndpoints.s3) missing.push('S3');
871
- if (!existingEndpoints.dynamodb) missing.push('DynamoDB');
872
- if (!existingEndpoints.kms && appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') missing.push('KMS');
873
- if (!existingEndpoints.secretsManager) missing.push('Secrets Manager');
874
- // SQS endpoint needed for database migrations (migration queue)
875
- if (!existingEndpoints.sqs && appDefinition.database?.postgres?.enable) missing.push('SQS');
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');
918
+ // SQS endpoint needed for job queues and async processing
919
+ if (!stackManagedEndpoints.sqs && !existingEndpoints.sqs) 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
+ }
876
932
 
877
933
  if (missing.length > 0) {
878
934
  console.log(` Creating missing VPC Endpoints: ${missing.join(', ')}...`);
879
- } else {
935
+ } else if (reused.length === 0) {
880
936
  console.log(' All required VPC Endpoints already exist - skipping creation');
881
937
  return;
938
+ } else {
939
+ // All endpoints are stack-managed, no creation needed
940
+ return;
882
941
  }
883
942
 
884
943
  const vpcId = result.vpcId || discoveredResources.defaultVpcId;
@@ -898,8 +957,13 @@ class VpcBuilder extends InfrastructureBuilder {
898
957
  };
899
958
  }
900
959
 
901
- // S3 Gateway Endpoint (only if missing)
902
- 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) {
903
967
  result.resources.FriggS3VPCEndpoint = {
904
968
  Type: 'AWS::EC2::VPCEndpoint',
905
969
  Properties: {
@@ -911,8 +975,8 @@ class VpcBuilder extends InfrastructureBuilder {
911
975
  };
912
976
  }
913
977
 
914
- // DynamoDB Gateway Endpoint (only if missing)
915
- if (!existingEndpoints.dynamodb) {
978
+ // DynamoDB Gateway Endpoint (only if not stack-managed and missing)
979
+ if (!stackManagedEndpoints.dynamodb && !existingEndpoints.dynamodb) {
916
980
  result.resources.FriggDynamoDBVPCEndpoint = {
917
981
  Type: 'AWS::EC2::VPCEndpoint',
918
982
  Properties: {
@@ -924,8 +988,13 @@ class VpcBuilder extends InfrastructureBuilder {
924
988
  };
925
989
  }
926
990
 
927
- // VPC Endpoint Security Group (only if KMS, Secrets Manager, or SQS are missing)
928
- 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);
996
+
997
+ if (needsSecurityGroup) {
929
998
  result.resources.FriggVPCEndpointSecurityGroup = {
930
999
  Type: 'AWS::EC2::SecurityGroup',
931
1000
  Properties: {
@@ -948,8 +1017,8 @@ class VpcBuilder extends InfrastructureBuilder {
948
1017
  };
949
1018
  }
950
1019
 
951
- // KMS Interface Endpoint (only if missing AND KMS encryption is enabled)
952
- 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') {
953
1022
  result.resources.FriggKMSVPCEndpoint = {
954
1023
  Type: 'AWS::EC2::VPCEndpoint',
955
1024
  Properties: {
@@ -963,8 +1032,8 @@ class VpcBuilder extends InfrastructureBuilder {
963
1032
  };
964
1033
  }
965
1034
 
966
- // Secrets Manager Interface Endpoint (only if missing)
967
- if (!existingEndpoints.secretsManager) {
1035
+ // Secrets Manager Interface Endpoint (only if not stack-managed and missing)
1036
+ if (!stackManagedEndpoints.secretsManager && !existingEndpoints.secretsManager) {
968
1037
  result.resources.FriggSecretsManagerVPCEndpoint = {
969
1038
  Type: 'AWS::EC2::VPCEndpoint',
970
1039
  Properties: {
@@ -978,8 +1047,9 @@ class VpcBuilder extends InfrastructureBuilder {
978
1047
  };
979
1048
  }
980
1049
 
981
- // SQS Interface Endpoint (only if missing AND database migrations are enabled)
982
- if (!existingEndpoints.sqs && appDefinition.database?.postgres?.enable) {
1050
+ // SQS Interface Endpoint (only if not stack-managed and missing)
1051
+ // Used for job queues and async processing (not just database migrations)
1052
+ if (!stackManagedEndpoints.sqs && !existingEndpoints.sqs) {
983
1053
  result.resources.FriggSQSVPCEndpoint = {
984
1054
  Type: 'AWS::EC2::VPCEndpoint',
985
1055
  Properties: {
@@ -398,6 +398,60 @@ 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
+ expect(result.resources.FriggSQSVPCEndpoint).toBeDefined();
453
+ });
454
+
401
455
  it('should skip VPC endpoints when disabled', async () => {
402
456
  const appDefinition = {
403
457
  vpc: {
@@ -418,6 +472,32 @@ describe('VpcBuilder', () => {
418
472
  expect(result.resources.FriggS3VPCEndpoint).toBeUndefined();
419
473
  });
420
474
 
475
+ it('should create route table associations when VPC endpoints exist but no NAT Gateway', async () => {
476
+ const appDefinition = {
477
+ vpc: { enable: true, enableVPCEndpoints: true, selfHeal: true },
478
+ };
479
+ const discoveredResources = {
480
+ defaultVpcId: 'vpc-123',
481
+ privateSubnetId1: 'subnet-1',
482
+ privateSubnetId2: 'subnet-2',
483
+ // No NAT Gateway, so associations won't be created by NAT Gateway routing
484
+ };
485
+
486
+ const result = await vpcBuilder.build(appDefinition, discoveredResources);
487
+
488
+ // Route table should be created for VPC endpoints
489
+ expect(result.resources.FriggLambdaRouteTable).toBeDefined();
490
+ expect(result.resources.FriggLambdaRouteTable.Type).toBe('AWS::EC2::RouteTable');
491
+
492
+ // Subnet associations should be created (healing)
493
+ expect(result.resources.FriggPrivateSubnet1RouteTableAssociation).toBeDefined();
494
+ expect(result.resources.FriggPrivateSubnet1RouteTableAssociation.Type).toBe('AWS::EC2::SubnetRouteTableAssociation');
495
+ expect(result.resources.FriggPrivateSubnet1RouteTableAssociation.Properties.SubnetId).toBe('subnet-1');
496
+
497
+ expect(result.resources.FriggPrivateSubnet2RouteTableAssociation).toBeDefined();
498
+ expect(result.resources.FriggPrivateSubnet2RouteTableAssociation.Properties.SubnetId).toBe('subnet-2');
499
+ });
500
+
421
501
  it('should include IAM permissions for VPC operations', async () => {
422
502
  const appDefinition = {
423
503
  vpc: {
@@ -894,8 +974,9 @@ describe('VpcBuilder', () => {
894
974
  };
895
975
 
896
976
  // No VPC in CloudFormation stack (fresh deployment)
977
+ // Default VPC might exist in AWS, but not stack-managed
897
978
  const discoveredResources = {
898
- defaultVpcId: 'vpc-default', // Only default VPC exists (not from stack)
979
+ // No defaultVpcId means no VPC in CloudFormation stack
899
980
  };
900
981
 
901
982
  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.c930ee6.0",
4
+ "version": "2.0.0--canary.461.ec1ad4e.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.c930ee6.0",
15
- "@friggframework/test": "2.0.0--canary.461.c930ee6.0",
14
+ "@friggframework/schemas": "2.0.0--canary.461.ec1ad4e.0",
15
+ "@friggframework/test": "2.0.0--canary.461.ec1ad4e.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.c930ee6.0",
38
- "@friggframework/prettier-config": "2.0.0--canary.461.c930ee6.0",
37
+ "@friggframework/eslint-config": "2.0.0--canary.461.ec1ad4e.0",
38
+ "@friggframework/prettier-config": "2.0.0--canary.461.ec1ad4e.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": "c930ee689c0dc0aa2a5eb51b445454fc95f2ce7a"
73
+ "gitHead": "ec1ad4ecc89d650ce9380aeed0a29831d8e2098b"
74
74
  }