@friggframework/devtools 2.0.0-next.53 → 2.0.0-next.54
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.
- package/frigg-cli/README.md +13 -14
- package/frigg-cli/__tests__/unit/commands/db-setup.test.js +267 -166
- package/frigg-cli/__tests__/unit/utils/database-validator.test.js +45 -14
- package/frigg-cli/__tests__/unit/utils/error-messages.test.js +44 -3
- package/frigg-cli/db-setup-command/index.js +75 -22
- package/frigg-cli/deploy-command/index.js +6 -3
- package/frigg-cli/utils/database-validator.js +18 -5
- package/frigg-cli/utils/error-messages.js +84 -12
- package/infrastructure/README.md +28 -0
- package/infrastructure/domains/database/migration-builder.js +26 -20
- package/infrastructure/domains/database/migration-builder.test.js +27 -0
- package/infrastructure/domains/integration/integration-builder.js +17 -13
- package/infrastructure/domains/integration/integration-builder.test.js +23 -0
- package/infrastructure/domains/networking/vpc-builder.js +240 -18
- package/infrastructure/domains/networking/vpc-builder.test.js +711 -13
- package/infrastructure/domains/networking/vpc-resolver.js +221 -40
- package/infrastructure/domains/networking/vpc-resolver.test.js +318 -18
- package/infrastructure/domains/security/kms-builder.js +55 -6
- package/infrastructure/domains/security/kms-builder.test.js +19 -1
- package/infrastructure/domains/shared/cloudformation-discovery.js +310 -13
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +395 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.js +41 -6
- package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +39 -0
- package/infrastructure/domains/shared/resource-discovery.js +17 -5
- package/infrastructure/domains/shared/resource-discovery.test.js +36 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +30 -20
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +43 -0
- package/infrastructure/infrastructure-composer.js +11 -3
- package/infrastructure/scripts/build-prisma-layer.js +153 -78
- package/infrastructure/scripts/build-prisma-layer.test.js +27 -11
- package/layers/prisma/.build-complete +3 -0
- package/package.json +7 -7
|
@@ -398,19 +398,93 @@ describe('VpcBuilder', () => {
|
|
|
398
398
|
expect(result.resources.FriggS3VPCEndpoint.Properties.VpcId).toBe('vpc-123');
|
|
399
399
|
});
|
|
400
400
|
|
|
401
|
-
it('should
|
|
401
|
+
it('should add stack-managed security group back to template to prevent deletion', async () => {
|
|
402
402
|
const appDefinition = {
|
|
403
|
-
vpc: { enable: true
|
|
403
|
+
vpc: { enable: true },
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const discoveredResources = {
|
|
407
|
+
fromCloudFormationStack: true,
|
|
408
|
+
stackName: 'test-stack',
|
|
409
|
+
existingLogicalIds: ['FriggLambdaSecurityGroup'],
|
|
410
|
+
defaultVpcId: 'vpc-123',
|
|
411
|
+
privateSubnetId1: 'subnet-1',
|
|
412
|
+
privateSubnetId2: 'subnet-2',
|
|
413
|
+
lambdaSecurityGroupId: 'sg-existing-stack', // Existing in stack
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const result = await vpcBuilder.build(appDefinition, discoveredResources);
|
|
417
|
+
|
|
418
|
+
// CRITICAL: Must RE-ADD stack-managed SG to template or CloudFormation will DELETE it
|
|
419
|
+
expect(result.resources.FriggLambdaSecurityGroup).toBeDefined();
|
|
420
|
+
expect(result.resources.FriggLambdaSecurityGroup.Type).toBe('AWS::EC2::SecurityGroup');
|
|
421
|
+
expect(result.resources.FriggLambdaSecurityGroup.Properties.VpcId).toBe('vpc-123');
|
|
422
|
+
expect(result.resources.FriggLambdaSecurityGroup.Properties.GroupDescription).toBeDefined();
|
|
423
|
+
|
|
424
|
+
// Should use Ref in Lambda config (not recreating)
|
|
425
|
+
expect(result.vpcConfig.securityGroupIds).toContainEqual({ Ref: 'FriggLambdaSecurityGroup' });
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('should add stack-managed subnets back to template to prevent deletion', async () => {
|
|
429
|
+
const appDefinition = {
|
|
430
|
+
vpc: { enable: true },
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const discoveredResources = {
|
|
434
|
+
fromCloudFormationStack: true,
|
|
435
|
+
stackName: 'test-stack',
|
|
436
|
+
existingLogicalIds: ['FriggPrivateSubnet1', 'FriggPrivateSubnet2'],
|
|
437
|
+
defaultVpcId: 'vpc-123',
|
|
438
|
+
// Subnets exist in stack with specific IDs
|
|
439
|
+
privateSubnetId1: 'subnet-existing-1',
|
|
440
|
+
privateSubnetId2: 'subnet-existing-2',
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const result = await vpcBuilder.build(appDefinition, discoveredResources);
|
|
444
|
+
|
|
445
|
+
// CRITICAL: Must RE-ADD stack-managed subnets to template or CloudFormation will DELETE them
|
|
446
|
+
expect(result.resources.FriggPrivateSubnet1).toBeDefined();
|
|
447
|
+
expect(result.resources.FriggPrivateSubnet1.Type).toBe('AWS::EC2::Subnet');
|
|
448
|
+
expect(result.resources.FriggPrivateSubnet1.Properties.VpcId).toBe('vpc-123');
|
|
449
|
+
|
|
450
|
+
expect(result.resources.FriggPrivateSubnet2).toBeDefined();
|
|
451
|
+
expect(result.resources.FriggPrivateSubnet2.Type).toBe('AWS::EC2::Subnet');
|
|
452
|
+
expect(result.resources.FriggPrivateSubnet2.Properties.VpcId).toBe('vpc-123');
|
|
453
|
+
|
|
454
|
+
// Should use Refs (not external IDs)
|
|
455
|
+
expect(result.vpcConfig.subnetIds).toEqual([
|
|
456
|
+
{ Ref: 'FriggPrivateSubnet1' },
|
|
457
|
+
{ Ref: 'FriggPrivateSubnet2' }
|
|
458
|
+
]);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('should add stack-managed VPC endpoints back to template to prevent deletion', async () => {
|
|
462
|
+
const appDefinition = {
|
|
463
|
+
vpc: { enable: true },
|
|
404
464
|
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
405
|
-
database: { postgres: { enable: true } },
|
|
406
465
|
};
|
|
466
|
+
|
|
467
|
+
// Structured discovery from CloudFormation
|
|
407
468
|
const discoveredResources = {
|
|
469
|
+
fromCloudFormationStack: true,
|
|
470
|
+
stackName: 'test-stack',
|
|
471
|
+
existingLogicalIds: [
|
|
472
|
+
'FriggLambdaSecurityGroup',
|
|
473
|
+
'FriggLambdaRouteTable',
|
|
474
|
+
'FriggS3VPCEndpoint',
|
|
475
|
+
'FriggDynamoDBVPCEndpoint',
|
|
476
|
+
'FriggKMSVPCEndpoint',
|
|
477
|
+
'FriggSecretsManagerVPCEndpoint',
|
|
478
|
+
'FriggSQSVPCEndpoint'
|
|
479
|
+
],
|
|
408
480
|
defaultVpcId: 'vpc-123',
|
|
409
481
|
privateSubnetId1: 'subnet-1',
|
|
410
482
|
privateSubnetId2: 'subnet-2',
|
|
411
|
-
|
|
483
|
+
routeTableId: 'rtb-123',
|
|
484
|
+
lambdaSecurityGroupId: 'sg-123',
|
|
485
|
+
// VPC endpoints discovered in stack
|
|
412
486
|
s3VpcEndpointId: 'vpce-s3-stack',
|
|
413
|
-
|
|
487
|
+
dynamodbVpcEndpointId: 'vpce-ddb-stack',
|
|
414
488
|
kmsVpcEndpointId: 'vpce-kms-stack',
|
|
415
489
|
secretsManagerVpcEndpointId: 'vpce-sm-stack',
|
|
416
490
|
sqsVpcEndpointId: 'vpce-sqs-stack',
|
|
@@ -418,15 +492,30 @@ describe('VpcBuilder', () => {
|
|
|
418
492
|
|
|
419
493
|
const result = await vpcBuilder.build(appDefinition, discoveredResources);
|
|
420
494
|
|
|
421
|
-
//
|
|
422
|
-
expect(result.resources.FriggS3VPCEndpoint).
|
|
423
|
-
expect(result.resources.
|
|
424
|
-
expect(result.resources.
|
|
425
|
-
|
|
426
|
-
expect(result.resources.
|
|
495
|
+
// CRITICAL: Must RE-ADD stack-managed endpoints to template or CloudFormation will DELETE them
|
|
496
|
+
expect(result.resources.FriggS3VPCEndpoint).toBeDefined();
|
|
497
|
+
expect(result.resources.FriggS3VPCEndpoint.Type).toBe('AWS::EC2::VPCEndpoint');
|
|
498
|
+
expect(result.resources.FriggS3VPCEndpoint.Properties.VpcEndpointType).toBe('Gateway');
|
|
499
|
+
|
|
500
|
+
expect(result.resources.FriggDynamoDBVPCEndpoint).toBeDefined();
|
|
501
|
+
expect(result.resources.FriggDynamoDBVPCEndpoint.Type).toBe('AWS::EC2::VPCEndpoint');
|
|
502
|
+
expect(result.resources.FriggDynamoDBVPCEndpoint.Properties.VpcEndpointType).toBe('Gateway');
|
|
503
|
+
|
|
504
|
+
expect(result.resources.FriggKMSVPCEndpoint).toBeDefined();
|
|
505
|
+
expect(result.resources.FriggKMSVPCEndpoint.Type).toBe('AWS::EC2::VPCEndpoint');
|
|
506
|
+
expect(result.resources.FriggKMSVPCEndpoint.Properties.VpcEndpointType).toBe('Interface');
|
|
507
|
+
|
|
508
|
+
expect(result.resources.FriggSecretsManagerVPCEndpoint).toBeDefined();
|
|
509
|
+
expect(result.resources.FriggSecretsManagerVPCEndpoint.Type).toBe('AWS::EC2::VPCEndpoint');
|
|
510
|
+
expect(result.resources.FriggSecretsManagerVPCEndpoint.Properties.VpcEndpointType).toBe('Interface');
|
|
511
|
+
|
|
512
|
+
expect(result.resources.FriggSQSVPCEndpoint).toBeDefined();
|
|
513
|
+
expect(result.resources.FriggSQSVPCEndpoint.Type).toBe('AWS::EC2::VPCEndpoint');
|
|
514
|
+
expect(result.resources.FriggSQSVPCEndpoint.Properties.VpcEndpointType).toBe('Interface');
|
|
427
515
|
|
|
428
|
-
// Should
|
|
429
|
-
expect(result.resources.FriggVPCEndpointSecurityGroup).
|
|
516
|
+
// Should create VPC Endpoint Security Group for interface endpoints
|
|
517
|
+
expect(result.resources.FriggVPCEndpointSecurityGroup).toBeDefined();
|
|
518
|
+
expect(result.resources.FriggVPCEndpointSecurityGroup.Type).toBe('AWS::EC2::SecurityGroup');
|
|
430
519
|
});
|
|
431
520
|
|
|
432
521
|
it('should create VPC endpoints when discovered from AWS but not stack', async () => {
|
|
@@ -715,6 +804,35 @@ describe('VpcBuilder', () => {
|
|
|
715
804
|
expect(result.resources.FriggPrivateSubnet2RouteTableAssociation.Properties.RouteTableId).toEqual({ Ref: 'FriggLambdaRouteTable' });
|
|
716
805
|
});
|
|
717
806
|
|
|
807
|
+
it('should add UpdateReplacePolicy to force association recreation on updates', async () => {
|
|
808
|
+
const appDefinition = {
|
|
809
|
+
vpc: {
|
|
810
|
+
enable: true,
|
|
811
|
+
management: 'discover',
|
|
812
|
+
subnets: { management: 'discover' },
|
|
813
|
+
natGateway: { management: 'discover' },
|
|
814
|
+
},
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
const discoveredResources = {
|
|
818
|
+
vpcId: 'vpc-123',
|
|
819
|
+
privateSubnetId1: 'subnet-existing-1',
|
|
820
|
+
privateSubnetId2: 'subnet-existing-2',
|
|
821
|
+
natGatewayId: 'nat-existing',
|
|
822
|
+
routeTableId: 'rtb-old',
|
|
823
|
+
existingLogicalIds: ['FriggSubnet1RouteAssociation', 'FriggSubnet2RouteAssociation'],
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
const result = await vpcBuilder.build(appDefinition, discoveredResources);
|
|
827
|
+
|
|
828
|
+
// Verify associations have UpdateReplacePolicy to force recreation
|
|
829
|
+
expect(result.resources.FriggSubnet1RouteAssociation.UpdateReplacePolicy).toBe('Delete');
|
|
830
|
+
expect(result.resources.FriggSubnet2RouteAssociation.UpdateReplacePolicy).toBe('Delete');
|
|
831
|
+
|
|
832
|
+
// This forces CloudFormation to delete old associations and create new ones
|
|
833
|
+
// instead of trying to update them in-place (which doesn't work)
|
|
834
|
+
});
|
|
835
|
+
|
|
718
836
|
it('should not create NAT when existing NAT is properly placed', async () => {
|
|
719
837
|
const appDefinition = {
|
|
720
838
|
vpc: {
|
|
@@ -1258,5 +1376,585 @@ describe('VpcBuilder', () => {
|
|
|
1258
1376
|
expect(result.outputs.PrivateSubnet2Id).toBeDefined();
|
|
1259
1377
|
});
|
|
1260
1378
|
});
|
|
1379
|
+
|
|
1380
|
+
describe('External VPC with stack-managed routing infrastructure pattern', () => {
|
|
1381
|
+
it('should correctly handle external VPC with NEW logical IDs (FriggPrivateRoute pattern)', async () => {
|
|
1382
|
+
// This pattern occurs when VPC/subnets/NAT are external but routing (route tables,
|
|
1383
|
+
// VPC endpoints, security groups) are managed by CloudFormation stack
|
|
1384
|
+
// This tests the NEWER naming convention
|
|
1385
|
+
const appDefinition = {
|
|
1386
|
+
vpc: { enable: true },
|
|
1387
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
1388
|
+
database: {
|
|
1389
|
+
dynamodb: { enable: true } // Enable DynamoDB to create DynamoDB VPC endpoint
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
|
|
1393
|
+
// Discovery results from real-world production scenario (newer stack)
|
|
1394
|
+
const discoveredResources = {
|
|
1395
|
+
fromCloudFormationStack: true,
|
|
1396
|
+
stackName: 'test-production-stack',
|
|
1397
|
+
existingLogicalIds: [
|
|
1398
|
+
'FriggLambdaSecurityGroup',
|
|
1399
|
+
'FriggLambdaRouteTable',
|
|
1400
|
+
'FriggPrivateRoute', // NEW naming
|
|
1401
|
+
'FriggPrivateSubnet1RouteTableAssociation', // NEW naming
|
|
1402
|
+
'FriggPrivateSubnet2RouteTableAssociation', // NEW naming
|
|
1403
|
+
'FriggS3VPCEndpoint', // NEW naming
|
|
1404
|
+
'FriggDynamoDBVPCEndpoint', // NEW naming
|
|
1405
|
+
'FriggKMSVPCEndpoint' // NEW naming
|
|
1406
|
+
],
|
|
1407
|
+
// Stack resources (from CloudFormation)
|
|
1408
|
+
lambdaSecurityGroupId: 'sg-01002240c6a446202',
|
|
1409
|
+
routeTableId: 'rtb-08af43bbf0775602d',
|
|
1410
|
+
s3VpcEndpointId: 'vpce-0d1ecb2c53ce9b4b8',
|
|
1411
|
+
dynamodbVpcEndpointId: 'vpce-0fb749b207f1020b0',
|
|
1412
|
+
kmsVpcEndpointId: 'vpce-0e38c25155b86de22',
|
|
1413
|
+
// External resources (discovered via queries)
|
|
1414
|
+
defaultVpcId: 'vpc-0cd17c0e06cb28b28',
|
|
1415
|
+
privateSubnetId1: 'subnet-034f6562dbbc16348',
|
|
1416
|
+
privateSubnetId2: 'subnet-0b8be2b82aeb5cdec',
|
|
1417
|
+
existingNatGatewayId: 'nat-022660c36a47e2d79'
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
const result = await vpcBuilder.build(appDefinition, discoveredResources);
|
|
1421
|
+
|
|
1422
|
+
// === ASSERTIONS: Template Structure ===
|
|
1423
|
+
|
|
1424
|
+
// 1. VPC should be external (not in template)
|
|
1425
|
+
expect(result.resources.FriggVPC).toBeUndefined();
|
|
1426
|
+
expect(result.vpcId).toBe('vpc-0cd17c0e06cb28b28');
|
|
1427
|
+
|
|
1428
|
+
// 2. Security Group MUST be in template (stack-managed)
|
|
1429
|
+
expect(result.resources.FriggLambdaSecurityGroup).toBeDefined();
|
|
1430
|
+
expect(result.resources.FriggLambdaSecurityGroup.Type).toBe('AWS::EC2::SecurityGroup');
|
|
1431
|
+
expect(result.vpcConfig.securityGroupIds).toEqual([{ Ref: 'FriggLambdaSecurityGroup' }]);
|
|
1432
|
+
|
|
1433
|
+
// 3. Subnets should be external (use hardcoded IDs, not in template)
|
|
1434
|
+
expect(result.resources.FriggPrivateSubnet1).toBeUndefined();
|
|
1435
|
+
expect(result.resources.FriggPrivateSubnet2).toBeUndefined();
|
|
1436
|
+
expect(result.vpcConfig.subnetIds).toEqual([
|
|
1437
|
+
'subnet-034f6562dbbc16348',
|
|
1438
|
+
'subnet-0b8be2b82aeb5cdec'
|
|
1439
|
+
]);
|
|
1440
|
+
|
|
1441
|
+
// 4. NAT Gateway should be external (not in template)
|
|
1442
|
+
expect(result.resources.FriggNATGateway).toBeUndefined();
|
|
1443
|
+
expect(result.resources.FriggNATGatewayEIP).toBeUndefined();
|
|
1444
|
+
expect(result.natGatewayId).toBe('nat-022660c36a47e2d79');
|
|
1445
|
+
|
|
1446
|
+
// 5. Route table MUST be in template (stack-managed)
|
|
1447
|
+
expect(result.resources.FriggLambdaRouteTable).toBeDefined();
|
|
1448
|
+
expect(result.resources.FriggLambdaRouteTable.Type).toBe('AWS::EC2::RouteTable');
|
|
1449
|
+
|
|
1450
|
+
// 6. Route table associations MUST be in template
|
|
1451
|
+
expect(result.resources.FriggPrivateSubnet1RouteTableAssociation).toBeDefined();
|
|
1452
|
+
expect(result.resources.FriggPrivateSubnet2RouteTableAssociation).toBeDefined();
|
|
1453
|
+
|
|
1454
|
+
// 7. VPC Endpoints MUST be in template (stack-managed, prevents deletion)
|
|
1455
|
+
expect(result.resources.FriggS3VPCEndpoint).toBeDefined();
|
|
1456
|
+
expect(result.resources.FriggS3VPCEndpoint.Properties.VpcEndpointType).toBe('Gateway');
|
|
1457
|
+
|
|
1458
|
+
expect(result.resources.FriggDynamoDBVPCEndpoint).toBeDefined();
|
|
1459
|
+
expect(result.resources.FriggDynamoDBVPCEndpoint.Properties.VpcEndpointType).toBe('Gateway');
|
|
1460
|
+
|
|
1461
|
+
expect(result.resources.FriggKMSVPCEndpoint).toBeDefined();
|
|
1462
|
+
expect(result.resources.FriggKMSVPCEndpoint.Properties.VpcEndpointType).toBe('Interface');
|
|
1463
|
+
|
|
1464
|
+
// 8. VPC Endpoint Security Group needed for interface endpoints
|
|
1465
|
+
expect(result.resources.FriggVPCEndpointSecurityGroup).toBeDefined();
|
|
1466
|
+
|
|
1467
|
+
// === ASSERTIONS: Resource Count ===
|
|
1468
|
+
const resourceKeys = Object.keys(result.resources);
|
|
1469
|
+
const friggResources = resourceKeys.filter(k => k.startsWith('Frigg') || k.startsWith('VPC'));
|
|
1470
|
+
|
|
1471
|
+
// Should have routing infrastructure + endpoints + security groups
|
|
1472
|
+
// NOT full VPC (no FriggVPC, FriggPrivateSubnet1/2, FriggNATGateway)
|
|
1473
|
+
expect(friggResources).toContain('FriggLambdaSecurityGroup');
|
|
1474
|
+
expect(friggResources).toContain('FriggLambdaRouteTable');
|
|
1475
|
+
expect(friggResources).toContain('FriggS3VPCEndpoint');
|
|
1476
|
+
expect(friggResources).toContain('FriggDynamoDBVPCEndpoint');
|
|
1477
|
+
expect(friggResources).toContain('FriggKMSVPCEndpoint');
|
|
1478
|
+
expect(friggResources).not.toContain('FriggVPC');
|
|
1479
|
+
expect(friggResources).not.toContain('FriggPrivateSubnet1');
|
|
1480
|
+
expect(friggResources).not.toContain('FriggNATGateway');
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
it('should use OLD logical IDs for backwards compatibility (FriggNATRoute, VPCEndpointS3 pattern)', async () => {
|
|
1484
|
+
// CRITICAL TEST: Real Frontify production stack uses OLD naming convention
|
|
1485
|
+
// Stack currently has: FriggNATRoute, VPCEndpointS3, VPCEndpointDynamoDB
|
|
1486
|
+
// We MUST use these same logical IDs to avoid AlreadyExists errors
|
|
1487
|
+
const appDefinition = {
|
|
1488
|
+
vpc: {
|
|
1489
|
+
enable: true,
|
|
1490
|
+
ownership: {
|
|
1491
|
+
securityGroup: 'external'
|
|
1492
|
+
},
|
|
1493
|
+
external: {
|
|
1494
|
+
securityGroupIds: ['sg-0c5e0d0e4a2f5efcf']
|
|
1495
|
+
}
|
|
1496
|
+
},
|
|
1497
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
1498
|
+
database: {
|
|
1499
|
+
dynamodb: { enable: true }
|
|
1500
|
+
}
|
|
1501
|
+
};
|
|
1502
|
+
|
|
1503
|
+
// Discovery results matching ACTUAL Frontify production stack
|
|
1504
|
+
const discoveredResources = {
|
|
1505
|
+
fromCloudFormationStack: true,
|
|
1506
|
+
stackName: 'create-frigg-app-production',
|
|
1507
|
+
existingLogicalIds: [
|
|
1508
|
+
'FriggLambdaRouteTable',
|
|
1509
|
+
'FriggNATRoute', // OLD naming
|
|
1510
|
+
'FriggSubnet1RouteAssociation', // OLD naming
|
|
1511
|
+
'FriggSubnet2RouteAssociation', // OLD naming
|
|
1512
|
+
'VPCEndpointS3', // OLD naming
|
|
1513
|
+
'VPCEndpointDynamoDB' // OLD naming
|
|
1514
|
+
],
|
|
1515
|
+
// Structured discovery (what resolver needs)
|
|
1516
|
+
_structured: {
|
|
1517
|
+
stackManaged: [
|
|
1518
|
+
{ logicalId: 'FriggLambdaRouteTable', physicalId: 'rtb-08af43bbf0775602d', resourceType: 'AWS::EC2::RouteTable' },
|
|
1519
|
+
{ logicalId: 'FriggNATRoute', physicalId: 'rtb-08af43bbf0775602d|0.0.0.0/0', resourceType: 'AWS::EC2::Route' },
|
|
1520
|
+
{ logicalId: 'VPCEndpointS3', physicalId: 'vpce-0352ceac2124c14be', resourceType: 'AWS::EC2::VPCEndpoint' },
|
|
1521
|
+
{ logicalId: 'VPCEndpointDynamoDB', physicalId: 'vpce-0b06c4f631199ea68', resourceType: 'AWS::EC2::VPCEndpoint' }
|
|
1522
|
+
],
|
|
1523
|
+
external: [
|
|
1524
|
+
{ physicalId: 'vpc-01cd124575c683a17', resourceType: 'AWS::EC2::VPC' },
|
|
1525
|
+
{ physicalId: 'sg-0c5e0d0e4a2f5efcf', resourceType: 'AWS::EC2::SecurityGroup' },
|
|
1526
|
+
{ physicalId: 'subnet-0bbca02e9981df72c', resourceType: 'AWS::EC2::Subnet' },
|
|
1527
|
+
{ physicalId: 'subnet-005f7092b91efaaeb', resourceType: 'AWS::EC2::Subnet' },
|
|
1528
|
+
{ physicalId: 'nat-05a536cbe7056325f', resourceType: 'AWS::EC2::NatGateway' }
|
|
1529
|
+
]
|
|
1530
|
+
},
|
|
1531
|
+
// Flat discovery (for backwards compatibility)
|
|
1532
|
+
routeTableId: 'rtb-08af43bbf0775602d',
|
|
1533
|
+
natRoute: 'rtb-08af43bbf0775602d|0.0.0.0/0',
|
|
1534
|
+
s3VpcEndpointId: 'vpce-0352ceac2124c14be',
|
|
1535
|
+
dynamodbVpcEndpointId: 'vpce-0b06c4f631199ea68',
|
|
1536
|
+
// External resources
|
|
1537
|
+
defaultVpcId: 'vpc-01cd124575c683a17',
|
|
1538
|
+
defaultSecurityGroupId: 'sg-0c5e0d0e4a2f5efcf',
|
|
1539
|
+
privateSubnetId1: 'subnet-0bbca02e9981df72c',
|
|
1540
|
+
privateSubnetId2: 'subnet-005f7092b91efaaeb',
|
|
1541
|
+
existingNatGatewayId: 'nat-05a536cbe7056325f'
|
|
1542
|
+
};
|
|
1543
|
+
|
|
1544
|
+
const result = await vpcBuilder.build(appDefinition, discoveredResources);
|
|
1545
|
+
|
|
1546
|
+
// CRITICAL: Must use OLD logical IDs to match existing stack
|
|
1547
|
+
expect(result.resources.FriggNATRoute).toBeDefined();
|
|
1548
|
+
expect(result.resources.FriggNATRoute.Type).toBe('AWS::EC2::Route');
|
|
1549
|
+
expect(result.resources.FriggPrivateRoute).toBeUndefined(); // Should NOT create new ID
|
|
1550
|
+
|
|
1551
|
+
expect(result.resources.FriggSubnet1RouteAssociation).toBeDefined();
|
|
1552
|
+
expect(result.resources.FriggSubnet2RouteAssociation).toBeDefined();
|
|
1553
|
+
expect(result.resources.FriggPrivateSubnet1RouteTableAssociation).toBeUndefined();
|
|
1554
|
+
expect(result.resources.FriggPrivateSubnet2RouteTableAssociation).toBeUndefined();
|
|
1555
|
+
|
|
1556
|
+
expect(result.resources.VPCEndpointS3).toBeDefined();
|
|
1557
|
+
expect(result.resources.VPCEndpointS3.Type).toBe('AWS::EC2::VPCEndpoint');
|
|
1558
|
+
expect(result.resources.FriggS3VPCEndpoint).toBeUndefined(); // Should NOT create new ID
|
|
1559
|
+
|
|
1560
|
+
expect(result.resources.VPCEndpointDynamoDB).toBeDefined();
|
|
1561
|
+
expect(result.resources.VPCEndpointDynamoDB.Type).toBe('AWS::EC2::VPCEndpoint');
|
|
1562
|
+
expect(result.resources.FriggDynamoDBVPCEndpoint).toBeUndefined(); // Should NOT create new ID
|
|
1563
|
+
|
|
1564
|
+
// Route table should still be created
|
|
1565
|
+
expect(result.resources.FriggLambdaRouteTable).toBeDefined();
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
it('should convert OLD logical IDs to structured discovery stackManaged array', () => {
|
|
1569
|
+
// TDD test: Verify that VPCEndpointS3 in existingLogicalIds gets added to stackManaged
|
|
1570
|
+
const flatDiscovery = {
|
|
1571
|
+
fromCloudFormationStack: true,
|
|
1572
|
+
stackName: 'create-frigg-app-production',
|
|
1573
|
+
existingLogicalIds: [
|
|
1574
|
+
'VPCEndpointS3', // OLD naming
|
|
1575
|
+
'VPCEndpointDynamoDB', // OLD naming
|
|
1576
|
+
'FriggNATRoute' // OLD naming
|
|
1577
|
+
],
|
|
1578
|
+
s3VpcEndpointId: 'vpce-0352ceac2124c14be',
|
|
1579
|
+
dynamodbVpcEndpointId: 'vpce-0b06c4f631199ea68',
|
|
1580
|
+
natRoute: 'rtb-xxx|0.0.0.0/0'
|
|
1581
|
+
};
|
|
1582
|
+
|
|
1583
|
+
const structured = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
|
|
1584
|
+
|
|
1585
|
+
// CRITICAL: Old logical IDs should be in stackManaged array
|
|
1586
|
+
expect(structured.stackManaged).toContainEqual(
|
|
1587
|
+
expect.objectContaining({
|
|
1588
|
+
logicalId: 'VPCEndpointS3',
|
|
1589
|
+
physicalId: 'vpce-0352ceac2124c14be',
|
|
1590
|
+
resourceType: 'AWS::EC2::VPCEndpoint'
|
|
1591
|
+
})
|
|
1592
|
+
);
|
|
1593
|
+
expect(structured.stackManaged).toContainEqual(
|
|
1594
|
+
expect.objectContaining({
|
|
1595
|
+
logicalId: 'VPCEndpointDynamoDB',
|
|
1596
|
+
physicalId: 'vpce-0b06c4f631199ea68',
|
|
1597
|
+
resourceType: 'AWS::EC2::VPCEndpoint'
|
|
1598
|
+
})
|
|
1599
|
+
);
|
|
1600
|
+
expect(structured.stackManaged).toContainEqual(
|
|
1601
|
+
expect.objectContaining({
|
|
1602
|
+
logicalId: 'FriggNATRoute',
|
|
1603
|
+
physicalId: 'rtb-xxx|0.0.0.0/0',
|
|
1604
|
+
resourceType: 'AWS::EC2::Route'
|
|
1605
|
+
})
|
|
1606
|
+
);
|
|
1607
|
+
});
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
describe('convertFlatDiscoveryToStructured - Direct Properties', () => {
|
|
1611
|
+
it('should copy flat discovery properties to structured discovery for resolver access', () => {
|
|
1612
|
+
const flatDiscovery = {
|
|
1613
|
+
fromCloudFormationStack: true,
|
|
1614
|
+
defaultVpcId: 'vpc-123',
|
|
1615
|
+
defaultSecurityGroupId: 'sg-default-456',
|
|
1616
|
+
lambdaSecurityGroupId: 'sg-lambda-789',
|
|
1617
|
+
privateSubnetId1: 'subnet-1',
|
|
1618
|
+
privateSubnetId2: 'subnet-2',
|
|
1619
|
+
natGatewayId: 'nat-123'
|
|
1620
|
+
};
|
|
1621
|
+
|
|
1622
|
+
const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
|
|
1623
|
+
|
|
1624
|
+
// Direct properties should be copied for resolver access
|
|
1625
|
+
expect(result.defaultVpcId).toBe('vpc-123');
|
|
1626
|
+
expect(result.defaultSecurityGroupId).toBe('sg-default-456');
|
|
1627
|
+
expect(result.lambdaSecurityGroupId).toBe('sg-lambda-789');
|
|
1628
|
+
expect(result.privateSubnetId1).toBe('subnet-1');
|
|
1629
|
+
expect(result.privateSubnetId2).toBe('subnet-2');
|
|
1630
|
+
expect(result.natGatewayId).toBe('nat-123');
|
|
1631
|
+
});
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
describe('VPC Endpoint Security Group with External Lambda SG', () => {
|
|
1635
|
+
it('should use external Lambda SG ID (not Ref) for VPC endpoint SG when Lambda SG is external', async () => {
|
|
1636
|
+
const appDefinition = {
|
|
1637
|
+
vpc: {
|
|
1638
|
+
enable: true,
|
|
1639
|
+
enableVPCEndpoints: true,
|
|
1640
|
+
ownership: {
|
|
1641
|
+
securityGroup: 'external' // External Lambda SG
|
|
1642
|
+
}
|
|
1643
|
+
},
|
|
1644
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' }
|
|
1645
|
+
};
|
|
1646
|
+
const discoveredResources = {
|
|
1647
|
+
fromCloudFormationStack: true,
|
|
1648
|
+
defaultVpcId: 'vpc-123',
|
|
1649
|
+
defaultSecurityGroupId: 'sg-default-456', // Default VPC SG
|
|
1650
|
+
lambdaSecurityGroupId: 'sg-stack-789', // Stack-managed SG (will be ignored)
|
|
1651
|
+
privateSubnetId1: 'subnet-1',
|
|
1652
|
+
privateSubnetId2: 'subnet-2',
|
|
1653
|
+
natGatewayId: 'nat-123',
|
|
1654
|
+
existingLogicalIds: ['FriggS3VPCEndpoint', 'FriggKMSVPCEndpoint'],
|
|
1655
|
+
s3VpcEndpointId: 'vpce-s3-stack',
|
|
1656
|
+
kmsVpcEndpointId: 'vpce-kms-stack'
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
const result = await vpcBuilder.build(appDefinition, discoveredResources);
|
|
1660
|
+
|
|
1661
|
+
// VPC Endpoint SG should be created
|
|
1662
|
+
expect(result.resources.FriggVPCEndpointSecurityGroup).toBeDefined();
|
|
1663
|
+
|
|
1664
|
+
// CRITICAL: Should use external Lambda SG ID directly, NOT a CloudFormation Ref
|
|
1665
|
+
const ingressRule = result.resources.FriggVPCEndpointSecurityGroup.Properties.SecurityGroupIngress[0];
|
|
1666
|
+
expect(ingressRule.SourceSecurityGroupId).toBe('sg-default-456'); // Direct ID, not { Ref: 'FriggLambdaSecurityGroup' }
|
|
1667
|
+
expect(typeof ingressRule.SourceSecurityGroupId).toBe('string');
|
|
1668
|
+
|
|
1669
|
+
// Verify FriggLambdaSecurityGroup is NOT in the template
|
|
1670
|
+
expect(result.resources.FriggLambdaSecurityGroup).toBeUndefined();
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
it('should use CloudFormation Ref when Lambda SG is stack-managed', async () => {
|
|
1674
|
+
const appDefinition = {
|
|
1675
|
+
vpc: {
|
|
1676
|
+
enable: true,
|
|
1677
|
+
enableVPCEndpoints: true,
|
|
1678
|
+
ownership: {
|
|
1679
|
+
securityGroup: 'stack' // Stack-managed Lambda SG
|
|
1680
|
+
}
|
|
1681
|
+
},
|
|
1682
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' }
|
|
1683
|
+
};
|
|
1684
|
+
const discoveredResources = {
|
|
1685
|
+
defaultVpcId: 'vpc-123',
|
|
1686
|
+
privateSubnetId1: 'subnet-1',
|
|
1687
|
+
privateSubnetId2: 'subnet-2'
|
|
1688
|
+
};
|
|
1689
|
+
|
|
1690
|
+
const result = await vpcBuilder.build(appDefinition, discoveredResources);
|
|
1691
|
+
|
|
1692
|
+
// VPC Endpoint SG should be created
|
|
1693
|
+
expect(result.resources.FriggVPCEndpointSecurityGroup).toBeDefined();
|
|
1694
|
+
|
|
1695
|
+
// Should use CloudFormation Ref when Lambda SG is in stack
|
|
1696
|
+
const ingressRule = result.resources.FriggVPCEndpointSecurityGroup.Properties.SecurityGroupIngress[0];
|
|
1697
|
+
expect(ingressRule.SourceSecurityGroupId).toEqual({ Ref: 'FriggLambdaSecurityGroup' });
|
|
1698
|
+
|
|
1699
|
+
// Verify FriggLambdaSecurityGroup IS in the template
|
|
1700
|
+
expect(result.resources.FriggLambdaSecurityGroup).toBeDefined();
|
|
1701
|
+
});
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
describe('convertFlatDiscoveryToStructured - VPC Endpoints from CloudFormation', () => {
|
|
1705
|
+
it('should add VPC endpoints to stackManaged when in existingLogicalIds', () => {
|
|
1706
|
+
const flatDiscovery = {
|
|
1707
|
+
fromCloudFormationStack: true,
|
|
1708
|
+
stackName: 'test-stack',
|
|
1709
|
+
existingLogicalIds: [
|
|
1710
|
+
'FriggS3VPCEndpoint',
|
|
1711
|
+
'FriggDynamoDBVPCEndpoint',
|
|
1712
|
+
'FriggKMSVPCEndpoint'
|
|
1713
|
+
],
|
|
1714
|
+
s3VpcEndpointId: 'vpce-s3-stack',
|
|
1715
|
+
dynamodbVpcEndpointId: 'vpce-ddb-stack',
|
|
1716
|
+
kmsVpcEndpointId: 'vpce-kms-stack'
|
|
1717
|
+
};
|
|
1718
|
+
|
|
1719
|
+
const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
|
|
1720
|
+
|
|
1721
|
+
// VPC endpoints should be in stackManaged (not external)
|
|
1722
|
+
expect(result.stackManaged).toContainEqual(
|
|
1723
|
+
expect.objectContaining({
|
|
1724
|
+
logicalId: 'FriggS3VPCEndpoint',
|
|
1725
|
+
physicalId: 'vpce-s3-stack',
|
|
1726
|
+
resourceType: 'AWS::EC2::VPCEndpoint'
|
|
1727
|
+
})
|
|
1728
|
+
);
|
|
1729
|
+
expect(result.stackManaged).toContainEqual(
|
|
1730
|
+
expect.objectContaining({
|
|
1731
|
+
logicalId: 'FriggDynamoDBVPCEndpoint',
|
|
1732
|
+
physicalId: 'vpce-ddb-stack',
|
|
1733
|
+
resourceType: 'AWS::EC2::VPCEndpoint'
|
|
1734
|
+
})
|
|
1735
|
+
);
|
|
1736
|
+
expect(result.stackManaged).toContainEqual(
|
|
1737
|
+
expect.objectContaining({
|
|
1738
|
+
logicalId: 'FriggKMSVPCEndpoint',
|
|
1739
|
+
physicalId: 'vpce-kms-stack',
|
|
1740
|
+
resourceType: 'AWS::EC2::VPCEndpoint'
|
|
1741
|
+
})
|
|
1742
|
+
);
|
|
1743
|
+
|
|
1744
|
+
// Should NOT be in external array
|
|
1745
|
+
expect(result.external.some(r => r.physicalId === 'vpce-s3-stack')).toBe(false);
|
|
1746
|
+
expect(result.external.some(r => r.physicalId === 'vpce-ddb-stack')).toBe(false);
|
|
1747
|
+
expect(result.external.some(r => r.physicalId === 'vpce-kms-stack')).toBe(false);
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
it('should add VPC endpoints to external when NOT in existingLogicalIds', () => {
|
|
1751
|
+
const flatDiscovery = {
|
|
1752
|
+
fromCloudFormationStack: false, // AWS API discovery
|
|
1753
|
+
s3VpcEndpointId: 'vpce-s3-external',
|
|
1754
|
+
dynamodbVpcEndpointId: 'vpce-ddb-external'
|
|
1755
|
+
};
|
|
1756
|
+
|
|
1757
|
+
const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
|
|
1758
|
+
|
|
1759
|
+
// Should be in external (AWS discovery)
|
|
1760
|
+
expect(result.external).toContainEqual(
|
|
1761
|
+
expect.objectContaining({
|
|
1762
|
+
physicalId: 'vpce-s3-external',
|
|
1763
|
+
resourceType: 'AWS::EC2::VPCEndpoint',
|
|
1764
|
+
source: 'aws-discovery'
|
|
1765
|
+
})
|
|
1766
|
+
);
|
|
1767
|
+
|
|
1768
|
+
// Should NOT be in stackManaged
|
|
1769
|
+
expect(result.stackManaged.some(r => r.physicalId === 'vpce-s3-external')).toBe(false);
|
|
1770
|
+
});
|
|
1771
|
+
|
|
1772
|
+
it('should preserve existing VPC endpoints and only create missing ones', async () => {
|
|
1773
|
+
const appDefinition = {
|
|
1774
|
+
vpc: { enable: true },
|
|
1775
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
1776
|
+
};
|
|
1777
|
+
|
|
1778
|
+
const discoveredResources = {
|
|
1779
|
+
fromCloudFormationStack: true,
|
|
1780
|
+
stackName: 'test-stack',
|
|
1781
|
+
existingLogicalIds: [
|
|
1782
|
+
'FriggS3VPCEndpoint', // In stack
|
|
1783
|
+
'FriggDynamoDBVPCEndpoint', // In stack
|
|
1784
|
+
'FriggKMSVPCEndpoint' // In stack
|
|
1785
|
+
// SecretsManager and SQS NOT in stack (were deleted)
|
|
1786
|
+
],
|
|
1787
|
+
defaultVpcId: 'vpc-123',
|
|
1788
|
+
privateSubnetId1: 'subnet-1',
|
|
1789
|
+
privateSubnetId2: 'subnet-2',
|
|
1790
|
+
lambdaSecurityGroupId: 'sg-123',
|
|
1791
|
+
routeTableId: 'rtb-123',
|
|
1792
|
+
// Endpoints in stack
|
|
1793
|
+
s3VpcEndpointId: 'vpce-s3-existing',
|
|
1794
|
+
dynamodbVpcEndpointId: 'vpce-ddb-existing',
|
|
1795
|
+
kmsVpcEndpointId: 'vpce-kms-existing'
|
|
1796
|
+
// secretsManagerVpcEndpointId and sqsVpcEndpointId NOT present
|
|
1797
|
+
};
|
|
1798
|
+
|
|
1799
|
+
const result = await vpcBuilder.build(appDefinition, discoveredResources);
|
|
1800
|
+
|
|
1801
|
+
// Existing endpoints MUST be in template (re-added)
|
|
1802
|
+
expect(result.resources.FriggS3VPCEndpoint).toBeDefined();
|
|
1803
|
+
expect(result.resources.FriggS3VPCEndpoint.Properties.VpcId).toBe('vpc-123');
|
|
1804
|
+
|
|
1805
|
+
expect(result.resources.FriggDynamoDBVPCEndpoint).toBeDefined();
|
|
1806
|
+
expect(result.resources.FriggDynamoDBVPCEndpoint.Properties.VpcId).toBe('vpc-123');
|
|
1807
|
+
|
|
1808
|
+
expect(result.resources.FriggKMSVPCEndpoint).toBeDefined();
|
|
1809
|
+
expect(result.resources.FriggKMSVPCEndpoint.Properties.VpcId).toBe('vpc-123');
|
|
1810
|
+
|
|
1811
|
+
// Missing endpoints should also be created
|
|
1812
|
+
expect(result.resources.FriggSecretsManagerVPCEndpoint).toBeDefined();
|
|
1813
|
+
expect(result.resources.FriggSQSVPCEndpoint).toBeDefined();
|
|
1814
|
+
|
|
1815
|
+
// VPC Endpoint Security Group should be created
|
|
1816
|
+
expect(result.resources.FriggVPCEndpointSecurityGroup).toBeDefined();
|
|
1817
|
+
});
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
describe('convertFlatDiscoveryToStructured - CloudFormation query results', () => {
|
|
1821
|
+
it('should add VPC from CloudFormation query to external array', () => {
|
|
1822
|
+
const flatDiscovery = {
|
|
1823
|
+
fromCloudFormationStack: true,
|
|
1824
|
+
stackName: 'test-stack',
|
|
1825
|
+
existingLogicalIds: ['FriggLambdaRouteTable', 'FriggLambdaSecurityGroup'],
|
|
1826
|
+
// VPC ID was extracted from security group query (NOT a stack resource)
|
|
1827
|
+
defaultVpcId: 'vpc-extracted-from-sg',
|
|
1828
|
+
lambdaSecurityGroupId: 'sg-123',
|
|
1829
|
+
routeTableId: 'rtb-123'
|
|
1830
|
+
};
|
|
1831
|
+
|
|
1832
|
+
const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
|
|
1833
|
+
|
|
1834
|
+
// VPC should be in external array (discovered via query, not in stack)
|
|
1835
|
+
const vpcExternal = result.external.find(r => r.resourceType === 'AWS::EC2::VPC');
|
|
1836
|
+
expect(vpcExternal).toBeDefined();
|
|
1837
|
+
expect(vpcExternal.physicalId).toBe('vpc-extracted-from-sg');
|
|
1838
|
+
expect(vpcExternal.source).toBe('cloudformation-query');
|
|
1839
|
+
|
|
1840
|
+
// Security group SHOULD be in stackManaged (is in stack)
|
|
1841
|
+
const sgStack = result.stackManaged.find(r => r.logicalId === 'FriggLambdaSecurityGroup');
|
|
1842
|
+
expect(sgStack).toBeDefined();
|
|
1843
|
+
expect(sgStack.physicalId).toBe('sg-123');
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1846
|
+
it('should add subnets from route table associations to external array', () => {
|
|
1847
|
+
const flatDiscovery = {
|
|
1848
|
+
fromCloudFormationStack: true,
|
|
1849
|
+
stackName: 'test-stack',
|
|
1850
|
+
existingLogicalIds: ['FriggLambdaRouteTable'],
|
|
1851
|
+
routeTableId: 'rtb-123',
|
|
1852
|
+
// Subnets extracted from route table associations (NOT stack resources)
|
|
1853
|
+
privateSubnetId1: 'subnet-1',
|
|
1854
|
+
privateSubnetId2: 'subnet-2'
|
|
1855
|
+
};
|
|
1856
|
+
|
|
1857
|
+
const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
|
|
1858
|
+
|
|
1859
|
+
// Subnets should be in external array
|
|
1860
|
+
const subnet1 = result.external.find(r => r.physicalId === 'subnet-1');
|
|
1861
|
+
const subnet2 = result.external.find(r => r.physicalId === 'subnet-2');
|
|
1862
|
+
|
|
1863
|
+
expect(subnet1).toBeDefined();
|
|
1864
|
+
expect(subnet1.resourceType).toBe('AWS::EC2::Subnet');
|
|
1865
|
+
expect(subnet1.source).toBe('cloudformation-query');
|
|
1866
|
+
|
|
1867
|
+
expect(subnet2).toBeDefined();
|
|
1868
|
+
expect(subnet2.resourceType).toBe('AWS::EC2::Subnet');
|
|
1869
|
+
expect(subnet2.source).toBe('cloudformation-query');
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
it('should add NAT Gateway from route table queries to external array', () => {
|
|
1873
|
+
const flatDiscovery = {
|
|
1874
|
+
fromCloudFormationStack: true,
|
|
1875
|
+
stackName: 'test-stack',
|
|
1876
|
+
existingLogicalIds: ['FriggLambdaRouteTable', 'FriggPrivateRoute'],
|
|
1877
|
+
routeTableId: 'rtb-123',
|
|
1878
|
+
// NAT Gateway extracted from route table routes (NOT a stack resource)
|
|
1879
|
+
existingNatGatewayId: 'nat-extracted'
|
|
1880
|
+
};
|
|
1881
|
+
|
|
1882
|
+
const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
|
|
1883
|
+
|
|
1884
|
+
// NAT should be in external array
|
|
1885
|
+
const natExternal = result.external.find(r => r.resourceType === 'AWS::EC2::NatGateway');
|
|
1886
|
+
expect(natExternal).toBeDefined();
|
|
1887
|
+
expect(natExternal.physicalId).toBe('nat-extracted');
|
|
1888
|
+
expect(natExternal.source).toBe('cloudformation-query');
|
|
1889
|
+
});
|
|
1890
|
+
|
|
1891
|
+
it('should NOT add resources to external if they are in stack', () => {
|
|
1892
|
+
const flatDiscovery = {
|
|
1893
|
+
fromCloudFormationStack: true,
|
|
1894
|
+
stackName: 'test-stack',
|
|
1895
|
+
existingLogicalIds: ['FriggVPC', 'FriggPrivateSubnet1'],
|
|
1896
|
+
// These ARE in the stack
|
|
1897
|
+
defaultVpcId: 'vpc-in-stack',
|
|
1898
|
+
privateSubnetId1: 'subnet-in-stack'
|
|
1899
|
+
};
|
|
1900
|
+
|
|
1901
|
+
const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
|
|
1902
|
+
|
|
1903
|
+
// Should be in stackManaged, NOT external
|
|
1904
|
+
expect(result.stackManaged.some(r => r.logicalId === 'FriggVPC')).toBe(true);
|
|
1905
|
+
expect(result.stackManaged.some(r => r.logicalId === 'FriggPrivateSubnet1')).toBe(true);
|
|
1906
|
+
|
|
1907
|
+
// Should NOT be in external
|
|
1908
|
+
expect(result.external.some(r => r.physicalId === 'vpc-in-stack')).toBe(false);
|
|
1909
|
+
expect(result.external.some(r => r.physicalId === 'subnet-in-stack')).toBe(false);
|
|
1910
|
+
});
|
|
1911
|
+
|
|
1912
|
+
it('should handle external VPC pattern: stack resources + queried external references', () => {
|
|
1913
|
+
const flatDiscovery = {
|
|
1914
|
+
fromCloudFormationStack: true,
|
|
1915
|
+
stackName: 'test-production-stack',
|
|
1916
|
+
existingLogicalIds: [
|
|
1917
|
+
'FriggLambdaSecurityGroup',
|
|
1918
|
+
'FriggLambdaRouteTable',
|
|
1919
|
+
'FriggPrivateRoute',
|
|
1920
|
+
'FriggPrivateSubnet1RouteTableAssociation',
|
|
1921
|
+
'FriggPrivateSubnet2RouteTableAssociation',
|
|
1922
|
+
'FriggS3VPCEndpoint',
|
|
1923
|
+
'FriggDynamoDBVPCEndpoint',
|
|
1924
|
+
'FriggKMSVPCEndpoint'
|
|
1925
|
+
],
|
|
1926
|
+
// Stack resources
|
|
1927
|
+
lambdaSecurityGroupId: 'sg-stack-123',
|
|
1928
|
+
routeTableId: 'rtb-stack-456',
|
|
1929
|
+
s3VpcEndpointId: 'vpce-s3-stack',
|
|
1930
|
+
// External resources (discovered via queries)
|
|
1931
|
+
defaultVpcId: 'vpc-external-123',
|
|
1932
|
+
privateSubnetId1: 'subnet-external-1',
|
|
1933
|
+
privateSubnetId2: 'subnet-external-2',
|
|
1934
|
+
existingNatGatewayId: 'nat-external-789'
|
|
1935
|
+
};
|
|
1936
|
+
|
|
1937
|
+
const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
|
|
1938
|
+
|
|
1939
|
+
// Stack resources should be in stackManaged
|
|
1940
|
+
expect(result.stackManaged).toEqual(
|
|
1941
|
+
expect.arrayContaining([
|
|
1942
|
+
expect.objectContaining({ logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-stack-123' }),
|
|
1943
|
+
expect.objectContaining({ logicalId: 'FriggLambdaRouteTable', physicalId: 'rtb-stack-456' }),
|
|
1944
|
+
expect.objectContaining({ logicalId: 'FriggS3VPCEndpoint', physicalId: 'vpce-s3-stack' })
|
|
1945
|
+
])
|
|
1946
|
+
);
|
|
1947
|
+
|
|
1948
|
+
// External resources should be in external array
|
|
1949
|
+
expect(result.external).toEqual(
|
|
1950
|
+
expect.arrayContaining([
|
|
1951
|
+
expect.objectContaining({ physicalId: 'vpc-external-123', resourceType: 'AWS::EC2::VPC', source: 'cloudformation-query' }),
|
|
1952
|
+
expect.objectContaining({ physicalId: 'subnet-external-1', resourceType: 'AWS::EC2::Subnet', source: 'cloudformation-query' }),
|
|
1953
|
+
expect.objectContaining({ physicalId: 'subnet-external-2', resourceType: 'AWS::EC2::Subnet', source: 'cloudformation-query' }),
|
|
1954
|
+
expect.objectContaining({ physicalId: 'nat-external-789', resourceType: 'AWS::EC2::NatGateway', source: 'cloudformation-query' })
|
|
1955
|
+
])
|
|
1956
|
+
);
|
|
1957
|
+
});
|
|
1958
|
+
});
|
|
1261
1959
|
});
|
|
1262
1960
|
|