@friggframework/devtools 2.0.0--canary.461.4116d1e.0 → 2.0.0--canary.461.77c8d12.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 +1 -1
- package/infrastructure/domains/shared/cloudformation-discovery.js +53 -7
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +137 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.js +21 -1
- package/infrastructure/domains/shared/resource-discovery.js +2 -1
- package/package.json +6 -6
|
@@ -989,7 +989,7 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
989
989
|
}
|
|
990
990
|
|
|
991
991
|
// VPC Endpoint Security Group (only if KMS, Secrets Manager, or SQS are not stack-managed and missing)
|
|
992
|
-
const needsSecurityGroup =
|
|
992
|
+
const needsSecurityGroup =
|
|
993
993
|
(!stackManagedEndpoints.kms && !existingEndpoints.kms && appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') ||
|
|
994
994
|
(!stackManagedEndpoints.secretsManager && !existingEndpoints.secretsManager) ||
|
|
995
995
|
(!stackManagedEndpoints.sqs && !existingEndpoints.sqs);
|
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
class CloudFormationDiscovery {
|
|
17
|
-
constructor(provider) {
|
|
17
|
+
constructor(provider, config = {}) {
|
|
18
18
|
this.provider = provider;
|
|
19
|
+
this.serviceName = config.serviceName;
|
|
20
|
+
this.stage = config.stage;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -41,9 +43,8 @@ class CloudFormationDiscovery {
|
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
// Extract from resources (now async to query AWS for details)
|
|
44
|
-
if
|
|
45
|
-
|
|
46
|
-
}
|
|
46
|
+
// Always call this even if resources is empty, as it may query AWS for resources
|
|
47
|
+
await this._extractFromResources(resources || [], discovered);
|
|
47
48
|
|
|
48
49
|
return discovered;
|
|
49
50
|
} catch (error) {
|
|
@@ -200,6 +201,30 @@ class CloudFormationDiscovery {
|
|
|
200
201
|
}
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
// KMS Key Alias - query to get the actual key ARN
|
|
205
|
+
if (LogicalResourceId === 'FriggKMSKeyAlias' && ResourceType === 'AWS::KMS::Alias') {
|
|
206
|
+
discovered.kmsKeyAlias = PhysicalResourceId;
|
|
207
|
+
console.log(` ✓ Found KMS key alias in stack: ${PhysicalResourceId}`);
|
|
208
|
+
|
|
209
|
+
// Query KMS to get the key ARN that this alias points to
|
|
210
|
+
// Always query even if key is already set, to ensure consistency
|
|
211
|
+
if (this.provider && this.provider.describeKmsKey) {
|
|
212
|
+
try {
|
|
213
|
+
console.log(` Querying KMS to get key ARN from alias...`);
|
|
214
|
+
const keyMetadata = await this.provider.describeKmsKey(PhysicalResourceId);
|
|
215
|
+
|
|
216
|
+
if (keyMetadata) {
|
|
217
|
+
discovered.defaultKmsKeyId = keyMetadata.Arn;
|
|
218
|
+
console.log(` ✓ Extracted KMS key ARN from alias: ${discovered.defaultKmsKeyId}`);
|
|
219
|
+
} else {
|
|
220
|
+
console.warn(` ⚠️ KMS key query returned no metadata`);
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.warn(` ⚠️ Could not get key ARN from alias: ${error.message}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
203
228
|
// Subnets
|
|
204
229
|
if (LogicalResourceId === 'FriggPrivateSubnet1' && ResourceType === 'AWS::EC2::Subnet') {
|
|
205
230
|
discovered.privateSubnetId1 = PhysicalResourceId;
|
|
@@ -243,7 +268,7 @@ class CloudFormationDiscovery {
|
|
|
243
268
|
}
|
|
244
269
|
|
|
245
270
|
// If we have a VPC ID but no subnet IDs, query EC2 for Frigg-managed subnets
|
|
246
|
-
if (discovered.defaultVpcId && this.provider &&
|
|
271
|
+
if (discovered.defaultVpcId && this.provider &&
|
|
247
272
|
!discovered.privateSubnetId1 && !discovered.publicSubnetId1) {
|
|
248
273
|
try {
|
|
249
274
|
console.log(' Querying EC2 for Frigg-managed subnets...');
|
|
@@ -266,7 +291,7 @@ class CloudFormationDiscovery {
|
|
|
266
291
|
}));
|
|
267
292
|
|
|
268
293
|
// Find private subnets
|
|
269
|
-
const privateSubnets = subnets.filter(s => !s.isPublic).sort((a, b) =>
|
|
294
|
+
const privateSubnets = subnets.filter(s => !s.isPublic).sort((a, b) =>
|
|
270
295
|
a.logicalId?.localeCompare(b.logicalId) || 0
|
|
271
296
|
);
|
|
272
297
|
if (privateSubnets.length >= 1) {
|
|
@@ -277,7 +302,7 @@ class CloudFormationDiscovery {
|
|
|
277
302
|
}
|
|
278
303
|
|
|
279
304
|
// Find public subnets
|
|
280
|
-
const publicSubnets = subnets.filter(s => s.isPublic).sort((a, b) =>
|
|
305
|
+
const publicSubnets = subnets.filter(s => s.isPublic).sort((a, b) =>
|
|
281
306
|
a.logicalId?.localeCompare(b.logicalId) || 0
|
|
282
307
|
);
|
|
283
308
|
if (publicSubnets.length >= 1) {
|
|
@@ -293,6 +318,27 @@ class CloudFormationDiscovery {
|
|
|
293
318
|
console.warn(` ⚠️ Could not query EC2 for subnets: ${error.message}`);
|
|
294
319
|
}
|
|
295
320
|
}
|
|
321
|
+
|
|
322
|
+
// Check for KMS key alias via AWS API if not found in stack resources
|
|
323
|
+
// This handles cases where the alias was created outside CloudFormation
|
|
324
|
+
if (!discovered.defaultKmsKeyId && !discovered.kmsKeyAlias &&
|
|
325
|
+
this.provider && this.provider.describeKmsKey && this.serviceName && this.stage) {
|
|
326
|
+
try {
|
|
327
|
+
const aliasName = `alias/${this.serviceName}-${this.stage}-frigg-kms`;
|
|
328
|
+
console.log(` Querying KMS for alias: ${aliasName}...`);
|
|
329
|
+
|
|
330
|
+
const keyMetadata = await this.provider.describeKmsKey(aliasName);
|
|
331
|
+
|
|
332
|
+
if (keyMetadata) {
|
|
333
|
+
discovered.defaultKmsKeyId = keyMetadata.Arn;
|
|
334
|
+
discovered.kmsKeyAlias = aliasName;
|
|
335
|
+
console.log(` ✓ Found KMS key via alias query: ${discovered.defaultKmsKeyId}`);
|
|
336
|
+
}
|
|
337
|
+
} catch (error) {
|
|
338
|
+
// Alias not found - this is expected if no KMS key exists yet
|
|
339
|
+
console.log(` ℹ No KMS key alias found via AWS API`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
296
342
|
}
|
|
297
343
|
}
|
|
298
344
|
|
|
@@ -399,6 +399,143 @@ describe('CloudFormationDiscovery', () => {
|
|
|
399
399
|
expect(result.defaultVpcId).toBe('vpc-123');
|
|
400
400
|
expect(result.privateSubnetId1).toBeUndefined();
|
|
401
401
|
});
|
|
402
|
+
|
|
403
|
+
it('should extract KMS key alias from stack resources and query for key ARN', async () => {
|
|
404
|
+
const mockStack = {
|
|
405
|
+
StackName: 'test-stack',
|
|
406
|
+
Outputs: [],
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const mockResources = [
|
|
410
|
+
{
|
|
411
|
+
LogicalResourceId: 'FriggKMSKeyAlias',
|
|
412
|
+
PhysicalResourceId: 'alias/test-service-dev-frigg-kms',
|
|
413
|
+
ResourceType: 'AWS::KMS::Alias',
|
|
414
|
+
},
|
|
415
|
+
];
|
|
416
|
+
|
|
417
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
418
|
+
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
419
|
+
mockProvider.describeKmsKey = jest.fn().mockResolvedValue({
|
|
420
|
+
KeyId: 'abc-123',
|
|
421
|
+
Arn: 'arn:aws:kms:us-east-1:123456789:key/abc-123',
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
425
|
+
|
|
426
|
+
expect(result.defaultKmsKeyId).toBe('arn:aws:kms:us-east-1:123456789:key/abc-123');
|
|
427
|
+
expect(result.kmsKeyAlias).toBe('alias/test-service-dev-frigg-kms');
|
|
428
|
+
expect(mockProvider.describeKmsKey).toHaveBeenCalledWith('alias/test-service-dev-frigg-kms');
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('should query AWS API for KMS alias when serviceName and stage are provided', async () => {
|
|
432
|
+
const mockStack = {
|
|
433
|
+
StackName: 'test-stack',
|
|
434
|
+
Outputs: [],
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const mockResources = [];
|
|
438
|
+
|
|
439
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
440
|
+
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
441
|
+
mockProvider.region = 'us-east-1';
|
|
442
|
+
mockProvider.describeKmsKey = jest.fn().mockResolvedValue({
|
|
443
|
+
KeyId: 'abc-123',
|
|
444
|
+
Arn: 'arn:aws:kms:us-east-1:123456789:key/abc-123',
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Pass serviceName and stage to discover alias
|
|
448
|
+
cfDiscovery.serviceName = 'test-service';
|
|
449
|
+
cfDiscovery.stage = 'dev';
|
|
450
|
+
|
|
451
|
+
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
452
|
+
|
|
453
|
+
expect(result.defaultKmsKeyId).toBe('arn:aws:kms:us-east-1:123456789:key/abc-123');
|
|
454
|
+
expect(result.kmsKeyAlias).toBe('alias/test-service-dev-frigg-kms');
|
|
455
|
+
expect(mockProvider.describeKmsKey).toHaveBeenCalledWith('alias/test-service-dev-frigg-kms');
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('should handle KMS alias not found gracefully', async () => {
|
|
459
|
+
const mockStack = {
|
|
460
|
+
StackName: 'test-stack',
|
|
461
|
+
Outputs: [],
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const mockResources = [];
|
|
465
|
+
|
|
466
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
467
|
+
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
468
|
+
mockProvider.region = 'us-east-1';
|
|
469
|
+
mockProvider.describeKmsKey = jest.fn().mockRejectedValue(
|
|
470
|
+
new Error('Alias/test-service-dev-frigg-kms is not found')
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
cfDiscovery.serviceName = 'test-service';
|
|
474
|
+
cfDiscovery.stage = 'dev';
|
|
475
|
+
|
|
476
|
+
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
477
|
+
|
|
478
|
+
expect(result.defaultKmsKeyId).toBeUndefined();
|
|
479
|
+
expect(result.kmsKeyAlias).toBeUndefined();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('should prefer KMS key from stack resources over alias query', async () => {
|
|
483
|
+
const mockStack = {
|
|
484
|
+
StackName: 'test-stack',
|
|
485
|
+
Outputs: [],
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const mockResources = [
|
|
489
|
+
{
|
|
490
|
+
LogicalResourceId: 'FriggKMSKey',
|
|
491
|
+
PhysicalResourceId: 'arn:aws:kms:us-east-1:123456789:key/xyz-789',
|
|
492
|
+
ResourceType: 'AWS::KMS::Key',
|
|
493
|
+
},
|
|
494
|
+
];
|
|
495
|
+
|
|
496
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
497
|
+
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
498
|
+
mockProvider.describeKmsKey = jest.fn();
|
|
499
|
+
|
|
500
|
+
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
501
|
+
|
|
502
|
+
// Should use the key from stack resources, not query for alias
|
|
503
|
+
expect(result.defaultKmsKeyId).toBe('arn:aws:kms:us-east-1:123456789:key/xyz-789');
|
|
504
|
+
expect(mockProvider.describeKmsKey).not.toHaveBeenCalled();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('should use KMS alias from stack resources even if key is also present', async () => {
|
|
508
|
+
const mockStack = {
|
|
509
|
+
StackName: 'test-stack',
|
|
510
|
+
Outputs: [],
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const mockResources = [
|
|
514
|
+
{
|
|
515
|
+
LogicalResourceId: 'FriggKMSKey',
|
|
516
|
+
PhysicalResourceId: 'arn:aws:kms:us-east-1:123456789:key/xyz-789',
|
|
517
|
+
ResourceType: 'AWS::KMS::Key',
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
LogicalResourceId: 'FriggKMSKeyAlias',
|
|
521
|
+
PhysicalResourceId: 'alias/test-service-dev-frigg-kms',
|
|
522
|
+
ResourceType: 'AWS::KMS::Alias',
|
|
523
|
+
},
|
|
524
|
+
];
|
|
525
|
+
|
|
526
|
+
mockProvider.describeStack.mockResolvedValue(mockStack);
|
|
527
|
+
mockProvider.listStackResources.mockResolvedValue(mockResources);
|
|
528
|
+
mockProvider.describeKmsKey = jest.fn().mockResolvedValue({
|
|
529
|
+
KeyId: 'xyz-789',
|
|
530
|
+
Arn: 'arn:aws:kms:us-east-1:123456789:key/xyz-789',
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
534
|
+
|
|
535
|
+
expect(result.defaultKmsKeyId).toBe('arn:aws:kms:us-east-1:123456789:key/xyz-789');
|
|
536
|
+
expect(result.kmsKeyAlias).toBe('alias/test-service-dev-frigg-kms');
|
|
537
|
+
expect(mockProvider.describeKmsKey).toHaveBeenCalledWith('alias/test-service-dev-frigg-kms');
|
|
538
|
+
});
|
|
402
539
|
});
|
|
403
540
|
});
|
|
404
541
|
|
|
@@ -469,9 +469,29 @@ class AWSProviderAdapter extends CloudProviderAdapter {
|
|
|
469
469
|
return result;
|
|
470
470
|
}
|
|
471
471
|
|
|
472
|
+
/**
|
|
473
|
+
* Describe KMS key by key ID or alias
|
|
474
|
+
*
|
|
475
|
+
* @param {string} keyIdOrAlias - Key ID or alias name
|
|
476
|
+
* @returns {Promise<Object>} Key metadata
|
|
477
|
+
*/
|
|
478
|
+
async describeKmsKey(keyIdOrAlias) {
|
|
479
|
+
const kms = this.getKMSClient();
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
const response = await kms.send(new DescribeKeyCommand({
|
|
483
|
+
KeyId: keyIdOrAlias,
|
|
484
|
+
}));
|
|
485
|
+
|
|
486
|
+
return response.KeyMetadata;
|
|
487
|
+
} catch (error) {
|
|
488
|
+
throw new Error(`Failed to describe KMS key ${keyIdOrAlias}: ${error.message}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
472
492
|
/**
|
|
473
493
|
* Describe CloudFormation stack
|
|
474
|
-
*
|
|
494
|
+
*
|
|
475
495
|
* @param {string} stackName - Name of the CloudFormation stack
|
|
476
496
|
* @returns {Promise<Object>} Stack details including outputs
|
|
477
497
|
*/
|
|
@@ -73,9 +73,10 @@ async function gatherDiscoveredResources(appDefinition) {
|
|
|
73
73
|
// Build discovery configuration
|
|
74
74
|
const stage = process.env.SLS_STAGE || 'dev';
|
|
75
75
|
const stackName = `${appDefinition.name || 'create-frigg-app'}-${stage}`;
|
|
76
|
+
const serviceName = appDefinition.name || 'create-frigg-app';
|
|
76
77
|
|
|
77
78
|
// Try CloudFormation-first discovery
|
|
78
|
-
const cfDiscovery = new CloudFormationDiscovery(provider);
|
|
79
|
+
const cfDiscovery = new CloudFormationDiscovery(provider, { serviceName, stage });
|
|
79
80
|
const stackResources = await cfDiscovery.discoverFromStack(stackName);
|
|
80
81
|
|
|
81
82
|
// Validate CF discovery results - only use if contains useful data
|
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.461.
|
|
4
|
+
"version": "2.0.0--canary.461.77c8d12.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-ec2": "^3.835.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.835.0",
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"@babel/eslint-parser": "^7.18.9",
|
|
12
12
|
"@babel/parser": "^7.25.3",
|
|
13
13
|
"@babel/traverse": "^7.25.3",
|
|
14
|
-
"@friggframework/schemas": "2.0.0--canary.461.
|
|
15
|
-
"@friggframework/test": "2.0.0--canary.461.
|
|
14
|
+
"@friggframework/schemas": "2.0.0--canary.461.77c8d12.0",
|
|
15
|
+
"@friggframework/test": "2.0.0--canary.461.77c8d12.0",
|
|
16
16
|
"@hapi/boom": "^10.0.1",
|
|
17
17
|
"@inquirer/prompts": "^5.3.8",
|
|
18
18
|
"axios": "^1.7.2",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"serverless-http": "^2.7.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@friggframework/eslint-config": "2.0.0--canary.461.
|
|
38
|
-
"@friggframework/prettier-config": "2.0.0--canary.461.
|
|
37
|
+
"@friggframework/eslint-config": "2.0.0--canary.461.77c8d12.0",
|
|
38
|
+
"@friggframework/prettier-config": "2.0.0--canary.461.77c8d12.0",
|
|
39
39
|
"aws-sdk-client-mock": "^4.1.0",
|
|
40
40
|
"aws-sdk-client-mock-jest": "^4.1.0",
|
|
41
41
|
"jest": "^30.1.3",
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
"publishConfig": {
|
|
71
71
|
"access": "public"
|
|
72
72
|
},
|
|
73
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "77c8d12f6a33bb2d66e19b43b6d96ee27d6f5e06"
|
|
74
74
|
}
|