@friggframework/devtools 2.0.0--canary.428.3bab734.0 → 2.0.0--canary.428.08c3f6f.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.
|
@@ -12,7 +12,8 @@ jest.mock('./aws-discovery', () => {
|
|
|
12
12
|
privateSubnetId2: 'subnet-789012',
|
|
13
13
|
publicSubnetId: 'subnet-public',
|
|
14
14
|
defaultRouteTableId: 'rtb-123456',
|
|
15
|
-
defaultKmsKeyId: 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012'
|
|
15
|
+
defaultKmsKeyId: 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012',
|
|
16
|
+
existingNatGatewayId: 'nat-default123' // Add default NAT Gateway for discover mode
|
|
16
17
|
})
|
|
17
18
|
};
|
|
18
19
|
})
|
|
@@ -110,7 +111,210 @@ describe('composeServerlessDefinition', () => {
|
|
|
110
111
|
});
|
|
111
112
|
|
|
112
113
|
describe('VPC Configuration', () => {
|
|
113
|
-
it('should add VPC configuration when vpc.enable is true', async () => {
|
|
114
|
+
it('should add VPC configuration when vpc.enable is true with discover mode', async () => {
|
|
115
|
+
const appDefinition = {
|
|
116
|
+
vpc: {
|
|
117
|
+
enable: true,
|
|
118
|
+
management: 'discover'
|
|
119
|
+
},
|
|
120
|
+
integrations: []
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
124
|
+
|
|
125
|
+
expect(result.provider.vpc).toBeDefined();
|
|
126
|
+
expect(result.provider.vpc.securityGroupIds).toEqual(['sg-123456']);
|
|
127
|
+
expect(result.provider.vpc.subnetIds).toEqual(['subnet-123456', 'subnet-789012']);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should create new VPC infrastructure when management is create-new', async () => {
|
|
131
|
+
const appDefinition = {
|
|
132
|
+
vpc: {
|
|
133
|
+
enable: true,
|
|
134
|
+
management: 'create-new'
|
|
135
|
+
},
|
|
136
|
+
integrations: []
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
140
|
+
|
|
141
|
+
expect(result.provider.vpc).toBeDefined();
|
|
142
|
+
expect(result.provider.vpc.securityGroupIds).toEqual([{ Ref: 'FriggLambdaSecurityGroup' }]);
|
|
143
|
+
expect(result.provider.vpc.subnetIds).toEqual([
|
|
144
|
+
{ Ref: 'FriggPrivateSubnet1' },
|
|
145
|
+
{ Ref: 'FriggPrivateSubnet2' }
|
|
146
|
+
]);
|
|
147
|
+
expect(result.resources.Resources.FriggVPC).toBeDefined();
|
|
148
|
+
expect(result.resources.Resources.FriggLambdaSecurityGroup).toBeDefined();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should use provided VPC resources when management is use-existing', async () => {
|
|
152
|
+
const appDefinition = {
|
|
153
|
+
vpc: {
|
|
154
|
+
enable: true,
|
|
155
|
+
management: 'use-existing',
|
|
156
|
+
vpcId: 'vpc-custom123',
|
|
157
|
+
subnets: {
|
|
158
|
+
ids: ['subnet-custom1', 'subnet-custom2']
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
integrations: []
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
165
|
+
|
|
166
|
+
expect(result.provider.vpc).toBeDefined();
|
|
167
|
+
expect(result.provider.vpc.subnetIds).toEqual(['subnet-custom1', 'subnet-custom2']);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Test all 9 combinations of VPC and Subnet management modes
|
|
171
|
+
it('should handle create-new VPC with create subnets', async () => {
|
|
172
|
+
const appDefinition = {
|
|
173
|
+
vpc: {
|
|
174
|
+
enable: true,
|
|
175
|
+
management: 'create-new',
|
|
176
|
+
subnets: { management: 'create' }
|
|
177
|
+
},
|
|
178
|
+
integrations: []
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
182
|
+
|
|
183
|
+
expect(result.resources.Resources.FriggVPC).toBeDefined();
|
|
184
|
+
expect(result.resources.Resources.FriggPrivateSubnet1).toBeDefined();
|
|
185
|
+
expect(result.resources.Resources.FriggPrivateSubnet2).toBeDefined();
|
|
186
|
+
expect(result.provider.vpc.subnetIds).toEqual([
|
|
187
|
+
{ Ref: 'FriggPrivateSubnet1' },
|
|
188
|
+
{ Ref: 'FriggPrivateSubnet2' }
|
|
189
|
+
]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should handle discover VPC with create subnets', async () => {
|
|
193
|
+
const appDefinition = {
|
|
194
|
+
vpc: {
|
|
195
|
+
enable: true,
|
|
196
|
+
management: 'discover',
|
|
197
|
+
subnets: { management: 'create' }
|
|
198
|
+
},
|
|
199
|
+
integrations: []
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
203
|
+
|
|
204
|
+
expect(result.resources.Resources.FriggVPC).toBeUndefined();
|
|
205
|
+
expect(result.resources.Resources.FriggPrivateSubnet1).toBeDefined();
|
|
206
|
+
expect(result.resources.Resources.FriggPrivateSubnet2).toBeDefined();
|
|
207
|
+
expect(result.resources.Resources.FriggPrivateSubnet1.Properties.VpcId).toBe('vpc-123456');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should handle use-existing VPC with create subnets', async () => {
|
|
211
|
+
const appDefinition = {
|
|
212
|
+
vpc: {
|
|
213
|
+
enable: true,
|
|
214
|
+
management: 'use-existing',
|
|
215
|
+
vpcId: 'vpc-existing123',
|
|
216
|
+
subnets: { management: 'create' }
|
|
217
|
+
},
|
|
218
|
+
integrations: []
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
222
|
+
|
|
223
|
+
expect(result.resources.Resources.FriggVPC).toBeUndefined();
|
|
224
|
+
expect(result.resources.Resources.FriggPrivateSubnet1).toBeDefined();
|
|
225
|
+
expect(result.resources.Resources.FriggPrivateSubnet2).toBeDefined();
|
|
226
|
+
expect(result.resources.Resources.FriggPrivateSubnet1.Properties.VpcId).toBe('vpc-existing123');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should handle use-existing VPC with use-existing subnets', async () => {
|
|
230
|
+
const appDefinition = {
|
|
231
|
+
vpc: {
|
|
232
|
+
enable: true,
|
|
233
|
+
management: 'use-existing',
|
|
234
|
+
vpcId: 'vpc-custom',
|
|
235
|
+
subnets: {
|
|
236
|
+
management: 'use-existing',
|
|
237
|
+
ids: ['subnet-explicit1', 'subnet-explicit2']
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
integrations: []
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
244
|
+
|
|
245
|
+
expect(result.resources.Resources.FriggPrivateSubnet1).toBeUndefined();
|
|
246
|
+
expect(result.resources.Resources.FriggPrivateSubnet2).toBeUndefined();
|
|
247
|
+
expect(result.provider.vpc.subnetIds).toEqual(['subnet-explicit1', 'subnet-explicit2']);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should use Fn::Cidr for subnet CIDR blocks in new VPC to avoid conflicts', async () => {
|
|
251
|
+
const appDefinition = {
|
|
252
|
+
vpc: {
|
|
253
|
+
enable: true,
|
|
254
|
+
management: 'create-new',
|
|
255
|
+
subnets: { management: 'create' }
|
|
256
|
+
},
|
|
257
|
+
integrations: []
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
261
|
+
|
|
262
|
+
// Verify new VPC uses 10.0.0.0/16
|
|
263
|
+
expect(result.resources.Resources.FriggVPC.Properties.CidrBlock).toBe('10.0.0.0/16');
|
|
264
|
+
|
|
265
|
+
// Verify subnets use Fn::Cidr to generate non-conflicting CIDRs
|
|
266
|
+
const subnet1Cidr = result.resources.Resources.FriggPrivateSubnet1.Properties.CidrBlock;
|
|
267
|
+
const subnet2Cidr = result.resources.Resources.FriggPrivateSubnet2.Properties.CidrBlock;
|
|
268
|
+
const publicSubnetCidr = result.resources.Resources.FriggPublicSubnet.Properties.CidrBlock;
|
|
269
|
+
|
|
270
|
+
// Check that CIDRs are generated using Fn::Cidr and Fn::Select
|
|
271
|
+
expect(subnet1Cidr).toHaveProperty('Fn::Select');
|
|
272
|
+
expect(subnet1Cidr['Fn::Select'][0]).toBe(0);
|
|
273
|
+
expect(subnet2Cidr).toHaveProperty('Fn::Select');
|
|
274
|
+
expect(subnet2Cidr['Fn::Select'][0]).toBe(1);
|
|
275
|
+
expect(publicSubnetCidr).toHaveProperty('Fn::Select');
|
|
276
|
+
expect(publicSubnetCidr['Fn::Select'][0]).toBe(2);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should create route tables for subnets even without NAT Gateway management', async () => {
|
|
280
|
+
const appDefinition = {
|
|
281
|
+
vpc: {
|
|
282
|
+
enable: true,
|
|
283
|
+
management: 'create-new',
|
|
284
|
+
subnets: { management: 'create' },
|
|
285
|
+
natGateway: { management: 'discover' }
|
|
286
|
+
},
|
|
287
|
+
integrations: []
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
291
|
+
|
|
292
|
+
// Verify route tables are created
|
|
293
|
+
expect(result.resources.Resources.FriggPublicRouteTable).toBeDefined();
|
|
294
|
+
expect(result.resources.Resources.FriggPublicRoute).toBeDefined();
|
|
295
|
+
expect(result.resources.Resources.FriggLambdaRouteTable).toBeDefined();
|
|
296
|
+
|
|
297
|
+
// Verify subnet associations
|
|
298
|
+
expect(result.resources.Resources.FriggPublicSubnetRouteTableAssociation).toBeDefined();
|
|
299
|
+
expect(result.resources.Resources.FriggPrivateSubnet1RouteTableAssociation).toBeDefined();
|
|
300
|
+
expect(result.resources.Resources.FriggPrivateSubnet2RouteTableAssociation).toBeDefined();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should throw error when use-existing mode without vpcId', async () => {
|
|
304
|
+
const appDefinition = {
|
|
305
|
+
vpc: {
|
|
306
|
+
enable: true,
|
|
307
|
+
management: 'use-existing'
|
|
308
|
+
},
|
|
309
|
+
integrations: []
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
await expect(composeServerlessDefinition(appDefinition)).rejects.toThrow(
|
|
313
|
+
'VPC management is set to "use-existing" but no vpcId was provided'
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should default to discover mode when management not specified', async () => {
|
|
114
318
|
const appDefinition = {
|
|
115
319
|
vpc: { enable: true },
|
|
116
320
|
integrations: []
|
|
@@ -125,7 +329,10 @@ describe('composeServerlessDefinition', () => {
|
|
|
125
329
|
|
|
126
330
|
it('should add VPC endpoint for S3 when VPC is enabled', async () => {
|
|
127
331
|
const appDefinition = {
|
|
128
|
-
vpc: {
|
|
332
|
+
vpc: {
|
|
333
|
+
enable: true,
|
|
334
|
+
management: 'discover'
|
|
335
|
+
},
|
|
129
336
|
integrations: []
|
|
130
337
|
};
|
|
131
338
|
|
|
@@ -533,11 +740,142 @@ describe('composeServerlessDefinition', () => {
|
|
|
533
740
|
});
|
|
534
741
|
});
|
|
535
742
|
|
|
743
|
+
describe('NAT Gateway Management', () => {
|
|
744
|
+
it('should handle NAT Gateway with createAndManage mode', async () => {
|
|
745
|
+
const appDefinition = {
|
|
746
|
+
vpc: {
|
|
747
|
+
enable: true,
|
|
748
|
+
management: 'discover',
|
|
749
|
+
natGateway: {
|
|
750
|
+
management: 'createAndManage'
|
|
751
|
+
}
|
|
752
|
+
},
|
|
753
|
+
integrations: []
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
757
|
+
|
|
758
|
+
expect(result.resources.Resources.FriggLambdaRouteTable).toBeDefined();
|
|
759
|
+
expect(result.resources.Resources.FriggNATRoute).toBeDefined();
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('should handle NAT Gateway with discover mode', async () => {
|
|
763
|
+
// Mock discovery to return existing NAT Gateway
|
|
764
|
+
const { AWSDiscovery } = require('./aws-discovery');
|
|
765
|
+
const mockDiscoverResources = jest.fn().mockResolvedValue({
|
|
766
|
+
defaultVpcId: 'vpc-123456',
|
|
767
|
+
defaultSecurityGroupId: 'sg-123456',
|
|
768
|
+
privateSubnetId1: 'subnet-123456',
|
|
769
|
+
privateSubnetId2: 'subnet-789012',
|
|
770
|
+
publicSubnetId: 'subnet-public',
|
|
771
|
+
defaultRouteTableId: 'rtb-123456',
|
|
772
|
+
defaultKmsKeyId: null,
|
|
773
|
+
existingNatGatewayId: 'nat-existing123'
|
|
774
|
+
});
|
|
775
|
+
AWSDiscovery.mockImplementation(() => ({
|
|
776
|
+
discoverResources: mockDiscoverResources
|
|
777
|
+
}));
|
|
778
|
+
|
|
779
|
+
const appDefinition = {
|
|
780
|
+
vpc: {
|
|
781
|
+
enable: true,
|
|
782
|
+
management: 'discover',
|
|
783
|
+
natGateway: {
|
|
784
|
+
management: 'discover'
|
|
785
|
+
}
|
|
786
|
+
},
|
|
787
|
+
integrations: []
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
791
|
+
|
|
792
|
+
expect(result.resources.Resources.FriggNATRoute).toBeDefined();
|
|
793
|
+
expect(result.resources.Resources.FriggNATRoute.Properties.NatGatewayId).toBe('nat-existing123');
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
it('should handle NAT Gateway with useExisting mode and provided ID', async () => {
|
|
797
|
+
const appDefinition = {
|
|
798
|
+
vpc: {
|
|
799
|
+
enable: true,
|
|
800
|
+
management: 'discover',
|
|
801
|
+
natGateway: {
|
|
802
|
+
management: 'useExisting',
|
|
803
|
+
id: 'nat-custom456'
|
|
804
|
+
}
|
|
805
|
+
},
|
|
806
|
+
integrations: []
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
810
|
+
|
|
811
|
+
expect(result.resources.Resources.FriggNATRoute).toBeDefined();
|
|
812
|
+
expect(result.resources.Resources.FriggNATRoute.Properties.NatGatewayId).toBe('nat-custom456');
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
it('should throw error when NAT Gateway not found in discover mode', async () => {
|
|
816
|
+
// Mock discovery to return no NAT Gateway
|
|
817
|
+
const { AWSDiscovery } = require('./aws-discovery');
|
|
818
|
+
const mockDiscoverResources = jest.fn().mockResolvedValue({
|
|
819
|
+
defaultVpcId: 'vpc-123456',
|
|
820
|
+
defaultSecurityGroupId: 'sg-123456',
|
|
821
|
+
privateSubnetId1: 'subnet-123456',
|
|
822
|
+
privateSubnetId2: 'subnet-789012',
|
|
823
|
+
publicSubnetId: 'subnet-public',
|
|
824
|
+
defaultRouteTableId: 'rtb-123456',
|
|
825
|
+
defaultKmsKeyId: null,
|
|
826
|
+
existingNatGatewayId: null // No NAT Gateway
|
|
827
|
+
});
|
|
828
|
+
AWSDiscovery.mockImplementation(() => ({
|
|
829
|
+
discoverResources: mockDiscoverResources
|
|
830
|
+
}));
|
|
831
|
+
|
|
832
|
+
const appDefinition = {
|
|
833
|
+
vpc: {
|
|
834
|
+
enable: true,
|
|
835
|
+
management: 'discover',
|
|
836
|
+
natGateway: {
|
|
837
|
+
management: 'discover'
|
|
838
|
+
}
|
|
839
|
+
},
|
|
840
|
+
integrations: []
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
await expect(composeServerlessDefinition(appDefinition)).rejects.toThrow(
|
|
844
|
+
'No existing NAT Gateway found in discovery mode'
|
|
845
|
+
);
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
it('should enable self-healing when selfHeal is true', async () => {
|
|
849
|
+
const appDefinition = {
|
|
850
|
+
vpc: {
|
|
851
|
+
enable: true,
|
|
852
|
+
management: 'discover',
|
|
853
|
+
natGateway: {
|
|
854
|
+
management: 'discover'
|
|
855
|
+
},
|
|
856
|
+
selfHeal: true
|
|
857
|
+
},
|
|
858
|
+
integrations: []
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
862
|
+
|
|
863
|
+
// With self-healing enabled, it should handle misconfigured NAT Gateways
|
|
864
|
+
expect(result.resources.Resources.FriggLambdaRouteTable).toBeDefined();
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
|
|
536
868
|
describe('Combined Configurations', () => {
|
|
537
869
|
it('should combine VPC, KMS, and SSM configurations', async () => {
|
|
538
870
|
const appDefinition = {
|
|
539
|
-
vpc: {
|
|
540
|
-
|
|
871
|
+
vpc: {
|
|
872
|
+
enable: true,
|
|
873
|
+
natGateway: { management: 'createAndManage' } // Explicitly set NAT management mode
|
|
874
|
+
},
|
|
875
|
+
encryption: {
|
|
876
|
+
fieldLevelEncryptionMethod: 'kms',
|
|
877
|
+
createResourceIfNoneFound: true // Allow creating KMS key if not found
|
|
878
|
+
},
|
|
541
879
|
ssm: { enable: true },
|
|
542
880
|
integrations: [mockIntegration]
|
|
543
881
|
};
|
|
@@ -575,8 +913,14 @@ describe('composeServerlessDefinition', () => {
|
|
|
575
913
|
|
|
576
914
|
it('should handle partial configuration combinations', async () => {
|
|
577
915
|
const appDefinition = {
|
|
578
|
-
vpc: {
|
|
579
|
-
|
|
916
|
+
vpc: {
|
|
917
|
+
enable: true,
|
|
918
|
+
natGateway: { management: 'createAndManage' } // Explicitly set NAT management mode
|
|
919
|
+
},
|
|
920
|
+
encryption: {
|
|
921
|
+
fieldLevelEncryptionMethod: 'kms',
|
|
922
|
+
createResourceIfNoneFound: true // Allow creating KMS key if not found
|
|
923
|
+
},
|
|
580
924
|
integrations: []
|
|
581
925
|
};
|
|
582
926
|
|
|
@@ -702,6 +1046,218 @@ describe('composeServerlessDefinition', () => {
|
|
|
702
1046
|
});
|
|
703
1047
|
});
|
|
704
1048
|
|
|
1049
|
+
describe('CRITICAL: NAT Gateway MUST be in PUBLIC Subnet', () => {
|
|
1050
|
+
it('should NEVER reuse NAT Gateway in private subnet - throw error when selfHeal disabled', async () => {
|
|
1051
|
+
// Mock NAT Gateway found in PRIVATE subnet (the original bug)
|
|
1052
|
+
const mockDiscovery = require('./aws-discovery').AWSDiscovery;
|
|
1053
|
+
const mockInstance = new mockDiscovery();
|
|
1054
|
+
mockInstance.discoverResources = jest.fn().mockResolvedValue({
|
|
1055
|
+
defaultVpcId: 'vpc-123456',
|
|
1056
|
+
defaultSecurityGroupId: 'sg-123456',
|
|
1057
|
+
privateSubnetId1: 'subnet-private1',
|
|
1058
|
+
privateSubnetId2: 'subnet-private2',
|
|
1059
|
+
publicSubnetId: 'subnet-public',
|
|
1060
|
+
existingNatGatewayId: 'nat-in-private',
|
|
1061
|
+
natGatewayInPrivateSubnet: true, // CRITICAL: NAT is in WRONG subnet
|
|
1062
|
+
existingElasticIpAllocationId: 'eipalloc-123'
|
|
1063
|
+
});
|
|
1064
|
+
mockDiscovery.mockImplementation(() => mockInstance);
|
|
1065
|
+
|
|
1066
|
+
const appDefinition = {
|
|
1067
|
+
vpc: {
|
|
1068
|
+
enable: true,
|
|
1069
|
+
natGateway: { management: 'createAndManage' },
|
|
1070
|
+
selfHeal: false
|
|
1071
|
+
},
|
|
1072
|
+
integrations: []
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
// Should throw error because NAT is in private subnet
|
|
1076
|
+
await expect(composeServerlessDefinition(appDefinition))
|
|
1077
|
+
.rejects
|
|
1078
|
+
.toThrow('CRITICAL: NAT Gateway is in PRIVATE subnet');
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
it('should create NEW NAT in PUBLIC subnet when existing NAT is in private subnet with selfHeal', async () => {
|
|
1082
|
+
const mockDiscovery = require('./aws-discovery').AWSDiscovery;
|
|
1083
|
+
const mockInstance = new mockDiscovery();
|
|
1084
|
+
mockInstance.discoverResources = jest.fn().mockResolvedValue({
|
|
1085
|
+
defaultVpcId: 'vpc-123456',
|
|
1086
|
+
defaultSecurityGroupId: 'sg-123456',
|
|
1087
|
+
privateSubnetId1: 'subnet-private1',
|
|
1088
|
+
privateSubnetId2: 'subnet-private2',
|
|
1089
|
+
publicSubnetId: 'subnet-public',
|
|
1090
|
+
existingNatGatewayId: 'nat-in-private',
|
|
1091
|
+
natGatewayInPrivateSubnet: true, // NAT is in WRONG subnet
|
|
1092
|
+
existingElasticIpAllocationId: 'eipalloc-123'
|
|
1093
|
+
});
|
|
1094
|
+
mockDiscovery.mockImplementation(() => mockInstance);
|
|
1095
|
+
|
|
1096
|
+
const appDefinition = {
|
|
1097
|
+
vpc: {
|
|
1098
|
+
enable: true,
|
|
1099
|
+
natGateway: { management: 'createAndManage' },
|
|
1100
|
+
selfHeal: true
|
|
1101
|
+
},
|
|
1102
|
+
integrations: []
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
1106
|
+
|
|
1107
|
+
// MUST create new NAT Gateway (not reuse the one in private subnet)
|
|
1108
|
+
expect(result.resources.Resources.FriggNATGateway).toBeDefined();
|
|
1109
|
+
|
|
1110
|
+
// MUST be placed in PUBLIC subnet
|
|
1111
|
+
const natSubnet = result.resources.Resources.FriggNATGateway.Properties.SubnetId;
|
|
1112
|
+
expect(natSubnet).toEqual('subnet-public');
|
|
1113
|
+
|
|
1114
|
+
// MUST create new EIP (cannot reuse the one associated with wrong NAT)
|
|
1115
|
+
expect(result.resources.Resources.FriggNATGatewayEIP).toBeDefined();
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
it('should create public subnet for NAT when none exists', async () => {
|
|
1119
|
+
const mockDiscovery = require('./aws-discovery').AWSDiscovery;
|
|
1120
|
+
const mockInstance = new mockDiscovery();
|
|
1121
|
+
mockInstance.discoverResources = jest.fn().mockResolvedValue({
|
|
1122
|
+
defaultVpcId: 'vpc-123456',
|
|
1123
|
+
defaultSecurityGroupId: 'sg-123456',
|
|
1124
|
+
privateSubnetId1: 'subnet-private1',
|
|
1125
|
+
privateSubnetId2: 'subnet-private2',
|
|
1126
|
+
publicSubnetId: null, // NO PUBLIC SUBNET EXISTS
|
|
1127
|
+
existingNatGatewayId: null
|
|
1128
|
+
});
|
|
1129
|
+
mockDiscovery.mockImplementation(() => mockInstance);
|
|
1130
|
+
|
|
1131
|
+
const appDefinition = {
|
|
1132
|
+
vpc: {
|
|
1133
|
+
enable: true,
|
|
1134
|
+
natGateway: { management: 'createAndManage' },
|
|
1135
|
+
selfHeal: true
|
|
1136
|
+
},
|
|
1137
|
+
integrations: []
|
|
1138
|
+
};
|
|
1139
|
+
|
|
1140
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
1141
|
+
|
|
1142
|
+
// MUST create public subnet
|
|
1143
|
+
expect(result.resources.Resources.FriggPublicSubnet).toBeDefined();
|
|
1144
|
+
expect(result.resources.Resources.FriggPublicSubnet.Properties.MapPublicIpOnLaunch).toBe(true);
|
|
1145
|
+
|
|
1146
|
+
// NAT Gateway MUST be in the newly created public subnet
|
|
1147
|
+
expect(result.resources.Resources.FriggNATGateway.Properties.SubnetId)
|
|
1148
|
+
.toEqual({ Ref: 'FriggPublicSubnet' });
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
it('should reuse CORRECTLY placed NAT Gateway in public subnet', async () => {
|
|
1152
|
+
const mockDiscovery = require('./aws-discovery').AWSDiscovery;
|
|
1153
|
+
const mockInstance = new mockDiscovery();
|
|
1154
|
+
mockInstance.discoverResources = jest.fn().mockResolvedValue({
|
|
1155
|
+
defaultVpcId: 'vpc-123456',
|
|
1156
|
+
defaultSecurityGroupId: 'sg-123456',
|
|
1157
|
+
privateSubnetId1: 'subnet-private1',
|
|
1158
|
+
privateSubnetId2: 'subnet-private2',
|
|
1159
|
+
publicSubnetId: 'subnet-public',
|
|
1160
|
+
existingNatGatewayId: 'nat-good',
|
|
1161
|
+
natGatewayInPrivateSubnet: false, // NAT is CORRECTLY in public subnet
|
|
1162
|
+
existingElasticIpAllocationId: 'eipalloc-123'
|
|
1163
|
+
});
|
|
1164
|
+
mockDiscovery.mockImplementation(() => mockInstance);
|
|
1165
|
+
|
|
1166
|
+
const appDefinition = {
|
|
1167
|
+
vpc: {
|
|
1168
|
+
enable: true,
|
|
1169
|
+
natGateway: { management: 'createAndManage' },
|
|
1170
|
+
selfHeal: true
|
|
1171
|
+
},
|
|
1172
|
+
integrations: []
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
1176
|
+
|
|
1177
|
+
// Should NOT create new NAT Gateway (reuse the good one)
|
|
1178
|
+
expect(result.resources.Resources.FriggNATGateway).toBeUndefined();
|
|
1179
|
+
expect(result.resources.Resources.FriggNATGatewayEIP).toBeUndefined();
|
|
1180
|
+
|
|
1181
|
+
// Should use existing NAT in routes
|
|
1182
|
+
expect(result.resources.Resources.FriggNATRoute.Properties.NatGatewayId)
|
|
1183
|
+
.toEqual('nat-good');
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
it('should fix route table associations to prevent NAT misconfiguration', async () => {
|
|
1187
|
+
const mockDiscovery = require('./aws-discovery').AWSDiscovery;
|
|
1188
|
+
const mockInstance = new mockDiscovery();
|
|
1189
|
+
mockInstance.discoverResources = jest.fn().mockResolvedValue({
|
|
1190
|
+
defaultVpcId: 'vpc-123456',
|
|
1191
|
+
defaultSecurityGroupId: 'sg-123456',
|
|
1192
|
+
privateSubnetId1: 'subnet-private1',
|
|
1193
|
+
privateSubnetId2: 'subnet-private2',
|
|
1194
|
+
publicSubnetId: 'subnet-public',
|
|
1195
|
+
existingNatGatewayId: 'nat-good',
|
|
1196
|
+
natGatewayInPrivateSubnet: false
|
|
1197
|
+
});
|
|
1198
|
+
mockDiscovery.mockImplementation(() => mockInstance);
|
|
1199
|
+
|
|
1200
|
+
const appDefinition = {
|
|
1201
|
+
vpc: {
|
|
1202
|
+
enable: true,
|
|
1203
|
+
natGateway: { management: 'discover' },
|
|
1204
|
+
selfHeal: true
|
|
1205
|
+
},
|
|
1206
|
+
integrations: []
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
1210
|
+
|
|
1211
|
+
// Should create route table to fix associations
|
|
1212
|
+
expect(result.resources.Resources.FriggLambdaRouteTable).toBeDefined();
|
|
1213
|
+
|
|
1214
|
+
// Should create NAT route pointing to good NAT
|
|
1215
|
+
expect(result.resources.Resources.FriggNATRoute).toBeDefined();
|
|
1216
|
+
expect(result.resources.Resources.FriggNATRoute.Properties.NatGatewayId)
|
|
1217
|
+
.toEqual('nat-good');
|
|
1218
|
+
|
|
1219
|
+
// Should associate private subnets with correct route table
|
|
1220
|
+
expect(result.resources.Resources.FriggSubnet1RouteAssociation).toBeDefined();
|
|
1221
|
+
expect(result.resources.Resources.FriggSubnet2RouteAssociation).toBeDefined();
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
it('should handle EIP already associated error by reusing existing NAT', async () => {
|
|
1225
|
+
const mockDiscovery = require('./aws-discovery').AWSDiscovery;
|
|
1226
|
+
const mockInstance = new mockDiscovery();
|
|
1227
|
+
mockInstance.discoverResources = jest.fn().mockResolvedValue({
|
|
1228
|
+
defaultVpcId: 'vpc-123456',
|
|
1229
|
+
defaultSecurityGroupId: 'sg-123456',
|
|
1230
|
+
privateSubnetId1: 'subnet-private1',
|
|
1231
|
+
privateSubnetId2: 'subnet-private2',
|
|
1232
|
+
publicSubnetId: 'subnet-public',
|
|
1233
|
+
existingNatGatewayId: 'nat-existing',
|
|
1234
|
+
natGatewayInPrivateSubnet: false, // NAT is correctly placed
|
|
1235
|
+
existingElasticIpAllocationId: 'eipalloc-inuse',
|
|
1236
|
+
elasticIpAlreadyAssociated: true // EIP is already in use
|
|
1237
|
+
});
|
|
1238
|
+
mockDiscovery.mockImplementation(() => mockInstance);
|
|
1239
|
+
|
|
1240
|
+
const appDefinition = {
|
|
1241
|
+
vpc: {
|
|
1242
|
+
enable: true,
|
|
1243
|
+
natGateway: { management: 'createAndManage' },
|
|
1244
|
+
selfHeal: true
|
|
1245
|
+
},
|
|
1246
|
+
integrations: []
|
|
1247
|
+
};
|
|
1248
|
+
|
|
1249
|
+
const result = await composeServerlessDefinition(appDefinition);
|
|
1250
|
+
|
|
1251
|
+
// Should NOT create new NAT or EIP (reuse existing)
|
|
1252
|
+
expect(result.resources.Resources.FriggNATGateway).toBeUndefined();
|
|
1253
|
+
expect(result.resources.Resources.FriggNATGatewayEIP).toBeUndefined();
|
|
1254
|
+
|
|
1255
|
+
// Should use existing NAT
|
|
1256
|
+
expect(result.resources.Resources.FriggNATRoute.Properties.NatGatewayId)
|
|
1257
|
+
.toEqual('nat-existing');
|
|
1258
|
+
});
|
|
1259
|
+
});
|
|
1260
|
+
|
|
705
1261
|
describe('Edge Cases', () => {
|
|
706
1262
|
it('should handle empty app definition', async () => {
|
|
707
1263
|
const appDefinition = {};
|
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.428.
|
|
4
|
+
"version": "2.0.0--canary.428.08c3f6f.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-ec2": "^3.835.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.835.0",
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"@babel/eslint-parser": "^7.18.9",
|
|
10
10
|
"@babel/parser": "^7.25.3",
|
|
11
11
|
"@babel/traverse": "^7.25.3",
|
|
12
|
-
"@friggframework/schemas": "2.0.0--canary.428.
|
|
13
|
-
"@friggframework/test": "2.0.0--canary.428.
|
|
12
|
+
"@friggframework/schemas": "2.0.0--canary.428.08c3f6f.0",
|
|
13
|
+
"@friggframework/test": "2.0.0--canary.428.08c3f6f.0",
|
|
14
14
|
"@hapi/boom": "^10.0.1",
|
|
15
15
|
"@inquirer/prompts": "^5.3.8",
|
|
16
16
|
"axios": "^1.7.2",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"serverless-http": "^2.7.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@friggframework/eslint-config": "2.0.0--canary.428.
|
|
36
|
-
"@friggframework/prettier-config": "2.0.0--canary.428.
|
|
35
|
+
"@friggframework/eslint-config": "2.0.0--canary.428.08c3f6f.0",
|
|
36
|
+
"@friggframework/prettier-config": "2.0.0--canary.428.08c3f6f.0",
|
|
37
37
|
"jest": "^30.1.3",
|
|
38
38
|
"prettier": "^2.7.1",
|
|
39
39
|
"serverless": "3.39.0",
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"publishConfig": {
|
|
67
67
|
"access": "public"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "08c3f6fe0ca98334f07909301c2ec1812746f657"
|
|
70
70
|
}
|