@friggframework/devtools 2.0.0--canary.490.feacde9.0 → 2.0.0--canary.497.a3f25f9.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/frigg-cli/deploy-command/index.js +3 -9
- package/infrastructure/README.md +0 -28
- package/infrastructure/domains/database/migration-builder.js +13 -19
- package/infrastructure/domains/database/migration-builder.test.js +0 -57
- package/infrastructure/domains/integration/integration-builder.js +14 -19
- package/infrastructure/domains/integration/integration-builder.test.js +74 -0
- package/infrastructure/domains/networking/vpc-builder.js +18 -240
- package/infrastructure/domains/networking/vpc-builder.test.js +13 -711
- package/infrastructure/domains/networking/vpc-resolver.js +40 -221
- package/infrastructure/domains/networking/vpc-resolver.test.js +18 -318
- package/infrastructure/domains/security/kms-builder.js +6 -55
- package/infrastructure/domains/security/kms-builder.test.js +1 -19
- package/infrastructure/domains/shared/cloudformation-discovery.js +13 -310
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +0 -395
- package/infrastructure/domains/shared/providers/aws-provider-adapter.js +6 -41
- package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +0 -39
- package/infrastructure/domains/shared/resource-discovery.js +5 -17
- package/infrastructure/domains/shared/resource-discovery.test.js +0 -36
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +17 -27
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +0 -73
- package/infrastructure/infrastructure-composer.js +3 -11
- package/infrastructure/scripts/build-prisma-layer.js +81 -8
- package/infrastructure/scripts/build-prisma-layer.test.js +53 -1
- package/infrastructure/scripts/verify-prisma-layer.js +72 -0
- package/package.json +7 -7
- package/layers/prisma/.build-complete +0 -3
|
@@ -9,7 +9,7 @@ describe('VpcResourceResolver', () => {
|
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
describe('resolveVpc', () => {
|
|
12
|
-
it('should resolve to EXTERNAL
|
|
12
|
+
it('should resolve to EXTERNAL when user specifies external', () => {
|
|
13
13
|
const appDefinition = {
|
|
14
14
|
vpc: {
|
|
15
15
|
ownership: { vpc: 'external' },
|
|
@@ -22,46 +22,20 @@ describe('VpcResourceResolver', () => {
|
|
|
22
22
|
|
|
23
23
|
expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
24
24
|
expect(decision.physicalId).toBe('vpc-external-123');
|
|
25
|
-
expect(decision.reason).toContain('
|
|
25
|
+
expect(decision.reason).toContain('User specified ownership=external');
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
it('should
|
|
28
|
+
it('should throw when external specified but vpcId missing', () => {
|
|
29
29
|
const appDefinition = {
|
|
30
30
|
vpc: {
|
|
31
|
-
ownership: { vpc: 'external' }
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
const discovery = {
|
|
36
|
-
stackManaged: [],
|
|
37
|
-
external: [],
|
|
38
|
-
fromCloudFormation: false,
|
|
39
|
-
defaultVpcId: 'vpc-discovered-123'
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const decision = resolver.resolveVpc(appDefinition, discovery);
|
|
43
|
-
|
|
44
|
-
expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
45
|
-
expect(decision.physicalId).toBe('vpc-discovered-123');
|
|
46
|
-
expect(decision.reason).toContain('discovered VPC');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should throw when external specified but no vpcId and no discovery', () => {
|
|
50
|
-
const appDefinition = {
|
|
51
|
-
vpc: {
|
|
52
|
-
ownership: { vpc: 'external' }
|
|
53
|
-
// No external.vpcId provided
|
|
31
|
+
ownership: { vpc: 'external' },
|
|
32
|
+
external: {}
|
|
54
33
|
}
|
|
55
34
|
};
|
|
56
|
-
const discovery = {
|
|
57
|
-
stackManaged: [],
|
|
58
|
-
external: [],
|
|
59
|
-
fromCloudFormation: false
|
|
60
|
-
// No defaultVpcId discovered
|
|
61
|
-
};
|
|
35
|
+
const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
|
|
62
36
|
|
|
63
37
|
expect(() => resolver.resolveVpc(appDefinition, discovery)).toThrow(
|
|
64
|
-
|
|
38
|
+
"ownership='external' for vpcId requires external.vpcId"
|
|
65
39
|
);
|
|
66
40
|
});
|
|
67
41
|
|
|
@@ -123,63 +97,22 @@ describe('VpcResourceResolver', () => {
|
|
|
123
97
|
expect(decision.physicalId).toBe('vpc-external');
|
|
124
98
|
});
|
|
125
99
|
|
|
126
|
-
it('should
|
|
100
|
+
it('should auto-resolve to STACK when not found (create new)', () => {
|
|
127
101
|
const appDefinition = {
|
|
128
102
|
vpc: { ownership: { vpc: 'auto' } }
|
|
129
|
-
// No management specified - defaults to discover
|
|
130
103
|
};
|
|
131
104
|
const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
|
|
132
105
|
|
|
133
|
-
// NEW BEHAVIOR: Auto mode with no VPC found should throw error (prevent accidental VPC creation)
|
|
134
|
-
expect(() => resolver.resolveVpc(appDefinition, discovery)).toThrow(
|
|
135
|
-
'VPC discovery failed: No VPC found'
|
|
136
|
-
);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('should throw error when auto mode finds no VPC and management is not create-new', () => {
|
|
140
|
-
const appDefinition = {
|
|
141
|
-
vpc: {
|
|
142
|
-
enable: true,
|
|
143
|
-
// No management specified - defaults to discover
|
|
144
|
-
// No ownership specified - defaults to auto
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
const discovery = {
|
|
148
|
-
stackManaged: [],
|
|
149
|
-
external: [],
|
|
150
|
-
fromCloudFormation: false
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
// Should throw error instead of trying to create VPC
|
|
154
|
-
expect(() => resolver.resolveVpc(appDefinition, discovery)).toThrow(
|
|
155
|
-
'VPC discovery failed: No VPC found'
|
|
156
|
-
);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('should allow creating VPC when management is create-new', () => {
|
|
160
|
-
const appDefinition = {
|
|
161
|
-
vpc: {
|
|
162
|
-
enable: true,
|
|
163
|
-
management: 'create-new',
|
|
164
|
-
ownership: { vpc: 'auto' }
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
const discovery = {
|
|
168
|
-
stackManaged: [],
|
|
169
|
-
external: [],
|
|
170
|
-
fromCloudFormation: false
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
// Should NOT throw - create-new explicitly allows VPC creation
|
|
174
106
|
const decision = resolver.resolveVpc(appDefinition, discovery);
|
|
175
|
-
|
|
107
|
+
|
|
176
108
|
expect(decision.ownership).toBe(ResourceOwnership.STACK);
|
|
177
|
-
expect(decision.physicalId).toBeUndefined();
|
|
109
|
+
expect(decision.physicalId).toBeUndefined();
|
|
110
|
+
expect(decision.reason).toContain('No existing resource found');
|
|
178
111
|
});
|
|
179
112
|
});
|
|
180
113
|
|
|
181
114
|
describe('resolveSecurityGroup', () => {
|
|
182
|
-
it('should resolve to EXTERNAL with user-provided
|
|
115
|
+
it('should resolve to EXTERNAL with user-provided IDs', () => {
|
|
183
116
|
const appDefinition = {
|
|
184
117
|
vpc: {
|
|
185
118
|
ownership: { securityGroup: 'external' },
|
|
@@ -192,71 +125,6 @@ describe('VpcResourceResolver', () => {
|
|
|
192
125
|
|
|
193
126
|
expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
194
127
|
expect(decision.physicalIds).toEqual(['sg-1', 'sg-2']);
|
|
195
|
-
expect(decision.reason).toContain('hardcoded');
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('should resolve to EXTERNAL using discovered default SG when no hardcoded IDs', () => {
|
|
199
|
-
const appDefinition = {
|
|
200
|
-
vpc: {
|
|
201
|
-
ownership: { securityGroup: 'external' }
|
|
202
|
-
// No external.securityGroupIds provided
|
|
203
|
-
}
|
|
204
|
-
};
|
|
205
|
-
const discovery = {
|
|
206
|
-
stackManaged: [],
|
|
207
|
-
external: [],
|
|
208
|
-
fromCloudFormation: false,
|
|
209
|
-
defaultSecurityGroupId: 'sg-discovered-default'
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const decision = resolver.resolveSecurityGroup(appDefinition, discovery);
|
|
213
|
-
|
|
214
|
-
expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
215
|
-
expect(decision.physicalIds).toEqual(['sg-discovered-default']);
|
|
216
|
-
expect(decision.reason).toContain('discovered default security group');
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('should throw error when ownership=external but no IDs and no discovery', () => {
|
|
220
|
-
const appDefinition = {
|
|
221
|
-
vpc: {
|
|
222
|
-
ownership: { securityGroup: 'external' }
|
|
223
|
-
// No external.securityGroupIds provided
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
const discovery = {
|
|
227
|
-
stackManaged: [],
|
|
228
|
-
external: [],
|
|
229
|
-
fromCloudFormation: false
|
|
230
|
-
// No defaultSecurityGroupId discovered
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
expect(() => resolver.resolveSecurityGroup(appDefinition, discovery)).toThrow(
|
|
234
|
-
/ownership='external' for securityGroup requires either/
|
|
235
|
-
);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('should prefer default SG over stack-managed SG when ownership=external and both discovered', () => {
|
|
239
|
-
const appDefinition = {
|
|
240
|
-
vpc: {
|
|
241
|
-
ownership: { securityGroup: 'external' }
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
|
-
const discovery = {
|
|
245
|
-
stackManaged: [
|
|
246
|
-
{ logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-stack-managed', resourceType: 'AWS::EC2::SecurityGroup' }
|
|
247
|
-
],
|
|
248
|
-
external: [],
|
|
249
|
-
fromCloudFormation: true,
|
|
250
|
-
lambdaSecurityGroupId: 'sg-stack-managed', // Stack-managed SG
|
|
251
|
-
defaultSecurityGroupId: 'sg-default-vpc' // Default VPC SG
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
const decision = resolver.resolveSecurityGroup(appDefinition, discovery);
|
|
255
|
-
|
|
256
|
-
// Should use default SG, NOT the stack-managed one
|
|
257
|
-
expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
258
|
-
expect(decision.physicalIds).toEqual(['sg-default-vpc']);
|
|
259
|
-
expect(decision.reason).toContain('discovered default security group');
|
|
260
128
|
});
|
|
261
129
|
|
|
262
130
|
it('should auto-resolve to STACK when FriggLambdaSecurityGroup in stack', () => {
|
|
@@ -278,7 +146,7 @@ describe('VpcResourceResolver', () => {
|
|
|
278
146
|
});
|
|
279
147
|
|
|
280
148
|
describe('resolveSubnets', () => {
|
|
281
|
-
it('should resolve to EXTERNAL with user-provided
|
|
149
|
+
it('should resolve to EXTERNAL with user-provided subnet IDs', () => {
|
|
282
150
|
const appDefinition = {
|
|
283
151
|
vpc: {
|
|
284
152
|
ownership: { subnets: 'external' },
|
|
@@ -291,48 +159,6 @@ describe('VpcResourceResolver', () => {
|
|
|
291
159
|
|
|
292
160
|
expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
293
161
|
expect(decision.physicalIds).toEqual(['subnet-1', 'subnet-2', 'subnet-3']);
|
|
294
|
-
expect(decision.reason).toContain('hardcoded');
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it('should resolve to EXTERNAL using discovered subnets when no hardcoded IDs', () => {
|
|
298
|
-
const appDefinition = {
|
|
299
|
-
vpc: {
|
|
300
|
-
ownership: { subnets: 'external' }
|
|
301
|
-
// No external.subnetIds provided
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
const discovery = {
|
|
305
|
-
stackManaged: [],
|
|
306
|
-
external: [],
|
|
307
|
-
fromCloudFormation: false,
|
|
308
|
-
privateSubnetId1: 'subnet-discovered-1',
|
|
309
|
-
privateSubnetId2: 'subnet-discovered-2'
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
const decision = resolver.resolveSubnets(appDefinition, discovery);
|
|
313
|
-
|
|
314
|
-
expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
315
|
-
expect(decision.physicalIds).toEqual(['subnet-discovered-1', 'subnet-discovered-2']);
|
|
316
|
-
expect(decision.reason).toContain('discovered subnets');
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it('should throw error when ownership=external but no IDs and no discovery', () => {
|
|
320
|
-
const appDefinition = {
|
|
321
|
-
vpc: {
|
|
322
|
-
ownership: { subnets: 'external' }
|
|
323
|
-
// No external.subnetIds provided
|
|
324
|
-
}
|
|
325
|
-
};
|
|
326
|
-
const discovery = {
|
|
327
|
-
stackManaged: [],
|
|
328
|
-
external: [],
|
|
329
|
-
fromCloudFormation: false
|
|
330
|
-
// No privateSubnetId1/2 discovered
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
expect(() => resolver.resolveSubnets(appDefinition, discovery)).toThrow(
|
|
334
|
-
/ownership='external' for subnets requires either/
|
|
335
|
-
);
|
|
336
162
|
});
|
|
337
163
|
|
|
338
164
|
it('should resolve to STACK when subnets found in stack', () => {
|
|
@@ -401,7 +227,7 @@ describe('VpcResourceResolver', () => {
|
|
|
401
227
|
expect(decision.reason).toContain('NAT Gateway disabled');
|
|
402
228
|
});
|
|
403
229
|
|
|
404
|
-
it('should resolve to EXTERNAL with user-provided
|
|
230
|
+
it('should resolve to EXTERNAL with user-provided ID', () => {
|
|
405
231
|
const appDefinition = {
|
|
406
232
|
vpc: {
|
|
407
233
|
ownership: { natGateway: 'external' },
|
|
@@ -414,47 +240,6 @@ describe('VpcResourceResolver', () => {
|
|
|
414
240
|
|
|
415
241
|
expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
416
242
|
expect(decision.physicalId).toBe('nat-external-123');
|
|
417
|
-
expect(decision.reason).toContain('hardcoded');
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
it('should resolve to EXTERNAL using discovered NAT when no hardcoded ID', () => {
|
|
421
|
-
const appDefinition = {
|
|
422
|
-
vpc: {
|
|
423
|
-
ownership: { natGateway: 'external' }
|
|
424
|
-
// No external.natGatewayId provided
|
|
425
|
-
}
|
|
426
|
-
};
|
|
427
|
-
const discovery = {
|
|
428
|
-
stackManaged: [],
|
|
429
|
-
external: [],
|
|
430
|
-
fromCloudFormation: false,
|
|
431
|
-
natGatewayId: 'nat-discovered-123'
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
const decision = resolver.resolveNatGateway(appDefinition, discovery);
|
|
435
|
-
|
|
436
|
-
expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
437
|
-
expect(decision.physicalId).toBe('nat-discovered-123');
|
|
438
|
-
expect(decision.reason).toContain('discovered NAT gateway');
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
it('should throw error when ownership=external but no ID and no discovery', () => {
|
|
442
|
-
const appDefinition = {
|
|
443
|
-
vpc: {
|
|
444
|
-
ownership: { natGateway: 'external' }
|
|
445
|
-
// No external.natGatewayId provided
|
|
446
|
-
}
|
|
447
|
-
};
|
|
448
|
-
const discovery = {
|
|
449
|
-
stackManaged: [],
|
|
450
|
-
external: [],
|
|
451
|
-
fromCloudFormation: false
|
|
452
|
-
// No natGatewayId discovered
|
|
453
|
-
};
|
|
454
|
-
|
|
455
|
-
expect(() => resolver.resolveNatGateway(appDefinition, discovery)).toThrow(
|
|
456
|
-
/ownership='external' for NAT gateway requires either/
|
|
457
|
-
);
|
|
458
243
|
});
|
|
459
244
|
|
|
460
245
|
it('should auto-resolve to STACK when found in stack', () => {
|
|
@@ -475,82 +260,6 @@ describe('VpcResourceResolver', () => {
|
|
|
475
260
|
});
|
|
476
261
|
|
|
477
262
|
describe('resolveVpcEndpoints', () => {
|
|
478
|
-
it('should skip DynamoDB endpoint when application uses MongoDB', () => {
|
|
479
|
-
const appDefinition = {
|
|
480
|
-
vpc: { ownership: { vpcEndpoints: 'auto' } },
|
|
481
|
-
database: { mongoDB: { enable: true } }, // Using MongoDB, not DynamoDB
|
|
482
|
-
encryption: { fieldLevelEncryptionMethod: 'kms' }
|
|
483
|
-
};
|
|
484
|
-
const discovery = {
|
|
485
|
-
stackManaged: [],
|
|
486
|
-
external: [],
|
|
487
|
-
fromCloudFormation: false
|
|
488
|
-
};
|
|
489
|
-
|
|
490
|
-
const decisions = resolver.resolveVpcEndpoints(appDefinition, discovery);
|
|
491
|
-
|
|
492
|
-
expect(decisions.s3.ownership).toBe('stack'); // S3 always needed
|
|
493
|
-
expect(decisions.dynamodb.ownership).toBeNull(); // DynamoDB NOT needed
|
|
494
|
-
expect(decisions.dynamodb.reason).toContain('MongoDB/PostgreSQL');
|
|
495
|
-
expect(decisions.kms.ownership).toBe('stack'); // KMS needed (encryption enabled)
|
|
496
|
-
expect(decisions.secretsManager.ownership).toBe('stack'); // SM always needed
|
|
497
|
-
expect(decisions.sqs.ownership).toBe('stack'); // SQS always needed
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
it('should skip DynamoDB endpoint when application uses PostgreSQL', () => {
|
|
501
|
-
const appDefinition = {
|
|
502
|
-
vpc: { ownership: { vpcEndpoints: 'auto' } },
|
|
503
|
-
database: { postgres: { enable: true } }, // Using PostgreSQL, not DynamoDB
|
|
504
|
-
};
|
|
505
|
-
const discovery = {
|
|
506
|
-
stackManaged: [],
|
|
507
|
-
external: [],
|
|
508
|
-
fromCloudFormation: false
|
|
509
|
-
};
|
|
510
|
-
|
|
511
|
-
const decisions = resolver.resolveVpcEndpoints(appDefinition, discovery);
|
|
512
|
-
|
|
513
|
-
expect(decisions.dynamodb.ownership).toBeNull();
|
|
514
|
-
expect(decisions.dynamodb.reason).toContain('MongoDB/PostgreSQL');
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
it('should create DynamoDB endpoint when explicitly enabled', () => {
|
|
518
|
-
const appDefinition = {
|
|
519
|
-
vpc: { ownership: { vpcEndpoints: 'auto' } },
|
|
520
|
-
database: { dynamodb: { enable: true } }, // Explicitly using DynamoDB
|
|
521
|
-
};
|
|
522
|
-
const discovery = {
|
|
523
|
-
stackManaged: [],
|
|
524
|
-
external: [],
|
|
525
|
-
fromCloudFormation: false
|
|
526
|
-
};
|
|
527
|
-
|
|
528
|
-
const decisions = resolver.resolveVpcEndpoints(appDefinition, discovery);
|
|
529
|
-
|
|
530
|
-
expect(decisions.dynamodb.ownership).toBe('stack'); // DynamoDB needed
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
it('should allow deletion of DynamoDB endpoint when not needed', () => {
|
|
534
|
-
const appDefinition = {
|
|
535
|
-
vpc: { ownership: { vpcEndpoints: 'auto' } },
|
|
536
|
-
database: { mongoDB: { enable: true } }, // Using MongoDB (DynamoDB not needed)
|
|
537
|
-
};
|
|
538
|
-
const discovery = {
|
|
539
|
-
stackManaged: [
|
|
540
|
-
{ logicalId: 'FriggDynamoDBVPCEndpoint', physicalId: 'vpce-ddb-legacy', resourceType: 'AWS::EC2::VPCEndpoint' }
|
|
541
|
-
],
|
|
542
|
-
external: [],
|
|
543
|
-
fromCloudFormation: true
|
|
544
|
-
};
|
|
545
|
-
|
|
546
|
-
const decisions = resolver.resolveVpcEndpoints(appDefinition, discovery);
|
|
547
|
-
|
|
548
|
-
// Should return null (allow CloudFormation to delete it since not needed)
|
|
549
|
-
expect(decisions.dynamodb.ownership).toBeNull();
|
|
550
|
-
expect(decisions.dynamodb.reason).toContain('MongoDB/PostgreSQL');
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
|
|
554
263
|
it('should return null decisions when endpoints disabled', () => {
|
|
555
264
|
const appDefinition = {
|
|
556
265
|
vpc: {
|
|
@@ -579,8 +288,7 @@ describe('VpcResourceResolver', () => {
|
|
|
579
288
|
dynamodb: 'vpce-ddb-456'
|
|
580
289
|
}
|
|
581
290
|
}
|
|
582
|
-
}
|
|
583
|
-
database: { dynamodb: { enable: true } } // Enable DynamoDB
|
|
291
|
+
}
|
|
584
292
|
};
|
|
585
293
|
const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
|
|
586
294
|
|
|
@@ -594,10 +302,7 @@ describe('VpcResourceResolver', () => {
|
|
|
594
302
|
});
|
|
595
303
|
|
|
596
304
|
it('should auto-resolve to STACK when endpoints found in stack', () => {
|
|
597
|
-
const appDefinition = {
|
|
598
|
-
vpc: { ownership: { vpcEndpoints: 'auto' } },
|
|
599
|
-
database: { dynamodb: { enable: true } } // Enable DynamoDB
|
|
600
|
-
};
|
|
305
|
+
const appDefinition = { vpc: { ownership: { vpcEndpoints: 'auto' } } };
|
|
601
306
|
const discovery = {
|
|
602
307
|
stackManaged: [
|
|
603
308
|
{ logicalId: 'FriggS3VPCEndpoint', physicalId: 'vpce-s3-stack', resourceType: 'AWS::EC2::VPCEndpoint' },
|
|
@@ -618,8 +323,7 @@ describe('VpcResourceResolver', () => {
|
|
|
618
323
|
it('should auto-resolve mixed: some in stack, some new', () => {
|
|
619
324
|
const appDefinition = {
|
|
620
325
|
vpc: { ownership: { vpcEndpoints: 'auto' } },
|
|
621
|
-
encryption: { fieldLevelEncryptionMethod: 'kms' }
|
|
622
|
-
database: { dynamodb: { enable: true } } // Enable DynamoDB
|
|
326
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' } // Enable KMS endpoint
|
|
623
327
|
};
|
|
624
328
|
const discovery = {
|
|
625
329
|
stackManaged: [
|
|
@@ -716,11 +420,7 @@ describe('VpcResourceResolver', () => {
|
|
|
716
420
|
describe('real-world scenarios', () => {
|
|
717
421
|
it('scenario: fresh deploy, no resources exist', () => {
|
|
718
422
|
const appDefinition = {
|
|
719
|
-
vpc: {
|
|
720
|
-
enable: true,
|
|
721
|
-
ownership: {},
|
|
722
|
-
management: 'create-new' // Explicitly allow VPC creation
|
|
723
|
-
}
|
|
423
|
+
vpc: { enable: true, ownership: {} }
|
|
724
424
|
};
|
|
725
425
|
const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
|
|
726
426
|
|
|
@@ -78,20 +78,8 @@ class KmsBuilder extends InfrastructureBuilder {
|
|
|
78
78
|
const resolver = new KmsResourceResolver();
|
|
79
79
|
const decisions = resolver.resolveAll(appDefinition, discovery);
|
|
80
80
|
|
|
81
|
-
// Check if external key exists (for accurate logging)
|
|
82
|
-
const externalKmsKey = discoveredResources?.defaultKmsKeyId ||
|
|
83
|
-
discoveredResources?.kmsKeyArn ||
|
|
84
|
-
discoveredResources?.kmsKeyId;
|
|
85
|
-
const willUseExternal = decisions.key.ownership === ResourceOwnership.STACK &&
|
|
86
|
-
!decisions.key.physicalId &&
|
|
87
|
-
externalKmsKey;
|
|
88
|
-
|
|
89
81
|
console.log('\n 📋 Resource Ownership Decisions:');
|
|
90
|
-
|
|
91
|
-
console.log(` Key: external - Found external KMS key (not in stack)`);
|
|
92
|
-
} else {
|
|
93
|
-
console.log(` Key: ${decisions.key.ownership} - ${decisions.key.reason}`);
|
|
94
|
-
}
|
|
82
|
+
console.log(` Key: ${decisions.key.ownership} - ${decisions.key.reason}`);
|
|
95
83
|
|
|
96
84
|
// Build resources based on ownership decisions
|
|
97
85
|
await this.buildFromDecisions(decisions, appDefinition, discoveredResources, result);
|
|
@@ -253,30 +241,13 @@ class KmsBuilder extends InfrastructureBuilder {
|
|
|
253
241
|
if (decisions.key.ownership === ResourceOwnership.STACK && decisions.key.physicalId) {
|
|
254
242
|
// Key exists in stack - add definitions (CloudFormation idempotency)
|
|
255
243
|
console.log(' → Adding KMS definitions to template (existing in stack)');
|
|
256
|
-
|
|
257
|
-
// CRITICAL: Check if alias exists in stack before trying to create it
|
|
258
|
-
// Matches old serverless-template.js behavior: only create alias if it doesn't exist
|
|
259
|
-
const aliasExistsInStack = discoveredResources?.existingLogicalIds?.includes('FriggKMSKeyAlias');
|
|
260
|
-
if (!aliasExistsInStack) {
|
|
261
|
-
if (appDefinition.encryption?.kmsKeyAlias !== true) {
|
|
262
|
-
// Alias doesn't exist in stack - skip creation unless explicitly enabled
|
|
263
|
-
// This avoids kms:CreateAlias permission errors
|
|
264
|
-
console.log(' ℹ KMS alias not in stack - skipping creation (set kmsKeyAlias: true to force)');
|
|
265
|
-
appDefinition.encryption = appDefinition.encryption || {};
|
|
266
|
-
appDefinition.encryption.kmsKeyAlias = false;
|
|
267
|
-
} else {
|
|
268
|
-
console.log(' → Will create KMS alias (kmsKeyAlias: true explicitly set)');
|
|
269
|
-
}
|
|
270
|
-
} else {
|
|
271
|
-
console.log(' ✓ KMS alias found in stack - will keep in template');
|
|
272
|
-
}
|
|
273
|
-
|
|
274
244
|
result.resources = this.createKmsKey(appDefinition);
|
|
275
245
|
result.environment.KMS_KEY_ARN = { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] };
|
|
276
246
|
console.log(' ✅ KMS key resources created');
|
|
277
247
|
} else if (decisions.key.ownership === ResourceOwnership.STACK && !decisions.key.physicalId && externalKmsKey) {
|
|
278
248
|
// ORPHANED KEY FIX: Key exists externally but not in stack
|
|
279
249
|
// Use it as external instead of trying to create (would fail with "already exists")
|
|
250
|
+
console.log(' ⚠️ KMS key exists externally but not in stack - using as external resource');
|
|
280
251
|
console.log(` → Using external KMS key: ${externalKmsKey}`);
|
|
281
252
|
|
|
282
253
|
// Format as ARN if it's just a key ID
|
|
@@ -288,17 +259,6 @@ class KmsBuilder extends InfrastructureBuilder {
|
|
|
288
259
|
} else if (decisions.key.ownership === ResourceOwnership.STACK && !decisions.key.physicalId && !useEnvVarFallback) {
|
|
289
260
|
// Create new KMS key (only if not using env var fallback and no external key found)
|
|
290
261
|
console.log(' → Creating new KMS key in stack');
|
|
291
|
-
|
|
292
|
-
// CRITICAL: Don't create alias by default to avoid kms:CreateAlias permission errors
|
|
293
|
-
// Matches old serverless-template.js behavior: only create alias if explicitly requested
|
|
294
|
-
if (appDefinition.encryption?.kmsKeyAlias !== true) {
|
|
295
|
-
console.log(' ℹ Skipping KMS alias creation by default (set kmsKeyAlias: true to enable)');
|
|
296
|
-
appDefinition.encryption = appDefinition.encryption || {};
|
|
297
|
-
appDefinition.encryption.kmsKeyAlias = false;
|
|
298
|
-
} else {
|
|
299
|
-
console.log(' → Will create KMS alias (kmsKeyAlias: true explicitly set)');
|
|
300
|
-
}
|
|
301
|
-
|
|
302
262
|
result.resources = this.createKmsKey(appDefinition);
|
|
303
263
|
result.environment.KMS_KEY_ARN = { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] };
|
|
304
264
|
console.log(' ✅ KMS key resources created');
|
|
@@ -336,7 +296,7 @@ class KmsBuilder extends InfrastructureBuilder {
|
|
|
336
296
|
* Create KMS key CloudFormation resources
|
|
337
297
|
*/
|
|
338
298
|
createKmsKey(appDefinition) {
|
|
339
|
-
|
|
299
|
+
return {
|
|
340
300
|
FriggKMSKey: {
|
|
341
301
|
Type: 'AWS::KMS::Key',
|
|
342
302
|
DeletionPolicy: 'Retain',
|
|
@@ -390,24 +350,15 @@ class KmsBuilder extends InfrastructureBuilder {
|
|
|
390
350
|
],
|
|
391
351
|
},
|
|
392
352
|
},
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
// Only create alias if explicitly enabled (default: true for backwards compatibility)
|
|
396
|
-
const createAlias = appDefinition.encryption?.kmsKeyAlias !== false;
|
|
397
|
-
if (createAlias) {
|
|
398
|
-
resources.FriggKMSKeyAlias = {
|
|
353
|
+
FriggKMSKeyAlias: {
|
|
399
354
|
Type: 'AWS::KMS::Alias',
|
|
400
355
|
DeletionPolicy: 'Retain',
|
|
401
356
|
Properties: {
|
|
402
357
|
AliasName: 'alias/${self:service}-${self:provider.stage}-frigg-kms',
|
|
403
358
|
TargetKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] },
|
|
404
359
|
},
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
console.log(' ℹ Skipping KMS key alias creation (kmsKeyAlias: false)');
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
return resources;
|
|
360
|
+
},
|
|
361
|
+
};
|
|
411
362
|
}
|
|
412
363
|
}
|
|
413
364
|
|
|
@@ -209,12 +209,11 @@ describe('KmsBuilder', () => {
|
|
|
209
209
|
expect(result.resources.FriggKMSKey.Type).toBe('AWS::KMS::Key');
|
|
210
210
|
});
|
|
211
211
|
|
|
212
|
-
it('should create KMS key alias
|
|
212
|
+
it('should create KMS key alias', async () => {
|
|
213
213
|
const appDefinition = {
|
|
214
214
|
encryption: {
|
|
215
215
|
fieldLevelEncryptionMethod: 'kms',
|
|
216
216
|
createResourceIfNoneFound: true,
|
|
217
|
-
kmsKeyAlias: true, // Explicitly enable alias creation
|
|
218
217
|
},
|
|
219
218
|
};
|
|
220
219
|
|
|
@@ -226,23 +225,6 @@ describe('KmsBuilder', () => {
|
|
|
226
225
|
expect(result.resources.FriggKMSKeyAlias.Type).toBe('AWS::KMS::Alias');
|
|
227
226
|
});
|
|
228
227
|
|
|
229
|
-
it('should skip alias creation when kmsKeyAlias: false', async () => {
|
|
230
|
-
const appDefinition = {
|
|
231
|
-
encryption: {
|
|
232
|
-
fieldLevelEncryptionMethod: 'kms',
|
|
233
|
-
createResourceIfNoneFound: true,
|
|
234
|
-
kmsKeyAlias: false,
|
|
235
|
-
},
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
const discoveredResources = {};
|
|
239
|
-
|
|
240
|
-
const result = await kmsBuilder.build(appDefinition, discoveredResources);
|
|
241
|
-
|
|
242
|
-
expect(result.resources.FriggKMSKey).toBeDefined();
|
|
243
|
-
expect(result.resources.FriggKMSKeyAlias).toBeUndefined();
|
|
244
|
-
});
|
|
245
|
-
|
|
246
228
|
it('should enable key rotation for new keys', async () => {
|
|
247
229
|
const appDefinition = {
|
|
248
230
|
encryption: {
|