@friggframework/devtools 2.0.0--canary.493.f8d621f.0 → 2.0.0--canary.490.a6abe40.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.
- package/infrastructure/domains/networking/vpc-builder.js +159 -4
- package/infrastructure/domains/networking/vpc-builder.test.js +242 -13
- package/infrastructure/domains/networking/vpc-resolver.js +57 -10
- package/infrastructure/domains/networking/vpc-resolver.test.js +40 -0
- package/infrastructure/domains/shared/cloudformation-discovery.js +157 -5
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +218 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.js +23 -0
- package/infrastructure/domains/shared/resource-discovery.js +17 -5
- package/infrastructure/domains/shared/resource-discovery.test.js +36 -0
- package/infrastructure/scripts/build-prisma-layer.js +8 -81
- package/infrastructure/scripts/build-prisma-layer.test.js +1 -53
- package/package.json +7 -7
- package/infrastructure/scripts/verify-prisma-layer.js +0 -72
|
@@ -149,7 +149,7 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
149
149
|
physicalId = flatDiscovery.defaultVpcId;
|
|
150
150
|
} else if (logicalId === 'FriggLambdaSecurityGroup') {
|
|
151
151
|
resourceType = 'AWS::EC2::SecurityGroup';
|
|
152
|
-
physicalId = flatDiscovery.defaultSecurityGroupId || flatDiscovery.securityGroupId;
|
|
152
|
+
physicalId = flatDiscovery.lambdaSecurityGroupId || flatDiscovery.defaultSecurityGroupId || flatDiscovery.securityGroupId;
|
|
153
153
|
} else if (logicalId === 'FriggPrivateSubnet1') {
|
|
154
154
|
resourceType = 'AWS::EC2::Subnet';
|
|
155
155
|
physicalId = flatDiscovery.privateSubnetId1;
|
|
@@ -159,6 +159,9 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
159
159
|
} else if (logicalId === 'FriggNATGateway') {
|
|
160
160
|
resourceType = 'AWS::EC2::NatGateway';
|
|
161
161
|
physicalId = flatDiscovery.existingNatGatewayId;
|
|
162
|
+
} else if (logicalId === 'FriggLambdaRouteTable') {
|
|
163
|
+
resourceType = 'AWS::EC2::RouteTable';
|
|
164
|
+
physicalId = flatDiscovery.routeTableId;
|
|
162
165
|
} else if (logicalId === 'FriggS3VPCEndpoint') {
|
|
163
166
|
resourceType = 'AWS::EC2::VPCEndpoint';
|
|
164
167
|
physicalId = flatDiscovery.s3VpcEndpointId;
|
|
@@ -184,6 +187,11 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
184
187
|
});
|
|
185
188
|
}
|
|
186
189
|
});
|
|
190
|
+
|
|
191
|
+
// Also check for external resources extracted via CloudFormation queries
|
|
192
|
+
// (e.g., VPC ID from security group query, subnets from route table associations)
|
|
193
|
+
// These are NOT in the stack but were discovered through stack resources
|
|
194
|
+
this._addExternalResourcesFromCloudFormationQueries(flatDiscovery, discovery, existingLogicalIds);
|
|
187
195
|
} else {
|
|
188
196
|
// Resources discovered from AWS API (not CloudFormation)
|
|
189
197
|
// These go into external array
|
|
@@ -290,6 +298,58 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
290
298
|
return discovery;
|
|
291
299
|
}
|
|
292
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Add external resources that were discovered via CloudFormation queries
|
|
303
|
+
* (e.g., VPC ID extracted from security group, subnets from route table associations)
|
|
304
|
+
*
|
|
305
|
+
* @private
|
|
306
|
+
*/
|
|
307
|
+
_addExternalResourcesFromCloudFormationQueries(flatDiscovery, discovery, existingLogicalIds) {
|
|
308
|
+
// VPC ID extracted from SG or route table (NOT a stack resource)
|
|
309
|
+
if (flatDiscovery.defaultVpcId &&
|
|
310
|
+
typeof flatDiscovery.defaultVpcId === 'string' &&
|
|
311
|
+
!existingLogicalIds.includes('FriggVPC')) {
|
|
312
|
+
discovery.external.push({
|
|
313
|
+
physicalId: flatDiscovery.defaultVpcId,
|
|
314
|
+
resourceType: 'AWS::EC2::VPC',
|
|
315
|
+
source: 'cloudformation-query'
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Subnets extracted from route table associations (NOT stack resources)
|
|
320
|
+
if (flatDiscovery.privateSubnetId1 &&
|
|
321
|
+
typeof flatDiscovery.privateSubnetId1 === 'string' &&
|
|
322
|
+
!existingLogicalIds.includes('FriggPrivateSubnet1')) {
|
|
323
|
+
discovery.external.push({
|
|
324
|
+
physicalId: flatDiscovery.privateSubnetId1,
|
|
325
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
326
|
+
source: 'cloudformation-query'
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (flatDiscovery.privateSubnetId2 &&
|
|
331
|
+
typeof flatDiscovery.privateSubnetId2 === 'string' &&
|
|
332
|
+
!existingLogicalIds.includes('FriggPrivateSubnet2')) {
|
|
333
|
+
discovery.external.push({
|
|
334
|
+
physicalId: flatDiscovery.privateSubnetId2,
|
|
335
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
336
|
+
source: 'cloudformation-query'
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// NAT Gateway extracted from route table routes
|
|
341
|
+
if (flatDiscovery.existingNatGatewayId &&
|
|
342
|
+
typeof flatDiscovery.existingNatGatewayId === 'string' &&
|
|
343
|
+
!existingLogicalIds.includes('FriggNATGateway') &&
|
|
344
|
+
!existingLogicalIds.includes('FriggNatGateway')) {
|
|
345
|
+
discovery.external.push({
|
|
346
|
+
physicalId: flatDiscovery.existingNatGatewayId,
|
|
347
|
+
resourceType: 'AWS::EC2::NatGateway',
|
|
348
|
+
source: 'cloudformation-query'
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
293
353
|
/**
|
|
294
354
|
* Translate legacy configuration (management modes) to new ownership-based configuration
|
|
295
355
|
* Provides backwards compatibility for existing app definitions
|
|
@@ -580,7 +640,6 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
580
640
|
buildSecurityGroupFromDecision(decision, appDefinition, result) {
|
|
581
641
|
if (decision.ownership === ResourceOwnership.STACK) {
|
|
582
642
|
// Always create security group resource in template
|
|
583
|
-
// CloudFormation handles idempotency if it already exists
|
|
584
643
|
console.log(' → Adding Lambda Security Group to template...');
|
|
585
644
|
|
|
586
645
|
result.resources.FriggLambdaSecurityGroup = {
|
|
@@ -630,7 +689,6 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
630
689
|
}
|
|
631
690
|
|
|
632
691
|
// For STACK ownership: ALWAYS add definitions to template
|
|
633
|
-
// CloudFormation idempotency ensures existing resources won't be recreated
|
|
634
692
|
if (decision.physicalIds && decision.physicalIds.length >= 2) {
|
|
635
693
|
console.log(` → Adding subnet definitions to template (existing: ${decision.physicalIds.join(', ')})`);
|
|
636
694
|
} else {
|
|
@@ -872,6 +930,8 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
872
930
|
|
|
873
931
|
if (endpointsInStack.length > 0) {
|
|
874
932
|
console.log(` ✓ VPC Endpoints in stack: ${endpointsInStack.join(', ')}`);
|
|
933
|
+
// CRITICAL: Must add stack-managed endpoints back to template or CloudFormation will DELETE them!
|
|
934
|
+
this._addStackManagedEndpointsToTemplate(decisions, result);
|
|
875
935
|
}
|
|
876
936
|
|
|
877
937
|
if (externalEndpoints.length > 0) {
|
|
@@ -1052,6 +1112,101 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
1052
1112
|
return healingReport;
|
|
1053
1113
|
}
|
|
1054
1114
|
|
|
1115
|
+
/**
|
|
1116
|
+
* Add stack-managed VPC endpoints back to template
|
|
1117
|
+
*
|
|
1118
|
+
* CRITICAL: CloudFormation will DELETE resources that exist in the previous template
|
|
1119
|
+
* but are missing from the new template. We must re-add discovered stack-managed
|
|
1120
|
+
* endpoints to prevent CloudFormation from deleting them.
|
|
1121
|
+
*
|
|
1122
|
+
* @private
|
|
1123
|
+
*/
|
|
1124
|
+
_addStackManagedEndpointsToTemplate(decisions, result) {
|
|
1125
|
+
const vpcId = result.vpcId;
|
|
1126
|
+
const logicalIdMap = {
|
|
1127
|
+
s3: 'FriggS3VPCEndpoint',
|
|
1128
|
+
dynamodb: 'FriggDynamoDBVPCEndpoint',
|
|
1129
|
+
kms: 'FriggKMSVPCEndpoint',
|
|
1130
|
+
secretsManager: 'FriggSecretsManagerVPCEndpoint',
|
|
1131
|
+
sqs: 'FriggSQSVPCEndpoint'
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
Object.entries(decisions).forEach(([type, decision]) => {
|
|
1135
|
+
if (decision.ownership === ResourceOwnership.STACK && decision.physicalId) {
|
|
1136
|
+
const logicalId = logicalIdMap[type];
|
|
1137
|
+
|
|
1138
|
+
// Determine endpoint type and properties based on service
|
|
1139
|
+
if (type === 's3') {
|
|
1140
|
+
result.resources[logicalId] = {
|
|
1141
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
1142
|
+
Properties: {
|
|
1143
|
+
VpcId: vpcId,
|
|
1144
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.s3',
|
|
1145
|
+
VpcEndpointType: 'Gateway',
|
|
1146
|
+
RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }]
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
} else if (type === 'dynamodb') {
|
|
1150
|
+
result.resources[logicalId] = {
|
|
1151
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
1152
|
+
Properties: {
|
|
1153
|
+
VpcId: vpcId,
|
|
1154
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.dynamodb',
|
|
1155
|
+
VpcEndpointType: 'Gateway',
|
|
1156
|
+
RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }]
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
} else {
|
|
1160
|
+
// Interface endpoints (KMS, Secrets Manager, SQS)
|
|
1161
|
+
const serviceMap = {
|
|
1162
|
+
kms: 'kms',
|
|
1163
|
+
secretsManager: 'secretsmanager',
|
|
1164
|
+
sqs: 'sqs'
|
|
1165
|
+
};
|
|
1166
|
+
|
|
1167
|
+
result.resources[logicalId] = {
|
|
1168
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
1169
|
+
Properties: {
|
|
1170
|
+
VpcId: vpcId,
|
|
1171
|
+
ServiceName: `com.amazonaws.\${self:provider.region}.${serviceMap[type]}`,
|
|
1172
|
+
VpcEndpointType: 'Interface',
|
|
1173
|
+
SubnetIds: result.vpcConfig.subnetIds,
|
|
1174
|
+
SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
|
|
1175
|
+
PrivateDnsEnabled: true
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
// If any interface endpoints exist, ensure security group is in template
|
|
1183
|
+
const hasInterfaceEndpoints = ['kms', 'secretsManager', 'sqs'].some(
|
|
1184
|
+
type => decisions[type]?.ownership === ResourceOwnership.STACK && decisions[type]?.physicalId
|
|
1185
|
+
);
|
|
1186
|
+
|
|
1187
|
+
if (hasInterfaceEndpoints && !result.resources.FriggVPCEndpointSecurityGroup) {
|
|
1188
|
+
result.resources.FriggVPCEndpointSecurityGroup = {
|
|
1189
|
+
Type: 'AWS::EC2::SecurityGroup',
|
|
1190
|
+
Properties: {
|
|
1191
|
+
GroupDescription: 'Security group for VPC Endpoints',
|
|
1192
|
+
VpcId: vpcId,
|
|
1193
|
+
SecurityGroupIngress: [
|
|
1194
|
+
{
|
|
1195
|
+
IpProtocol: 'tcp',
|
|
1196
|
+
FromPort: 443,
|
|
1197
|
+
ToPort: 443,
|
|
1198
|
+
SourceSecurityGroupId: { Ref: 'FriggLambdaSecurityGroup' }
|
|
1199
|
+
}
|
|
1200
|
+
],
|
|
1201
|
+
Tags: [
|
|
1202
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc-endpoint-sg' },
|
|
1203
|
+
{ Key: 'ManagedBy', Value: 'Frigg' }
|
|
1204
|
+
]
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1055
1210
|
/**
|
|
1056
1211
|
* Build new VPC from scratch
|
|
1057
1212
|
*/
|
|
@@ -1661,7 +1816,7 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
1661
1816
|
// Stack-managed resources should be reused, not recreated
|
|
1662
1817
|
const stackManagedEndpoints = {
|
|
1663
1818
|
s3: discoveredResources.s3VpcEndpointId && typeof discoveredResources.s3VpcEndpointId === 'string',
|
|
1664
|
-
dynamodb: discoveredResources.
|
|
1819
|
+
dynamodb: discoveredResources.dynamodbVpcEndpointId && typeof discoveredResources.dynamodbVpcEndpointId === 'string',
|
|
1665
1820
|
kms: discoveredResources.kmsVpcEndpointId && typeof discoveredResources.kmsVpcEndpointId === 'string',
|
|
1666
1821
|
secretsManager: discoveredResources.secretsManagerVpcEndpointId && typeof discoveredResources.secretsManagerVpcEndpointId === 'string',
|
|
1667
1822
|
sqs: discoveredResources.sqsVpcEndpointId && typeof discoveredResources.sqsVpcEndpointId === 'string',
|
|
@@ -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 () => {
|
|
@@ -1258,5 +1347,145 @@ describe('VpcBuilder', () => {
|
|
|
1258
1347
|
expect(result.outputs.PrivateSubnet2Id).toBeDefined();
|
|
1259
1348
|
});
|
|
1260
1349
|
});
|
|
1350
|
+
|
|
1351
|
+
describe('convertFlatDiscoveryToStructured - CloudFormation query results', () => {
|
|
1352
|
+
it('should add VPC from CloudFormation query to external array', () => {
|
|
1353
|
+
const flatDiscovery = {
|
|
1354
|
+
fromCloudFormationStack: true,
|
|
1355
|
+
stackName: 'test-stack',
|
|
1356
|
+
existingLogicalIds: ['FriggLambdaRouteTable', 'FriggLambdaSecurityGroup'],
|
|
1357
|
+
// VPC ID was extracted from security group query (NOT a stack resource)
|
|
1358
|
+
defaultVpcId: 'vpc-extracted-from-sg',
|
|
1359
|
+
lambdaSecurityGroupId: 'sg-123',
|
|
1360
|
+
routeTableId: 'rtb-123'
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1363
|
+
const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
|
|
1364
|
+
|
|
1365
|
+
// VPC should be in external array (discovered via query, not in stack)
|
|
1366
|
+
const vpcExternal = result.external.find(r => r.resourceType === 'AWS::EC2::VPC');
|
|
1367
|
+
expect(vpcExternal).toBeDefined();
|
|
1368
|
+
expect(vpcExternal.physicalId).toBe('vpc-extracted-from-sg');
|
|
1369
|
+
expect(vpcExternal.source).toBe('cloudformation-query');
|
|
1370
|
+
|
|
1371
|
+
// Security group SHOULD be in stackManaged (is in stack)
|
|
1372
|
+
const sgStack = result.stackManaged.find(r => r.logicalId === 'FriggLambdaSecurityGroup');
|
|
1373
|
+
expect(sgStack).toBeDefined();
|
|
1374
|
+
expect(sgStack.physicalId).toBe('sg-123');
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
it('should add subnets from route table associations to external array', () => {
|
|
1378
|
+
const flatDiscovery = {
|
|
1379
|
+
fromCloudFormationStack: true,
|
|
1380
|
+
stackName: 'test-stack',
|
|
1381
|
+
existingLogicalIds: ['FriggLambdaRouteTable'],
|
|
1382
|
+
routeTableId: 'rtb-123',
|
|
1383
|
+
// Subnets extracted from route table associations (NOT stack resources)
|
|
1384
|
+
privateSubnetId1: 'subnet-1',
|
|
1385
|
+
privateSubnetId2: 'subnet-2'
|
|
1386
|
+
};
|
|
1387
|
+
|
|
1388
|
+
const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
|
|
1389
|
+
|
|
1390
|
+
// Subnets should be in external array
|
|
1391
|
+
const subnet1 = result.external.find(r => r.physicalId === 'subnet-1');
|
|
1392
|
+
const subnet2 = result.external.find(r => r.physicalId === 'subnet-2');
|
|
1393
|
+
|
|
1394
|
+
expect(subnet1).toBeDefined();
|
|
1395
|
+
expect(subnet1.resourceType).toBe('AWS::EC2::Subnet');
|
|
1396
|
+
expect(subnet1.source).toBe('cloudformation-query');
|
|
1397
|
+
|
|
1398
|
+
expect(subnet2).toBeDefined();
|
|
1399
|
+
expect(subnet2.resourceType).toBe('AWS::EC2::Subnet');
|
|
1400
|
+
expect(subnet2.source).toBe('cloudformation-query');
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
it('should add NAT Gateway from route table queries to external array', () => {
|
|
1404
|
+
const flatDiscovery = {
|
|
1405
|
+
fromCloudFormationStack: true,
|
|
1406
|
+
stackName: 'test-stack',
|
|
1407
|
+
existingLogicalIds: ['FriggLambdaRouteTable', 'FriggPrivateRoute'],
|
|
1408
|
+
routeTableId: 'rtb-123',
|
|
1409
|
+
// NAT Gateway extracted from route table routes (NOT a stack resource)
|
|
1410
|
+
existingNatGatewayId: 'nat-extracted'
|
|
1411
|
+
};
|
|
1412
|
+
|
|
1413
|
+
const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
|
|
1414
|
+
|
|
1415
|
+
// NAT should be in external array
|
|
1416
|
+
const natExternal = result.external.find(r => r.resourceType === 'AWS::EC2::NatGateway');
|
|
1417
|
+
expect(natExternal).toBeDefined();
|
|
1418
|
+
expect(natExternal.physicalId).toBe('nat-extracted');
|
|
1419
|
+
expect(natExternal.source).toBe('cloudformation-query');
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
it('should NOT add resources to external if they are in stack', () => {
|
|
1423
|
+
const flatDiscovery = {
|
|
1424
|
+
fromCloudFormationStack: true,
|
|
1425
|
+
stackName: 'test-stack',
|
|
1426
|
+
existingLogicalIds: ['FriggVPC', 'FriggPrivateSubnet1'],
|
|
1427
|
+
// These ARE in the stack
|
|
1428
|
+
defaultVpcId: 'vpc-in-stack',
|
|
1429
|
+
privateSubnetId1: 'subnet-in-stack'
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
|
|
1433
|
+
|
|
1434
|
+
// Should be in stackManaged, NOT external
|
|
1435
|
+
expect(result.stackManaged.some(r => r.logicalId === 'FriggVPC')).toBe(true);
|
|
1436
|
+
expect(result.stackManaged.some(r => r.logicalId === 'FriggPrivateSubnet1')).toBe(true);
|
|
1437
|
+
|
|
1438
|
+
// Should NOT be in external
|
|
1439
|
+
expect(result.external.some(r => r.physicalId === 'vpc-in-stack')).toBe(false);
|
|
1440
|
+
expect(result.external.some(r => r.physicalId === 'subnet-in-stack')).toBe(false);
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
it('should handle Frontify pattern: stack resources + queried external references', () => {
|
|
1444
|
+
const flatDiscovery = {
|
|
1445
|
+
fromCloudFormationStack: true,
|
|
1446
|
+
stackName: 'create-frigg-app-production',
|
|
1447
|
+
existingLogicalIds: [
|
|
1448
|
+
'FriggLambdaSecurityGroup',
|
|
1449
|
+
'FriggLambdaRouteTable',
|
|
1450
|
+
'FriggPrivateRoute',
|
|
1451
|
+
'FriggPrivateSubnet1RouteTableAssociation',
|
|
1452
|
+
'FriggPrivateSubnet2RouteTableAssociation',
|
|
1453
|
+
'FriggS3VPCEndpoint',
|
|
1454
|
+
'FriggDynamoDBVPCEndpoint',
|
|
1455
|
+
'FriggKMSVPCEndpoint'
|
|
1456
|
+
],
|
|
1457
|
+
// Stack resources
|
|
1458
|
+
lambdaSecurityGroupId: 'sg-01002240c6a446202',
|
|
1459
|
+
routeTableId: 'rtb-08af43bbf0775602d',
|
|
1460
|
+
s3VpcEndpointId: 'vpce-s3',
|
|
1461
|
+
// External resources (discovered via queries)
|
|
1462
|
+
defaultVpcId: 'vpc-0cd17c0e06cb28b28',
|
|
1463
|
+
privateSubnetId1: 'subnet-034f6562dbbc16348',
|
|
1464
|
+
privateSubnetId2: 'subnet-0b8be2b82aeb5cdec',
|
|
1465
|
+
existingNatGatewayId: 'nat-022660c36a47e2d79'
|
|
1466
|
+
};
|
|
1467
|
+
|
|
1468
|
+
const result = vpcBuilder.convertFlatDiscoveryToStructured(flatDiscovery);
|
|
1469
|
+
|
|
1470
|
+
// Stack resources should be in stackManaged
|
|
1471
|
+
expect(result.stackManaged).toEqual(
|
|
1472
|
+
expect.arrayContaining([
|
|
1473
|
+
expect.objectContaining({ logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-01002240c6a446202' }),
|
|
1474
|
+
expect.objectContaining({ logicalId: 'FriggLambdaRouteTable', physicalId: 'rtb-08af43bbf0775602d' }),
|
|
1475
|
+
expect.objectContaining({ logicalId: 'FriggS3VPCEndpoint' })
|
|
1476
|
+
])
|
|
1477
|
+
);
|
|
1478
|
+
|
|
1479
|
+
// External resources should be in external array
|
|
1480
|
+
expect(result.external).toEqual(
|
|
1481
|
+
expect.arrayContaining([
|
|
1482
|
+
expect.objectContaining({ physicalId: 'vpc-0cd17c0e06cb28b28', resourceType: 'AWS::EC2::VPC', source: 'cloudformation-query' }),
|
|
1483
|
+
expect.objectContaining({ physicalId: 'subnet-034f6562dbbc16348', resourceType: 'AWS::EC2::Subnet', source: 'cloudformation-query' }),
|
|
1484
|
+
expect.objectContaining({ physicalId: 'subnet-0b8be2b82aeb5cdec', resourceType: 'AWS::EC2::Subnet', source: 'cloudformation-query' }),
|
|
1485
|
+
expect.objectContaining({ physicalId: 'nat-022660c36a47e2d79', resourceType: 'AWS::EC2::NatGateway', source: 'cloudformation-query' })
|
|
1486
|
+
])
|
|
1487
|
+
);
|
|
1488
|
+
});
|
|
1489
|
+
});
|
|
1261
1490
|
});
|
|
1262
1491
|
|
|
@@ -23,6 +23,7 @@ class VpcResourceResolver extends BaseResourceResolver {
|
|
|
23
23
|
*/
|
|
24
24
|
resolveVpc(appDefinition, discovery) {
|
|
25
25
|
const userIntent = appDefinition.vpc?.ownership?.vpc || 'auto';
|
|
26
|
+
const vpcManagement = appDefinition.vpc?.management; // Legacy config
|
|
26
27
|
|
|
27
28
|
// Explicit external
|
|
28
29
|
if (userIntent === 'external') {
|
|
@@ -43,20 +44,35 @@ class VpcResourceResolver extends BaseResourceResolver {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
// Auto-decide
|
|
46
|
-
|
|
47
|
+
const decision = this.resolveResourceOwnership(
|
|
47
48
|
'auto',
|
|
48
49
|
'FriggVPC',
|
|
49
50
|
'AWS::EC2::VPC',
|
|
50
51
|
discovery
|
|
51
52
|
);
|
|
53
|
+
|
|
54
|
+
// CRITICAL: If auto-resolution wants to create a VPC but management mode is 'discover' (or undefined),
|
|
55
|
+
// throw an error instead. Creating a VPC is expensive and should be explicit.
|
|
56
|
+
if (decision.ownership === ResourceOwnership.STACK &&
|
|
57
|
+
!decision.physicalId &&
|
|
58
|
+
vpcManagement !== 'create-new' &&
|
|
59
|
+
userIntent === 'auto') {
|
|
60
|
+
throw new Error(
|
|
61
|
+
'VPC discovery failed: No VPC found. ' +
|
|
62
|
+
'Either set vpc.management to "create-new" or provide vpc.vpcId with vpc.management "use-existing".'
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return decision;
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
/**
|
|
55
70
|
* Resolve Security Group ownership
|
|
56
71
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
72
|
+
* Logic:
|
|
73
|
+
* - If FriggLambdaSecurityGroup exists in stack → STACK (keep it)
|
|
74
|
+
* - If default SG discovered from VPC → EXTERNAL (use it)
|
|
75
|
+
* - Otherwise → STACK (create FriggLambdaSecurityGroup)
|
|
60
76
|
*
|
|
61
77
|
* @param {Object} appDefinition - App definition
|
|
62
78
|
* @param {Object} discovery - Discovery result
|
|
@@ -65,7 +81,7 @@ class VpcResourceResolver extends BaseResourceResolver {
|
|
|
65
81
|
resolveSecurityGroup(appDefinition, discovery) {
|
|
66
82
|
const userIntent = appDefinition.vpc?.ownership?.securityGroup || 'auto';
|
|
67
83
|
|
|
68
|
-
// Explicit external -
|
|
84
|
+
// Explicit external - use provided SG IDs
|
|
69
85
|
if (userIntent === 'external') {
|
|
70
86
|
this.requireExternalIds(
|
|
71
87
|
appDefinition.vpc?.external?.securityGroupIds,
|
|
@@ -77,21 +93,52 @@ class VpcResourceResolver extends BaseResourceResolver {
|
|
|
77
93
|
);
|
|
78
94
|
}
|
|
79
95
|
|
|
80
|
-
//
|
|
81
|
-
|
|
96
|
+
// Explicit stack - always create FriggLambdaSecurityGroup
|
|
97
|
+
if (userIntent === 'stack') {
|
|
98
|
+
const inStack = this.findInStack('FriggLambdaSecurityGroup', discovery);
|
|
99
|
+
return this.createStackDecision(
|
|
100
|
+
inStack?.physicalId,
|
|
101
|
+
inStack
|
|
102
|
+
? 'Found FriggLambdaSecurityGroup in CloudFormation stack'
|
|
103
|
+
: 'User specified ownership=stack - will create FriggLambdaSecurityGroup'
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Auto mode: Check stack first, then check for discovered default SG
|
|
82
108
|
const inStack = this.findInStack('FriggLambdaSecurityGroup', discovery);
|
|
83
109
|
|
|
84
110
|
if (inStack) {
|
|
85
111
|
return this.createStackDecision(
|
|
86
112
|
inStack.physicalId,
|
|
87
|
-
'Found FriggLambdaSecurityGroup in CloudFormation stack'
|
|
113
|
+
'Found FriggLambdaSecurityGroup in CloudFormation stack - must keep in template'
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Also check flat discovery for lambdaSecurityGroupId (from CloudFormation extraction)
|
|
118
|
+
const structured = discovery._structured || discovery;
|
|
119
|
+
const lambdaSgId = structured.lambdaSecurityGroupId || discovery.lambdaSecurityGroupId;
|
|
120
|
+
|
|
121
|
+
if (lambdaSgId) {
|
|
122
|
+
return this.createStackDecision(
|
|
123
|
+
lambdaSgId,
|
|
124
|
+
'Found FriggLambdaSecurityGroup in CloudFormation stack - must keep in template'
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check for discovered default security group (from external VPC pattern)
|
|
129
|
+
const defaultSgId = structured.defaultSecurityGroupId || discovery.defaultSecurityGroupId;
|
|
130
|
+
|
|
131
|
+
if (defaultSgId) {
|
|
132
|
+
return this.createExternalDecision(
|
|
133
|
+
[defaultSgId],
|
|
134
|
+
'Found default security group via discovery - will reuse (matches canary behavior)'
|
|
88
135
|
);
|
|
89
136
|
}
|
|
90
137
|
|
|
91
|
-
//
|
|
138
|
+
// No SG found anywhere - create new FriggLambdaSecurityGroup
|
|
92
139
|
return this.createStackDecision(
|
|
93
140
|
null,
|
|
94
|
-
'No
|
|
141
|
+
'No security group found - will create FriggLambdaSecurityGroup in stack'
|
|
95
142
|
);
|
|
96
143
|
}
|
|
97
144
|
|
|
@@ -109,6 +109,46 @@ describe('VpcResourceResolver', () => {
|
|
|
109
109
|
expect(decision.physicalId).toBeUndefined();
|
|
110
110
|
expect(decision.reason).toContain('No existing resource found');
|
|
111
111
|
});
|
|
112
|
+
|
|
113
|
+
it('should throw error when auto mode finds no VPC and management is not create-new', () => {
|
|
114
|
+
const appDefinition = {
|
|
115
|
+
vpc: {
|
|
116
|
+
enable: true,
|
|
117
|
+
// No management specified - defaults to discover
|
|
118
|
+
// No ownership specified - defaults to auto
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const discovery = {
|
|
122
|
+
stackManaged: [],
|
|
123
|
+
external: [],
|
|
124
|
+
fromCloudFormation: false
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Should throw error instead of trying to create VPC
|
|
128
|
+
expect(() => resolver.resolveVpc(appDefinition, discovery)).toThrow(
|
|
129
|
+
'VPC discovery failed: No VPC found'
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should allow creating VPC when management is create-new', () => {
|
|
134
|
+
const appDefinition = {
|
|
135
|
+
vpc: {
|
|
136
|
+
enable: true,
|
|
137
|
+
management: 'create-new'
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const discovery = {
|
|
141
|
+
stackManaged: [],
|
|
142
|
+
external: [],
|
|
143
|
+
fromCloudFormation: false
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Should NOT throw - create-new explicitly allows VPC creation
|
|
147
|
+
const decision = resolver.resolveVpc(appDefinition, discovery);
|
|
148
|
+
|
|
149
|
+
expect(decision.ownership).toBe(ResourceOwnership.STACK);
|
|
150
|
+
expect(decision.physicalId).toBeNull();
|
|
151
|
+
});
|
|
112
152
|
});
|
|
113
153
|
|
|
114
154
|
describe('resolveSecurityGroup', () => {
|