@friggframework/devtools 2.0.0-next.52 → 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 -10
- package/infrastructure/domains/integration/integration-builder.test.js +97 -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 +2 -2
- package/package.json +7 -7
|
@@ -586,5 +586,400 @@ 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 external VPC 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
|
+
|
|
808
|
+
describe('existingLogicalIds tracking', () => {
|
|
809
|
+
it('should track OLD VPC endpoint logical IDs (VPCEndpointS3 pattern) for backwards compatibility', async () => {
|
|
810
|
+
// CRITICAL: Frontify production uses OLD naming convention
|
|
811
|
+
const mockStack = {
|
|
812
|
+
StackName: 'create-frigg-app-production',
|
|
813
|
+
Outputs: []
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
const mockResources = [
|
|
817
|
+
{ LogicalResourceId: 'FriggLambdaRouteTable', PhysicalResourceId: 'rtb-123', ResourceType: 'AWS::EC2::RouteTable' },
|
|
818
|
+
{ LogicalResourceId: 'FriggNATRoute', PhysicalResourceId: 'rtb-123|0.0.0.0/0', ResourceType: 'AWS::EC2::Route' },
|
|
819
|
+
{ LogicalResourceId: 'FriggSubnet1RouteAssociation', PhysicalResourceId: 'rtbassoc-1', ResourceType: 'AWS::EC2::SubnetRouteTableAssociation' },
|
|
820
|
+
{ LogicalResourceId: 'FriggSubnet2RouteAssociation', PhysicalResourceId: 'rtbassoc-2', ResourceType: 'AWS::EC2::SubnetRouteTableAssociation' },
|
|
821
|
+
{ LogicalResourceId: 'VPCEndpointS3', PhysicalResourceId: 'vpce-s3-123', ResourceType: 'AWS::EC2::VPCEndpoint' },
|
|
822
|
+
{ LogicalResourceId: 'VPCEndpointDynamoDB', PhysicalResourceId: 'vpce-ddb-123', ResourceType: 'AWS::EC2::VPCEndpoint' }
|
|
823
|
+
];
|
|
824
|
+
|
|
825
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
826
|
+
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
827
|
+
|
|
828
|
+
const result = await cfDiscovery.discoverFromStack('create-frigg-app-production');
|
|
829
|
+
|
|
830
|
+
// CRITICAL: existingLogicalIds MUST contain old VPC endpoint names
|
|
831
|
+
expect(result.existingLogicalIds).toBeDefined();
|
|
832
|
+
expect(result.existingLogicalIds).toContain('FriggNATRoute');
|
|
833
|
+
expect(result.existingLogicalIds).toContain('FriggSubnet1RouteAssociation');
|
|
834
|
+
expect(result.existingLogicalIds).toContain('FriggSubnet2RouteAssociation');
|
|
835
|
+
expect(result.existingLogicalIds).toContain('VPCEndpointS3'); // OLD naming
|
|
836
|
+
expect(result.existingLogicalIds).toContain('VPCEndpointDynamoDB'); // OLD naming
|
|
837
|
+
|
|
838
|
+
// Should also have the flat discovery properties
|
|
839
|
+
expect(result.routeTableId).toBe('rtb-123');
|
|
840
|
+
expect(result.natRoute).toBe('rtb-123|0.0.0.0/0');
|
|
841
|
+
expect(result.s3VpcEndpointId).toBe('vpce-s3-123');
|
|
842
|
+
expect(result.dynamodbVpcEndpointId).toBe('vpce-ddb-123');
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it('should track NEW VPC endpoint logical IDs (FriggS3VPCEndpoint pattern) for newer stacks', async () => {
|
|
846
|
+
const mockStack = {
|
|
847
|
+
StackName: 'test-stack',
|
|
848
|
+
Outputs: []
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
const mockResources = [
|
|
852
|
+
{ LogicalResourceId: 'FriggLambdaRouteTable', PhysicalResourceId: 'rtb-456', ResourceType: 'AWS::EC2::RouteTable' },
|
|
853
|
+
{ LogicalResourceId: 'FriggPrivateRoute', PhysicalResourceId: 'rtb-456|0.0.0.0/0', ResourceType: 'AWS::EC2::Route' },
|
|
854
|
+
{ LogicalResourceId: 'FriggS3VPCEndpoint', PhysicalResourceId: 'vpce-s3-456', ResourceType: 'AWS::EC2::VPCEndpoint' },
|
|
855
|
+
{ LogicalResourceId: 'FriggDynamoDBVPCEndpoint', PhysicalResourceId: 'vpce-ddb-456', ResourceType: 'AWS::EC2::VPCEndpoint' },
|
|
856
|
+
{ LogicalResourceId: 'FriggKMSVPCEndpoint', PhysicalResourceId: 'vpce-kms-456', ResourceType: 'AWS::EC2::VPCEndpoint' }
|
|
857
|
+
];
|
|
858
|
+
|
|
859
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
860
|
+
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
861
|
+
|
|
862
|
+
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
863
|
+
|
|
864
|
+
// Should track NEW naming pattern in existingLogicalIds
|
|
865
|
+
expect(result.existingLogicalIds).toContain('FriggPrivateRoute');
|
|
866
|
+
expect(result.existingLogicalIds).toContain('FriggS3VPCEndpoint');
|
|
867
|
+
expect(result.existingLogicalIds).toContain('FriggDynamoDBVPCEndpoint');
|
|
868
|
+
expect(result.existingLogicalIds).toContain('FriggKMSVPCEndpoint');
|
|
869
|
+
|
|
870
|
+
// Should NOT contain old naming patterns
|
|
871
|
+
expect(result.existingLogicalIds).not.toContain('FriggNATRoute');
|
|
872
|
+
expect(result.existingLogicalIds).not.toContain('VPCEndpointS3');
|
|
873
|
+
});
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
describe('Subnet extraction from VPC query (OLD reliable approach)', () => {
|
|
877
|
+
it('should extract subnets by querying ALL subnets in VPC then filtering by route table', async () => {
|
|
878
|
+
// Tests the proven method from aws-discovery.js
|
|
879
|
+
// 1. Query ALL subnets in VPC using vpc-id filter (not association filter!)
|
|
880
|
+
// 2. Query route table by ID (RouteTableIds parameter, not Filters!)
|
|
881
|
+
// 3. Extract subnet IDs from route table's Associations array
|
|
882
|
+
|
|
883
|
+
const mockStack = {
|
|
884
|
+
StackName: 'test-stack',
|
|
885
|
+
Outputs: []
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
const mockResources = [
|
|
889
|
+
{ LogicalResourceId: 'FriggLambdaRouteTable', PhysicalResourceId: 'rtb-123', ResourceType: 'AWS::EC2::RouteTable' },
|
|
890
|
+
{ LogicalResourceId: 'FriggVPC', PhysicalResourceId: 'vpc-456', ResourceType: 'AWS::EC2::VPC' }
|
|
891
|
+
];
|
|
892
|
+
|
|
893
|
+
const sendMock = jest.fn();
|
|
894
|
+
sendMock
|
|
895
|
+
.mockResolvedValueOnce({
|
|
896
|
+
RouteTables: [{
|
|
897
|
+
RouteTableId: 'rtb-123',
|
|
898
|
+
VpcId: 'vpc-456',
|
|
899
|
+
Associations: [],
|
|
900
|
+
Routes: [{ NatGatewayId: 'nat-789', DestinationCidrBlock: '0.0.0.0/0' }]
|
|
901
|
+
}]
|
|
902
|
+
})
|
|
903
|
+
.mockResolvedValueOnce({ SecurityGroups: [{ GroupId: 'sg-default' }] })
|
|
904
|
+
.mockResolvedValueOnce({
|
|
905
|
+
Subnets: [
|
|
906
|
+
{ SubnetId: 'subnet-aaa', VpcId: 'vpc-456', AvailabilityZone: 'us-east-1a' },
|
|
907
|
+
{ SubnetId: 'subnet-bbb', VpcId: 'vpc-456', AvailabilityZone: 'us-east-1b' },
|
|
908
|
+
{ SubnetId: 'subnet-ccc', VpcId: 'vpc-456', AvailabilityZone: 'us-east-1c' }
|
|
909
|
+
]
|
|
910
|
+
})
|
|
911
|
+
.mockResolvedValueOnce({
|
|
912
|
+
RouteTables: [{
|
|
913
|
+
RouteTableId: 'rtb-123',
|
|
914
|
+
Associations: [
|
|
915
|
+
{ RouteTableAssociationId: 'rtbassoc-111', SubnetId: 'subnet-aaa' },
|
|
916
|
+
{ RouteTableAssociationId: 'rtbassoc-222', SubnetId: 'subnet-bbb' }
|
|
917
|
+
]
|
|
918
|
+
}]
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
const mockEC2Client = { send: sendMock };
|
|
922
|
+
|
|
923
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
924
|
+
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
925
|
+
mockProvider.getEC2Client = jest.fn().mockReturnValue(mockEC2Client);
|
|
926
|
+
|
|
927
|
+
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
928
|
+
|
|
929
|
+
// Should have extracted subnets using VPC query approach
|
|
930
|
+
expect(result.privateSubnetId1).toBe('subnet-aaa');
|
|
931
|
+
expect(result.privateSubnetId2).toBe('subnet-bbb');
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
it('should handle VPC with only 1 associated subnet (use second as fallback)', async () => {
|
|
935
|
+
const mockStack = {
|
|
936
|
+
StackName: 'test-stack',
|
|
937
|
+
Outputs: []
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
const mockResources = [
|
|
941
|
+
{ LogicalResourceId: 'FriggLambdaRouteTable', PhysicalResourceId: 'rtb-123', ResourceType: 'AWS::EC2::RouteTable' },
|
|
942
|
+
{ LogicalResourceId: 'FriggVPC', PhysicalResourceId: 'vpc-456', ResourceType: 'AWS::EC2::VPC' }
|
|
943
|
+
];
|
|
944
|
+
|
|
945
|
+
const sendMock = jest.fn();
|
|
946
|
+
sendMock
|
|
947
|
+
.mockResolvedValueOnce({
|
|
948
|
+
RouteTables: [{
|
|
949
|
+
RouteTableId: 'rtb-123',
|
|
950
|
+
VpcId: 'vpc-456',
|
|
951
|
+
Associations: [],
|
|
952
|
+
Routes: [{ NatGatewayId: 'nat-789', DestinationCidrBlock: '0.0.0.0/0' }]
|
|
953
|
+
}]
|
|
954
|
+
})
|
|
955
|
+
.mockResolvedValueOnce({ SecurityGroups: [{ GroupId: 'sg-default' }] })
|
|
956
|
+
.mockResolvedValueOnce({
|
|
957
|
+
Subnets: [
|
|
958
|
+
{ SubnetId: 'subnet-aaa', VpcId: 'vpc-456' },
|
|
959
|
+
{ SubnetId: 'subnet-bbb', VpcId: 'vpc-456' }
|
|
960
|
+
]
|
|
961
|
+
})
|
|
962
|
+
.mockResolvedValueOnce({
|
|
963
|
+
RouteTables: [{
|
|
964
|
+
RouteTableId: 'rtb-123',
|
|
965
|
+
Associations: [
|
|
966
|
+
{ RouteTableAssociationId: 'rtbassoc-111', SubnetId: 'subnet-aaa' }
|
|
967
|
+
]
|
|
968
|
+
}]
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
const mockEC2Client = { send: sendMock };
|
|
972
|
+
|
|
973
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
974
|
+
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
975
|
+
mockProvider.getEC2Client = jest.fn().mockReturnValue(mockEC2Client);
|
|
976
|
+
|
|
977
|
+
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
978
|
+
|
|
979
|
+
// Should use first from route table, second from fallback
|
|
980
|
+
expect(result.privateSubnetId1).toBe('subnet-aaa');
|
|
981
|
+
expect(result.privateSubnetId2).toBe('subnet-bbb');
|
|
982
|
+
});
|
|
983
|
+
});
|
|
589
984
|
});
|
|
590
985
|
|
|
@@ -20,7 +20,7 @@ let KMSClient, ListKeysCommand, DescribeKeyCommand, ListAliasesCommand;
|
|
|
20
20
|
let RDSClient, DescribeDBClustersCommand, DescribeDBInstancesCommand;
|
|
21
21
|
let SSMClient, GetParameterCommand, GetParametersByPathCommand;
|
|
22
22
|
let SecretsManagerClient, ListSecretsCommand, GetSecretValueCommand;
|
|
23
|
-
let CloudFormationClient, DescribeStacksCommand, ListStackResourcesCommand;
|
|
23
|
+
let CloudFormationClient, DescribeStacksCommand, ListStackResourcesCommand, GetTemplateCommand;
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Lazy load EC2 SDK
|
|
@@ -518,24 +518,59 @@ class AWSProviderAdapter extends CloudProviderAdapter {
|
|
|
518
518
|
|
|
519
519
|
/**
|
|
520
520
|
* List CloudFormation stack resources
|
|
521
|
+
* Handles pagination to retrieve all resources (CloudFormation limits to 1 MB per page)
|
|
521
522
|
*
|
|
522
523
|
* @param {string} stackName - Name of the CloudFormation stack
|
|
523
|
-
* @returns {Promise<Array>} List of stack resources
|
|
524
|
+
* @returns {Promise<Array>} List of all stack resources across all pages
|
|
524
525
|
*/
|
|
525
526
|
async listStackResources(stackName) {
|
|
526
527
|
const cf = this.getCloudFormationClient();
|
|
528
|
+
const allResources = [];
|
|
529
|
+
let nextToken = null;
|
|
527
530
|
|
|
528
531
|
try {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
+
do {
|
|
533
|
+
const response = await cf.send(new ListStackResourcesCommand({
|
|
534
|
+
StackName: stackName,
|
|
535
|
+
NextToken: nextToken
|
|
536
|
+
}));
|
|
537
|
+
|
|
538
|
+
if (response.StackResourceSummaries) {
|
|
539
|
+
allResources.push(...response.StackResourceSummaries);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
nextToken = response.NextToken || null;
|
|
543
|
+
} while (nextToken);
|
|
532
544
|
|
|
533
|
-
return
|
|
545
|
+
return allResources;
|
|
534
546
|
} catch (error) {
|
|
535
547
|
console.warn(`Failed to list stack resources for ${stackName}:`, error.message);
|
|
536
548
|
return [];
|
|
537
549
|
}
|
|
538
550
|
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Describe a specific stack resource to get its full details including properties
|
|
554
|
+
* @param {string} stackName - Stack name
|
|
555
|
+
* @param {string} logicalResourceId - Logical resource ID
|
|
556
|
+
* @returns {Promise<Object>} Resource details
|
|
557
|
+
*/
|
|
558
|
+
async describeStackResource(stackName, logicalResourceId) {
|
|
559
|
+
const cf = this.getCloudFormationClient();
|
|
560
|
+
|
|
561
|
+
try {
|
|
562
|
+
const { DescribeStackResourceCommand } = require('@aws-sdk/client-cloudformation');
|
|
563
|
+
const response = await cf.send(new DescribeStackResourceCommand({
|
|
564
|
+
StackName: stackName,
|
|
565
|
+
LogicalResourceId: logicalResourceId,
|
|
566
|
+
}));
|
|
567
|
+
|
|
568
|
+
return response.StackResourceDetail || null;
|
|
569
|
+
} catch (error) {
|
|
570
|
+
console.warn(`Failed to describe stack resource ${logicalResourceId}:`, error.message);
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
539
574
|
}
|
|
540
575
|
|
|
541
576
|
module.exports = {
|
|
@@ -14,6 +14,45 @@ jest.mock('@aws-sdk/client-ssm');
|
|
|
14
14
|
jest.mock('@aws-sdk/client-secrets-manager');
|
|
15
15
|
|
|
16
16
|
describe('AWSProviderAdapter', () => {
|
|
17
|
+
describe('listStackResources', () => {
|
|
18
|
+
it.skip('should handle pagination and return all resources across multiple pages', async () => {
|
|
19
|
+
const mockCfClient = {
|
|
20
|
+
send: jest.fn()
|
|
21
|
+
.mockResolvedValueOnce({
|
|
22
|
+
StackResourceSummaries: [
|
|
23
|
+
{ LogicalResourceId: 'Resource1', PhysicalResourceId: 'res-1', ResourceType: 'AWS::EC2::VPC' },
|
|
24
|
+
{ LogicalResourceId: 'Resource2', PhysicalResourceId: 'res-2', ResourceType: 'AWS::EC2::Subnet' }
|
|
25
|
+
],
|
|
26
|
+
NextToken: 'token-page-2' // More pages available
|
|
27
|
+
})
|
|
28
|
+
.mockResolvedValueOnce({
|
|
29
|
+
StackResourceSummaries: [
|
|
30
|
+
{ LogicalResourceId: 'VPCEndpointS3', PhysicalResourceId: 'vpce-s3', ResourceType: 'AWS::EC2::VPCEndpoint' },
|
|
31
|
+
{ LogicalResourceId: 'VPCEndpointDynamoDB', PhysicalResourceId: 'vpce-ddb', ResourceType: 'AWS::EC2::VPCEndpoint' }
|
|
32
|
+
],
|
|
33
|
+
NextToken: null // Last page
|
|
34
|
+
})
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const provider = new AWSProviderAdapter('us-east-1');
|
|
38
|
+
provider.getCloudFormationClient = jest.fn().mockReturnValue(mockCfClient);
|
|
39
|
+
|
|
40
|
+
const resources = await provider.listStackResources('test-stack');
|
|
41
|
+
|
|
42
|
+
// Should have ALL resources from ALL pages
|
|
43
|
+
expect(resources).toHaveLength(4);
|
|
44
|
+
expect(resources.map(r => r.LogicalResourceId)).toEqual([
|
|
45
|
+
'Resource1',
|
|
46
|
+
'Resource2',
|
|
47
|
+
'VPCEndpointS3',
|
|
48
|
+
'VPCEndpointDynamoDB'
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
// Should have called CloudFormation twice (once for each page)
|
|
52
|
+
expect(mockCfClient.send).toHaveBeenCalledTimes(2);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
17
56
|
let provider;
|
|
18
57
|
|
|
19
58
|
beforeEach(() => {
|
|
@@ -95,11 +95,23 @@ async function gatherDiscoveredResources(appDefinition) {
|
|
|
95
95
|
const cfDiscovery = new CloudFormationDiscovery(provider, { serviceName, stage });
|
|
96
96
|
const stackResources = await cfDiscovery.discoverFromStack(stackName);
|
|
97
97
|
|
|
98
|
-
// Validate CF discovery results -
|
|
99
|
-
const hasVpcData = stackResources?.defaultVpcId;
|
|
100
|
-
const hasKmsData = stackResources?.defaultKmsKeyId;
|
|
101
|
-
const hasAuroraData = stackResources?.auroraClusterId;
|
|
102
|
-
|
|
98
|
+
// Validate CF discovery results - check for ANY useful infrastructure
|
|
99
|
+
const hasVpcData = stackResources?.defaultVpcId; // VPC resource in stack
|
|
100
|
+
const hasKmsData = stackResources?.defaultKmsKeyId; // KMS resource in stack
|
|
101
|
+
const hasAuroraData = stackResources?.auroraClusterId; // Aurora in stack
|
|
102
|
+
|
|
103
|
+
// Check for routing infrastructure (proves VPC config exists even with external VPC)
|
|
104
|
+
const hasRoutingInfra = stackResources?.routeTableId || // FriggLambdaRouteTable
|
|
105
|
+
stackResources?.natRoute || // FriggNATRoute
|
|
106
|
+
stackResources?.vpcEndpoints?.s3 || // VPC endpoints
|
|
107
|
+
stackResources?.vpcEndpoints?.dynamodb;
|
|
108
|
+
|
|
109
|
+
// Stack is useful if it has EITHER actual resources OR routing infrastructure
|
|
110
|
+
const hasSomeUsefulData = hasVpcData || hasKmsData || hasAuroraData || hasRoutingInfra;
|
|
111
|
+
|
|
112
|
+
if (hasRoutingInfra && !hasVpcData) {
|
|
113
|
+
console.log(' ✓ Found VPC routing infrastructure in stack (external VPC pattern)');
|
|
114
|
+
}
|
|
103
115
|
|
|
104
116
|
// Check if we're in isolated mode (each stage gets its own VPC/Aurora)
|
|
105
117
|
const isIsolatedMode = appDefinition.managementMode === 'managed' &&
|
|
@@ -415,6 +415,42 @@ 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
|
+
|
|
418
454
|
it('should include secrets in SSM discovery by default', async () => {
|
|
419
455
|
const appDefinition = {
|
|
420
456
|
ssm: { enable: true },
|