@friggframework/devtools 2.0.0-next.53 → 2.0.0-next.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frigg-cli/README.md +13 -14
- package/frigg-cli/__tests__/unit/commands/db-setup.test.js +267 -166
- package/frigg-cli/__tests__/unit/utils/database-validator.test.js +45 -14
- package/frigg-cli/__tests__/unit/utils/error-messages.test.js +44 -3
- package/frigg-cli/db-setup-command/index.js +75 -22
- package/frigg-cli/deploy-command/index.js +6 -3
- package/frigg-cli/utils/database-validator.js +18 -5
- package/frigg-cli/utils/error-messages.js +84 -12
- package/infrastructure/README.md +28 -0
- package/infrastructure/domains/database/migration-builder.js +26 -20
- package/infrastructure/domains/database/migration-builder.test.js +27 -0
- package/infrastructure/domains/integration/integration-builder.js +17 -13
- package/infrastructure/domains/integration/integration-builder.test.js +23 -0
- package/infrastructure/domains/networking/vpc-builder.js +240 -18
- package/infrastructure/domains/networking/vpc-builder.test.js +711 -13
- package/infrastructure/domains/networking/vpc-resolver.js +221 -40
- package/infrastructure/domains/networking/vpc-resolver.test.js +318 -18
- package/infrastructure/domains/security/kms-builder.js +55 -6
- package/infrastructure/domains/security/kms-builder.test.js +19 -1
- package/infrastructure/domains/shared/cloudformation-discovery.js +310 -13
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +395 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.js +41 -6
- package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +39 -0
- package/infrastructure/domains/shared/resource-discovery.js +17 -5
- package/infrastructure/domains/shared/resource-discovery.test.js +36 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +30 -20
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +43 -0
- package/infrastructure/infrastructure-composer.js +11 -3
- package/infrastructure/scripts/build-prisma-layer.js +153 -78
- package/infrastructure/scripts/build-prisma-layer.test.js +27 -11
- package/layers/prisma/.build-complete +3 -0
- package/package.json +7 -7
|
@@ -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 with hardcoded vpcId', () => {
|
|
13
13
|
const appDefinition = {
|
|
14
14
|
vpc: {
|
|
15
15
|
ownership: { vpc: 'external' },
|
|
@@ -22,20 +22,46 @@ 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('hardcoded vpcId');
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
it('should
|
|
28
|
+
it('should resolve to EXTERNAL using discovered VPC when no hardcoded ID', () => {
|
|
29
29
|
const appDefinition = {
|
|
30
30
|
vpc: {
|
|
31
|
-
ownership: { vpc: 'external' }
|
|
32
|
-
external
|
|
31
|
+
ownership: { vpc: 'external' }
|
|
32
|
+
// No external.vpcId provided
|
|
33
33
|
}
|
|
34
34
|
};
|
|
35
|
-
const discovery = {
|
|
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
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const discovery = {
|
|
57
|
+
stackManaged: [],
|
|
58
|
+
external: [],
|
|
59
|
+
fromCloudFormation: false
|
|
60
|
+
// No defaultVpcId discovered
|
|
61
|
+
};
|
|
36
62
|
|
|
37
63
|
expect(() => resolver.resolveVpc(appDefinition, discovery)).toThrow(
|
|
38
|
-
|
|
64
|
+
/ownership='external' for VPC requires either/
|
|
39
65
|
);
|
|
40
66
|
});
|
|
41
67
|
|
|
@@ -97,22 +123,63 @@ describe('VpcResourceResolver', () => {
|
|
|
97
123
|
expect(decision.physicalId).toBe('vpc-external');
|
|
98
124
|
});
|
|
99
125
|
|
|
100
|
-
it('should auto
|
|
126
|
+
it('should throw error when auto mode finds no VPC (changed behavior)', () => {
|
|
101
127
|
const appDefinition = {
|
|
102
128
|
vpc: { ownership: { vpc: 'auto' } }
|
|
129
|
+
// No management specified - defaults to discover
|
|
103
130
|
};
|
|
104
131
|
const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
|
|
105
132
|
|
|
106
|
-
|
|
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
|
+
});
|
|
107
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
|
+
const decision = resolver.resolveVpc(appDefinition, discovery);
|
|
175
|
+
|
|
108
176
|
expect(decision.ownership).toBe(ResourceOwnership.STACK);
|
|
109
|
-
expect(decision.physicalId).toBeUndefined();
|
|
110
|
-
expect(decision.reason).toContain('No existing resource found');
|
|
177
|
+
expect(decision.physicalId).toBeUndefined(); // resolveResourceOwnership returns undefined, not null
|
|
111
178
|
});
|
|
112
179
|
});
|
|
113
180
|
|
|
114
181
|
describe('resolveSecurityGroup', () => {
|
|
115
|
-
it('should resolve to EXTERNAL with user-provided IDs', () => {
|
|
182
|
+
it('should resolve to EXTERNAL with user-provided hardcoded IDs', () => {
|
|
116
183
|
const appDefinition = {
|
|
117
184
|
vpc: {
|
|
118
185
|
ownership: { securityGroup: 'external' },
|
|
@@ -125,6 +192,71 @@ describe('VpcResourceResolver', () => {
|
|
|
125
192
|
|
|
126
193
|
expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
127
194
|
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');
|
|
128
260
|
});
|
|
129
261
|
|
|
130
262
|
it('should auto-resolve to STACK when FriggLambdaSecurityGroup in stack', () => {
|
|
@@ -146,7 +278,7 @@ describe('VpcResourceResolver', () => {
|
|
|
146
278
|
});
|
|
147
279
|
|
|
148
280
|
describe('resolveSubnets', () => {
|
|
149
|
-
it('should resolve to EXTERNAL with user-provided subnet IDs', () => {
|
|
281
|
+
it('should resolve to EXTERNAL with user-provided hardcoded subnet IDs', () => {
|
|
150
282
|
const appDefinition = {
|
|
151
283
|
vpc: {
|
|
152
284
|
ownership: { subnets: 'external' },
|
|
@@ -159,6 +291,48 @@ describe('VpcResourceResolver', () => {
|
|
|
159
291
|
|
|
160
292
|
expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
161
293
|
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
|
+
);
|
|
162
336
|
});
|
|
163
337
|
|
|
164
338
|
it('should resolve to STACK when subnets found in stack', () => {
|
|
@@ -227,7 +401,7 @@ describe('VpcResourceResolver', () => {
|
|
|
227
401
|
expect(decision.reason).toContain('NAT Gateway disabled');
|
|
228
402
|
});
|
|
229
403
|
|
|
230
|
-
it('should resolve to EXTERNAL with user-provided ID', () => {
|
|
404
|
+
it('should resolve to EXTERNAL with user-provided hardcoded ID', () => {
|
|
231
405
|
const appDefinition = {
|
|
232
406
|
vpc: {
|
|
233
407
|
ownership: { natGateway: 'external' },
|
|
@@ -240,6 +414,47 @@ describe('VpcResourceResolver', () => {
|
|
|
240
414
|
|
|
241
415
|
expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
242
416
|
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
|
+
);
|
|
243
458
|
});
|
|
244
459
|
|
|
245
460
|
it('should auto-resolve to STACK when found in stack', () => {
|
|
@@ -260,6 +475,82 @@ describe('VpcResourceResolver', () => {
|
|
|
260
475
|
});
|
|
261
476
|
|
|
262
477
|
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
|
+
|
|
263
554
|
it('should return null decisions when endpoints disabled', () => {
|
|
264
555
|
const appDefinition = {
|
|
265
556
|
vpc: {
|
|
@@ -288,7 +579,8 @@ describe('VpcResourceResolver', () => {
|
|
|
288
579
|
dynamodb: 'vpce-ddb-456'
|
|
289
580
|
}
|
|
290
581
|
}
|
|
291
|
-
}
|
|
582
|
+
},
|
|
583
|
+
database: { dynamodb: { enable: true } } // Enable DynamoDB
|
|
292
584
|
};
|
|
293
585
|
const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
|
|
294
586
|
|
|
@@ -302,7 +594,10 @@ describe('VpcResourceResolver', () => {
|
|
|
302
594
|
});
|
|
303
595
|
|
|
304
596
|
it('should auto-resolve to STACK when endpoints found in stack', () => {
|
|
305
|
-
const appDefinition = {
|
|
597
|
+
const appDefinition = {
|
|
598
|
+
vpc: { ownership: { vpcEndpoints: 'auto' } },
|
|
599
|
+
database: { dynamodb: { enable: true } } // Enable DynamoDB
|
|
600
|
+
};
|
|
306
601
|
const discovery = {
|
|
307
602
|
stackManaged: [
|
|
308
603
|
{ logicalId: 'FriggS3VPCEndpoint', physicalId: 'vpce-s3-stack', resourceType: 'AWS::EC2::VPCEndpoint' },
|
|
@@ -323,7 +618,8 @@ describe('VpcResourceResolver', () => {
|
|
|
323
618
|
it('should auto-resolve mixed: some in stack, some new', () => {
|
|
324
619
|
const appDefinition = {
|
|
325
620
|
vpc: { ownership: { vpcEndpoints: 'auto' } },
|
|
326
|
-
encryption: { fieldLevelEncryptionMethod: 'kms' } // Enable KMS endpoint
|
|
621
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' }, // Enable KMS endpoint
|
|
622
|
+
database: { dynamodb: { enable: true } } // Enable DynamoDB
|
|
327
623
|
};
|
|
328
624
|
const discovery = {
|
|
329
625
|
stackManaged: [
|
|
@@ -420,7 +716,11 @@ describe('VpcResourceResolver', () => {
|
|
|
420
716
|
describe('real-world scenarios', () => {
|
|
421
717
|
it('scenario: fresh deploy, no resources exist', () => {
|
|
422
718
|
const appDefinition = {
|
|
423
|
-
vpc: {
|
|
719
|
+
vpc: {
|
|
720
|
+
enable: true,
|
|
721
|
+
ownership: {},
|
|
722
|
+
management: 'create-new' // Explicitly allow VPC creation
|
|
723
|
+
}
|
|
424
724
|
};
|
|
425
725
|
const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
|
|
426
726
|
|
|
@@ -78,8 +78,20 @@ 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
|
+
|
|
81
89
|
console.log('\n 📋 Resource Ownership Decisions:');
|
|
82
|
-
|
|
90
|
+
if (willUseExternal) {
|
|
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
|
+
}
|
|
83
95
|
|
|
84
96
|
// Build resources based on ownership decisions
|
|
85
97
|
await this.buildFromDecisions(decisions, appDefinition, discoveredResources, result);
|
|
@@ -241,13 +253,30 @@ class KmsBuilder extends InfrastructureBuilder {
|
|
|
241
253
|
if (decisions.key.ownership === ResourceOwnership.STACK && decisions.key.physicalId) {
|
|
242
254
|
// Key exists in stack - add definitions (CloudFormation idempotency)
|
|
243
255
|
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
|
+
|
|
244
274
|
result.resources = this.createKmsKey(appDefinition);
|
|
245
275
|
result.environment.KMS_KEY_ARN = { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] };
|
|
246
276
|
console.log(' ✅ KMS key resources created');
|
|
247
277
|
} else if (decisions.key.ownership === ResourceOwnership.STACK && !decisions.key.physicalId && externalKmsKey) {
|
|
248
278
|
// ORPHANED KEY FIX: Key exists externally but not in stack
|
|
249
279
|
// 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');
|
|
251
280
|
console.log(` → Using external KMS key: ${externalKmsKey}`);
|
|
252
281
|
|
|
253
282
|
// Format as ARN if it's just a key ID
|
|
@@ -259,6 +288,17 @@ class KmsBuilder extends InfrastructureBuilder {
|
|
|
259
288
|
} else if (decisions.key.ownership === ResourceOwnership.STACK && !decisions.key.physicalId && !useEnvVarFallback) {
|
|
260
289
|
// Create new KMS key (only if not using env var fallback and no external key found)
|
|
261
290
|
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
|
+
|
|
262
302
|
result.resources = this.createKmsKey(appDefinition);
|
|
263
303
|
result.environment.KMS_KEY_ARN = { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] };
|
|
264
304
|
console.log(' ✅ KMS key resources created');
|
|
@@ -296,7 +336,7 @@ class KmsBuilder extends InfrastructureBuilder {
|
|
|
296
336
|
* Create KMS key CloudFormation resources
|
|
297
337
|
*/
|
|
298
338
|
createKmsKey(appDefinition) {
|
|
299
|
-
|
|
339
|
+
const resources = {
|
|
300
340
|
FriggKMSKey: {
|
|
301
341
|
Type: 'AWS::KMS::Key',
|
|
302
342
|
DeletionPolicy: 'Retain',
|
|
@@ -350,15 +390,24 @@ class KmsBuilder extends InfrastructureBuilder {
|
|
|
350
390
|
],
|
|
351
391
|
},
|
|
352
392
|
},
|
|
353
|
-
|
|
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 = {
|
|
354
399
|
Type: 'AWS::KMS::Alias',
|
|
355
400
|
DeletionPolicy: 'Retain',
|
|
356
401
|
Properties: {
|
|
357
402
|
AliasName: 'alias/${self:service}-${self:provider.stage}-frigg-kms',
|
|
358
403
|
TargetKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] },
|
|
359
404
|
},
|
|
360
|
-
}
|
|
361
|
-
}
|
|
405
|
+
};
|
|
406
|
+
} else {
|
|
407
|
+
console.log(' ℹ Skipping KMS key alias creation (kmsKeyAlias: false)');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return resources;
|
|
362
411
|
}
|
|
363
412
|
}
|
|
364
413
|
|
|
@@ -209,11 +209,12 @@ describe('KmsBuilder', () => {
|
|
|
209
209
|
expect(result.resources.FriggKMSKey.Type).toBe('AWS::KMS::Key');
|
|
210
210
|
});
|
|
211
211
|
|
|
212
|
-
it('should create KMS key alias', async () => {
|
|
212
|
+
it('should create KMS key alias when explicitly enabled', async () => {
|
|
213
213
|
const appDefinition = {
|
|
214
214
|
encryption: {
|
|
215
215
|
fieldLevelEncryptionMethod: 'kms',
|
|
216
216
|
createResourceIfNoneFound: true,
|
|
217
|
+
kmsKeyAlias: true, // Explicitly enable alias creation
|
|
217
218
|
},
|
|
218
219
|
};
|
|
219
220
|
|
|
@@ -225,6 +226,23 @@ describe('KmsBuilder', () => {
|
|
|
225
226
|
expect(result.resources.FriggKMSKeyAlias.Type).toBe('AWS::KMS::Alias');
|
|
226
227
|
});
|
|
227
228
|
|
|
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
|
+
|
|
228
246
|
it('should enable key rotation for new keys', async () => {
|
|
229
247
|
const appDefinition = {
|
|
230
248
|
encryption: {
|