@friggframework/devtools 2.0.0--canary.461.5fd8136.0 → 2.0.0--canary.461.9de6fdd.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.
|
@@ -277,7 +277,7 @@ class AuroraBuilder extends InfrastructureBuilder {
|
|
|
277
277
|
// Check if we should auto-create credentials
|
|
278
278
|
if (dbConfig.autoCreateCredentials && !discoveredResources.databaseSecretArn) {
|
|
279
279
|
console.log(' Creating Secrets Manager secret and rotating Aurora password...');
|
|
280
|
-
|
|
280
|
+
|
|
281
281
|
// Create Secrets Manager secret with auto-generated password
|
|
282
282
|
result.resources.FriggDBSecret = {
|
|
283
283
|
Type: 'AWS::SecretsManager::Secret',
|
|
@@ -470,31 +470,20 @@ exports.handler = async (event, context) => {
|
|
|
470
470
|
|
|
471
471
|
console.log(' ✅ Using discovered Secrets Manager credentials');
|
|
472
472
|
} else {
|
|
473
|
-
// No secret and no auto-create -
|
|
473
|
+
// No secret and no auto-create - set individual DB connection components
|
|
474
|
+
// The application will construct DATABASE_URL at runtime from these components + DATABASE_USER + DATABASE_PASSWORD
|
|
474
475
|
const dbName = dbConfig.database || 'frigg';
|
|
475
|
-
|
|
476
|
-
// Set individual environment variables for flexible credential management
|
|
476
|
+
|
|
477
477
|
result.environment.DATABASE_HOST = discoveredResources.auroraClusterEndpoint;
|
|
478
478
|
result.environment.DATABASE_PORT = String(discoveredResources.auroraPort || 5432);
|
|
479
479
|
result.environment.DATABASE_NAME = dbName;
|
|
480
|
-
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
DatabaseUser: '${env:DATABASE_USER, "postgres"}',
|
|
488
|
-
DatabasePassword: '${env:DATABASE_PASSWORD}',
|
|
489
|
-
DatabaseHost: discoveredResources.auroraClusterEndpoint,
|
|
490
|
-
DatabasePort: String(discoveredResources.auroraPort || 5432),
|
|
491
|
-
DatabaseName: dbName,
|
|
492
|
-
},
|
|
493
|
-
],
|
|
494
|
-
};
|
|
495
|
-
|
|
496
|
-
console.log(' ℹ️ No Secrets Manager secret found - DATABASE_URL will use DATABASE_USER and DATABASE_PASSWORD from environment');
|
|
497
|
-
console.log(' ℹ️ Set DATABASE_USER and DATABASE_PASSWORD in Lambda environment or via serverless deploy --param');
|
|
480
|
+
|
|
481
|
+
// Note: DATABASE_URL is NOT set here to avoid Serverless variable resolution errors
|
|
482
|
+
// The application (Frigg Core) should construct it at runtime from:
|
|
483
|
+
// DATABASE_HOST, DATABASE_PORT, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD
|
|
484
|
+
|
|
485
|
+
console.log(' ℹ️ No Secrets Manager secret found - set DATABASE_USER and DATABASE_PASSWORD in Lambda environment');
|
|
486
|
+
console.log(' ℹ️ Application will construct DATABASE_URL at runtime from DATABASE_HOST, DATABASE_PORT, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD');
|
|
498
487
|
console.log(' ℹ️ Or enable autoCreateCredentials=true to automatically create and rotate credentials');
|
|
499
488
|
}
|
|
500
489
|
|
|
@@ -519,14 +508,47 @@ exports.handler = async (event, context) => {
|
|
|
519
508
|
|
|
520
509
|
/**
|
|
521
510
|
* Build DATABASE_URL connection string
|
|
511
|
+
* @param {string|object} host - Database host (string or CloudFormation intrinsic function)
|
|
512
|
+
* @param {string|number|object} port - Database port (string/number or CloudFormation intrinsic function)
|
|
513
|
+
* @param {string} database - Database name
|
|
514
|
+
* @param {string|object} secretRef - Secret ARN (string) or CloudFormation Ref object
|
|
522
515
|
*/
|
|
523
516
|
buildDatabaseUrl(host, port, database, secretRef) {
|
|
517
|
+
// Handle secretRef as either a string ARN or CloudFormation Ref object
|
|
518
|
+
const resolveSecretRef = (secretRefValue) => {
|
|
519
|
+
if (typeof secretRefValue === 'object' && secretRefValue.Ref) {
|
|
520
|
+
// CloudFormation Ref - use nested Fn::Sub to resolve it
|
|
521
|
+
return {
|
|
522
|
+
'Fn::Sub': [
|
|
523
|
+
'{{resolve:secretsmanager:${SecretArn}:SecretString:username}}',
|
|
524
|
+
{ SecretArn: secretRefValue },
|
|
525
|
+
],
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
// String ARN - use directly
|
|
529
|
+
return `{{resolve:secretsmanager:${secretRefValue}:SecretString:username}}`;
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const resolveSecretPassword = (secretRefValue) => {
|
|
533
|
+
if (typeof secretRefValue === 'object' && secretRefValue.Ref) {
|
|
534
|
+
// CloudFormation Ref - use nested Fn::Sub to resolve it
|
|
535
|
+
return {
|
|
536
|
+
'Fn::Sub': [
|
|
537
|
+
'{{resolve:secretsmanager:${SecretArn}:SecretString:password}}',
|
|
538
|
+
{ SecretArn: secretRefValue },
|
|
539
|
+
],
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
// String ARN - use directly
|
|
543
|
+
return `{{resolve:secretsmanager:${secretRefValue}:SecretString:password}}`;
|
|
544
|
+
};
|
|
545
|
+
|
|
524
546
|
return {
|
|
525
547
|
'Fn::Sub': [
|
|
526
548
|
`postgresql://\${Username}:\${Password}@\${Host}:\${Port}/\${Database}`,
|
|
527
549
|
{
|
|
528
|
-
Username:
|
|
529
|
-
Password:
|
|
550
|
+
Username: resolveSecretRef(secretRef),
|
|
551
|
+
Password: resolveSecretPassword(secretRef),
|
|
530
552
|
Host: host,
|
|
531
553
|
Port: port,
|
|
532
554
|
Database: database,
|
|
@@ -388,8 +388,14 @@ describe('AuroraBuilder', () => {
|
|
|
388
388
|
// Check DATABASE_URL uses the secret
|
|
389
389
|
expect(result.environment.DATABASE_URL).toBeDefined();
|
|
390
390
|
expect(result.environment.DATABASE_URL['Fn::Sub']).toBeDefined();
|
|
391
|
-
|
|
392
|
-
|
|
391
|
+
|
|
392
|
+
// Username and Password should use nested Fn::Sub to resolve the Ref
|
|
393
|
+
expect(result.environment.DATABASE_URL['Fn::Sub'][1].Username['Fn::Sub']).toBeDefined();
|
|
394
|
+
expect(result.environment.DATABASE_URL['Fn::Sub'][1].Password['Fn::Sub']).toBeDefined();
|
|
395
|
+
|
|
396
|
+
// Should contain secretsmanager resolution
|
|
397
|
+
expect(result.environment.DATABASE_URL['Fn::Sub'][1].Username['Fn::Sub'][0]).toContain('resolve:secretsmanager');
|
|
398
|
+
expect(result.environment.DATABASE_URL['Fn::Sub'][1].Password['Fn::Sub'][0]).toContain('resolve:secretsmanager');
|
|
393
399
|
|
|
394
400
|
// Check IAM permissions for secret access
|
|
395
401
|
const secretPermission = result.iamStatements.find(stmt =>
|
|
@@ -422,11 +428,17 @@ describe('AuroraBuilder', () => {
|
|
|
422
428
|
expect(result.resources.FriggAuroraPasswordRotator).toBeUndefined();
|
|
423
429
|
expect(result.resources.PasswordRotatorRole).toBeUndefined();
|
|
424
430
|
|
|
425
|
-
//
|
|
426
|
-
expect(result.environment.
|
|
427
|
-
expect(result.environment.
|
|
428
|
-
expect(result.environment.
|
|
429
|
-
|
|
431
|
+
// Should set individual environment variables for flexible credential management
|
|
432
|
+
expect(result.environment.DATABASE_HOST).toBe('cluster.abc.us-east-1.rds.amazonaws.com');
|
|
433
|
+
expect(result.environment.DATABASE_PORT).toBe('5432');
|
|
434
|
+
expect(result.environment.DATABASE_NAME).toBe('frigg');
|
|
435
|
+
|
|
436
|
+
// DATABASE_URL should NOT be set (to avoid Serverless variable resolution errors)
|
|
437
|
+
// The application should construct it at runtime from DATABASE_HOST, DATABASE_PORT, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD
|
|
438
|
+
expect(result.environment.DATABASE_URL).toBeUndefined();
|
|
439
|
+
|
|
440
|
+
// DATABASE_USER and DATABASE_PASSWORD should come from appDefinition.environment
|
|
441
|
+
// and will be set by the environment-builder, not here
|
|
430
442
|
});
|
|
431
443
|
|
|
432
444
|
it('should not create credentials when secret is already discovered', async () => {
|
|
@@ -517,15 +529,49 @@ describe('AuroraBuilder', () => {
|
|
|
517
529
|
const result = await auroraBuilder.build(appDefinition, discoveredResources);
|
|
518
530
|
|
|
519
531
|
const zipFileCode = result.resources.PasswordRotatorLambda.Properties.Code.ZipFile;
|
|
520
|
-
|
|
532
|
+
|
|
521
533
|
// Should not contain template literals that would conflict with CloudFormation ${} substitution
|
|
522
534
|
// CloudFormation uses ${} for parameter substitution, so we should avoid `${variable}` in ZipFile
|
|
523
535
|
expect(zipFileCode).not.toMatch(/`.*\$\{(?!env:).*\}`/); // No template literals with ${} except ${env:...}
|
|
524
|
-
|
|
536
|
+
|
|
525
537
|
// Should use string concatenation instead
|
|
526
538
|
expect(zipFileCode).toContain("'Successfully rotated password for cluster: ' + ClusterIdentifier");
|
|
527
539
|
});
|
|
528
540
|
|
|
541
|
+
it('should properly handle Ref objects in buildDatabaseUrl when autoCreateCredentials is enabled', async () => {
|
|
542
|
+
const appDefinition = {
|
|
543
|
+
database: {
|
|
544
|
+
postgres: {
|
|
545
|
+
enable: true,
|
|
546
|
+
management: 'discover',
|
|
547
|
+
autoCreateCredentials: true,
|
|
548
|
+
database: 'testdb',
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const discoveredResources = {
|
|
554
|
+
auroraClusterEndpoint: 'cluster.abc.us-east-1.rds.amazonaws.com',
|
|
555
|
+
auroraPort: 5432,
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const result = await auroraBuilder.build(appDefinition, discoveredResources);
|
|
559
|
+
|
|
560
|
+
const dbUrl = result.environment.DATABASE_URL;
|
|
561
|
+
|
|
562
|
+
// Should use Fn::Sub with nested Fn::Sub to resolve the Ref
|
|
563
|
+
expect(dbUrl['Fn::Sub']).toBeDefined();
|
|
564
|
+
expect(dbUrl['Fn::Sub'][0]).toBe('postgresql://${Username}:${Password}@${Host}:${Port}/${Database}');
|
|
565
|
+
|
|
566
|
+
// The Username and Password should use Fn::Sub to resolve the secret Ref, not literal "[object Object]"
|
|
567
|
+
expect(dbUrl['Fn::Sub'][1].Username['Fn::Sub']).toBeDefined();
|
|
568
|
+
expect(dbUrl['Fn::Sub'][1].Password['Fn::Sub']).toBeDefined();
|
|
569
|
+
|
|
570
|
+
// Should not contain the literal string "[object Object]"
|
|
571
|
+
const jsonOutput = JSON.stringify(dbUrl);
|
|
572
|
+
expect(jsonOutput).not.toContain('[object Object]');
|
|
573
|
+
});
|
|
574
|
+
|
|
529
575
|
it('should properly escape ExcludeCharacters for valid JSON in CloudFormation template', async () => {
|
|
530
576
|
const appDefinition = {
|
|
531
577
|
database: {
|
|
@@ -545,15 +591,15 @@ describe('AuroraBuilder', () => {
|
|
|
545
591
|
const result = await auroraBuilder.build(appDefinition, discoveredResources);
|
|
546
592
|
|
|
547
593
|
const excludeChars = result.resources.FriggDBSecret.Properties.GenerateSecretString.ExcludeCharacters;
|
|
548
|
-
|
|
594
|
+
|
|
549
595
|
// Should properly escape the backslash so it's valid JSON
|
|
550
596
|
// In JavaScript string: '"@/\\' represents the string: "@/\
|
|
551
597
|
// When serialized to JSON, backslash must be doubled: '"@/\\'
|
|
552
598
|
expect(excludeChars).toBe('"@/\\\\');
|
|
553
|
-
|
|
599
|
+
|
|
554
600
|
// Verify it can be JSON-stringified without errors
|
|
555
601
|
expect(() => JSON.stringify(result.resources.FriggDBSecret)).not.toThrow();
|
|
556
|
-
|
|
602
|
+
|
|
557
603
|
// Verify the JSON output has the correct escape sequence
|
|
558
604
|
const jsonOutput = JSON.stringify(result.resources.FriggDBSecret);
|
|
559
605
|
expect(jsonOutput).toContain('\\"@/\\\\\\\\'); // In JSON string: "\"@/\\\\"
|
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.9de6fdd.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.9de6fdd.0",
|
|
15
|
+
"@friggframework/test": "2.0.0--canary.461.9de6fdd.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.9de6fdd.0",
|
|
38
|
+
"@friggframework/prettier-config": "2.0.0--canary.461.9de6fdd.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": "9de6fddc9112148ee8c11b4c4873371c806a576c"
|
|
74
74
|
}
|