RdsFactory.java
package com.cloudforgeci.api.database;
import com.cloudforgeci.api.core.SystemContext;
import com.cloudforgeci.api.core.rules.AwsConfigRule;
import com.cloudforge.core.interfaces.DatabaseSpec;
import com.cloudforge.core.interfaces.DatabaseSpec.DatabaseRequirement;
import com.cloudforge.core.interfaces.DatabaseSpec.DatabaseConnection;
import com.cloudforge.core.enums.SecurityProfile;
import software.amazon.awscdk.Duration;
import software.amazon.awscdk.RemovalPolicy;
import software.amazon.awscdk.services.ec2.IVpc;
import software.amazon.awscdk.services.ec2.Peer;
import software.amazon.awscdk.services.ec2.Port;
import software.amazon.awscdk.services.ec2.SecurityGroup;
import software.amazon.awscdk.services.ec2.SubnetSelection;
import software.amazon.awscdk.services.ec2.SubnetType;
import software.amazon.awscdk.services.ec2.InstanceClass;
import software.amazon.awscdk.services.ec2.InstanceSize;
import software.amazon.awscdk.services.ec2.InstanceType;
import software.amazon.awscdk.services.kms.IKey;
import software.amazon.awscdk.services.kms.Key;
import software.amazon.awscdk.services.logs.RetentionDays;
import software.amazon.awscdk.services.rds.*;
import software.amazon.awscdk.services.secretsmanager.*;
import software.constructs.Construct;
import io.github.cdklabs.cdknag.NagPackSuppression;
import io.github.cdklabs.cdknag.NagSuppressions;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Factory for provisioning AWS RDS database instances based on DatabaseSpec requirements.
*
* <p>This factory creates production-ready RDS instances with security best practices
* for PCI-DSS, HIPAA, SOC 2, and GDPR compliance.</p>
*
* <h2>Compliance Coverage</h2>
* <ul>
* <li><b>SOC2-C1.1:</b> Encryption at rest (storageEncrypted)</li>
* <li><b>SOC2-CC6.6:</b> Network isolation (privateSubnets, publiclyAccessible=false)</li>
* <li><b>SOC2-A1.2-MultiAZ:</b> High availability (multiAz)</li>
* <li><b>SOC2-A1.3:</b> Automated backups (backupRetention)</li>
* <li><b>HIPAA §164.312(a)(2)(iv):</b> Encryption of ePHI at rest</li>
* <li><b>HIPAA §164.312(a)(1):</b> Access control (no public access)</li>
* <li><b>HIPAA §164.310(d)(2)(iii):</b> Data backup procedures</li>
* <li><b>PCI-DSS Req 1.3:</b> Prohibit direct public access to cardholder data</li>
* <li><b>PCI-DSS Req 3.4:</b> Render cardholder data unreadable (encryption)</li>
* <li><b>PCI-DSS Req 8.3.1:</b> IAM authentication for database access</li>
* <li><b>GDPR Art. 32:</b> Security of processing (encryption, access control)</li>
* </ul>
*
* <h2>Security Features</h2>
* <ul>
* <li><b>Encryption at Rest:</b> KMS encryption for production/staging</li>
* <li><b>Automated Backups:</b> Configurable retention (7-30 days)</li>
* <li><b>Multi-AZ Deployment:</b> High availability for production</li>
* <li><b>Secrets Manager:</b> Automatic credential rotation</li>
* <li><b>Private Subnets:</b> No public accessibility</li>
* <li><b>Deletion Protection:</b> Enabled for production</li>
* <li><b>Automatic Patching:</b> Minor version upgrades for production</li>
* <li><b>Enhanced Monitoring:</b> Real-time OS metrics for production</li>
* <li><b>Performance Insights:</b> Query performance monitoring</li>
* </ul>
*
* <h2>Supported Engines</h2>
* <ul>
* <li>PostgreSQL 11, 12, 13, 14, 15, 16</li>
* <li>MySQL 5.7, 8.0</li>
* <li>MariaDB 10.6, 10.11</li>
* <li>Aurora PostgreSQL</li>
* <li>Aurora MySQL</li>
* </ul>
*
* <h2>Usage Example</h2>
* <pre>{@code
* DatabaseRequirement req = DatabaseRequirement.required("postgres", "15")
* .withInstanceClass("db.t3.medium")
* .withStorage(100)
* .withDatabaseName("myapp");
*
* DatabaseConnection conn = RdsFactory.createDatabase(ctx, req, vpc, "myapp-db");
*
* // Use connection in application
* Map<String, String> env = appSpec.containerEnvironmentVariables(fqdn, true, "oidc", conn);
* }</pre>
*
* @see DatabaseSpec
* @see DatabaseRequirement
* @see DatabaseConnection
* @since 3.0.0
*/
public class RdsFactory {
/**
* Create RDS database instance from DatabaseSpec requirement.
*
* <p>This method provisions a fully-configured RDS instance with security
* settings appropriate for the deployment security profile.</p>
*
* @param ctx System context with security profile and deployment settings
* @param requirement Database requirements from ApplicationSpec (merged with DeploymentConfig)
* @param vpc VPC to deploy database into
* @param instanceId Logical ID for the database instance
* @return Database connection information for application configuration
*/
public static DatabaseConnection createDatabase(
SystemContext ctx,
DatabaseRequirement requirement,
IVpc vpc,
String instanceId) {
return createDatabase(ctx, requirement, vpc, instanceId, null, null, null);
}
/**
* Create RDS database instance with optional DeploymentConfig overrides.
*
* @param ctx System context with security profile and deployment settings
* @param requirement Database requirements (already merged with DeploymentConfig in ApplicationFactory)
* @param vpc VPC to deploy database into
* @param instanceId Logical ID for the database instance
* @param backupRetentionDaysOverride Optional backup retention days from DeploymentConfig
* @param multiAzOverride Optional Multi-AZ setting from DeploymentConfig
* @param enableEncryptionOverride Optional encryption setting from DeploymentConfig
* @return Database connection information for application configuration
*/
public static DatabaseConnection createDatabase(
SystemContext ctx,
DatabaseRequirement requirement,
IVpc vpc,
String instanceId,
Integer backupRetentionDaysOverride,
Boolean multiAzOverride,
Boolean enableEncryptionOverride) {
Construct scope = ctx;
String stackName = ctx.stackName;
SecurityProfile security = ctx.security;
// Determine encryption setting with priority: DeploymentConfig > SecurityProfile default
// For production deployments, encryption defaults to true
boolean enableEncryption;
if (enableEncryptionOverride != null) {
enableEncryption = enableEncryptionOverride;
} else {
// Default: encrypt for PRODUCTION and STAGING, optional for DEV
enableEncryption = (security != SecurityProfile.DEV);
}
// Create KMS key for encryption if enabled
IKey encryptionKey = null;
if (enableEncryption) {
encryptionKey = Key.Builder.create(scope, instanceId + "EncryptionKey")
.description("RDS encryption key for " + stackName + "-" + instanceId)
.enableKeyRotation(true)
// Always destroy - if database is deleted, encrypted data is gone anyway
.removalPolicy(RemovalPolicy.DESTROY)
.build();
}
// Create database credentials in Secrets Manager
// Always destroy - if the database is deleted, credentials are useless
// Note: No secretName specified - CloudFormation will auto-generate unique name
// This prevents "AlreadyExists" errors from retained secrets in previous deployments
Secret databaseSecret = Secret.Builder.create(scope, instanceId + "Secret")
.description("Database credentials for " + stackName + "-" + instanceId)
.generateSecretString(SecretStringGenerator.builder()
.secretStringTemplate("{\"username\":\"" + requirement.databaseName() + "admin\"}")
.generateStringKey("password")
.excludePunctuation(true)
.passwordLength(32)
.build())
.removalPolicy(RemovalPolicy.DESTROY)
.build();
// TODO: Enable automatic credential rotation for production
// Requires Lambda function or hosted rotation setup
// if (security == SecurityProfile.PRODUCTION) {
// databaseSecret.addRotationSchedule(instanceId + "Rotation",
// RotationScheduleOptions.builder()
// .automaticallyAfter(Duration.days(30))
// .build());
// }
// Determine database engine
IInstanceEngine engine = getEngine(requirement.engine(), requirement.version());
// Create parameter group with optimized settings
IParameterGroup parameterGroup = createParameterGroup(
scope, instanceId, requirement.engine(), requirement.version());
// Create subnet group for private subnets only
SubnetGroup subnetGroup = SubnetGroup.Builder.create(scope, instanceId + "SubnetGroup")
.description("Subnet group for " + stackName + "-" + instanceId)
.vpc(vpc)
.vpcSubnets(SubnetSelection.builder()
.subnetType(SubnetType.PRIVATE_WITH_EGRESS)
.build())
.removalPolicy(RemovalPolicy.DESTROY)
.build();
// Create explicit security group for RDS
// When restrictSecurityGroupEgress is enabled, restrict egress to VPC CIDR only
boolean restrictEgress = ctx.securityProfileConfig.get()
.map(config -> config.isRestrictSecurityGroupEgressEnabled())
.orElse(false);
SecurityGroup dbSecurityGroup = SecurityGroup.Builder.create(scope, instanceId + "SecurityGroup")
.vpc(vpc)
.description("Security group for " + instanceId + " database")
.allowAllOutbound(!restrictEgress) // Restrict egress when flag is enabled
.build();
// If egress is restricted, add explicit egress rule for VPC CIDR only
if (restrictEgress) {
dbSecurityGroup.addEgressRule(
Peer.ipv4(vpc.getVpcCidrBlock()),
Port.allTraffic(),
"Allow egress to VPC CIDR only"
);
}
// Determine backup retention based on DeploymentConfig override or security profile
int backupRetention;
if (backupRetentionDaysOverride != null) {
backupRetention = backupRetentionDaysOverride;
} else {
backupRetention = switch (security) {
case PRODUCTION -> 30; // 30 days for production
case STAGING -> 14; // 14 days for staging
case DEV -> 7; // 7 days minimum for dev
};
}
// Parse instance type from instance class
InstanceType instanceType = parseInstanceType(requirement.instanceClass());
// Build instance identifier with 63 character limit (RDS constraint)
String dbInstanceIdentifier = truncateDbIdentifier(stackName + "-" + instanceId, 63);
// Create database instance builder
DatabaseInstance.Builder instanceBuilder = DatabaseInstance.Builder.create(scope, instanceId)
.instanceIdentifier(dbInstanceIdentifier)
.engine(engine)
.instanceType(instanceType)
.vpc(vpc)
.subnetGroup(subnetGroup)
.securityGroups(List.of(dbSecurityGroup)) // Use explicit security group
.databaseName(requirement.databaseName())
.credentials(Credentials.fromSecret(databaseSecret))
.parameterGroup(parameterGroup)
// Allow stack deletion to remove database
.removalPolicy(RemovalPolicy.DESTROY)
// Security configurations
.storageEncrypted(enableEncryption)
.publiclyAccessible(false)
// Enable deletion protection based on security profile configuration
.deletionProtection(ctx.securityProfileConfig.get()
.map(config -> config.isRdsDeletionProtectionEnabled())
.orElse(false))
.autoMinorVersionUpgrade(security == SecurityProfile.PRODUCTION)
.iamAuthentication(security == SecurityProfile.PRODUCTION || security == SecurityProfile.STAGING)
// Backup configurations
.backupRetention(Duration.days(backupRetention))
.preferredBackupWindow("03:00-04:00")
.preferredMaintenanceWindow("sun:04:00-sun:05:00")
.copyTagsToSnapshot(true)
// Enable Multi-AZ based on deployment context override, security profile configuration, or compliance requirements
.multiAz(multiAzOverride != null ? multiAzOverride :
ctx.securityProfileConfig.get()
.map(config -> config.isRdsDatabaseMultiAzEnabled())
.orElse(security == SecurityProfile.PRODUCTION))
// Storage configuration
.allocatedStorage(requirement.allocatedStorageGB())
.maxAllocatedStorage(requirement.allocatedStorageGB() * 2)
.storageType(StorageType.GP3)
.cloudwatchLogsExports(getCloudWatchLogsExports(requirement.engine()));
// Conditionally enable Performance Insights for PRODUCTION only
if (security == SecurityProfile.PRODUCTION) {
instanceBuilder
.enablePerformanceInsights(true)
.performanceInsightRetention(PerformanceInsightRetention.LONG_TERM)
.performanceInsightEncryptionKey(encryptionKey)
.monitoringInterval(Duration.seconds(60))
.cloudwatchLogsRetention(RetentionDays.ONE_YEAR);
} else {
instanceBuilder
.enablePerformanceInsights(false)
.monitoringInterval(Duration.seconds(0))
.cloudwatchLogsRetention(RetentionDays.ONE_MONTH);
}
DatabaseInstance instance = instanceBuilder.build();
// Register AWS Config rules for RDS compliance monitoring
ctx.requireConfigRule(AwsConfigRule.RDS_STORAGE_ENCRYPTED);
ctx.requireConfigRule(AwsConfigRule.DB_INSTANCE_BACKUP_ENABLED);
ctx.requireConfigRule(AwsConfigRule.RDS_INSTANCE_PUBLIC_ACCESS_CHECK);
ctx.requireConfigRule(AwsConfigRule.RDS_INSTANCE_DELETION_PROTECTION_ENABLED);
ctx.requireConfigRule(AwsConfigRule.RDS_LOGGING_ENABLED);
if (multiAzOverride != null ? multiAzOverride : (security == SecurityProfile.PRODUCTION)) {
ctx.requireConfigRule(AwsConfigRule.RDS_MULTI_AZ);
}
// Add CDK-NAG suppressions for RDS compliance findings
NagSuppressions.addResourceSuppressions(
databaseSecret,
List.of(
NagPackSuppression.builder()
.id("AwsSolutions-SMG4")
.reason("Secret rotation requires Lambda function setup - scheduled for future implementation. Credentials are generated with 32-char password and stored securely in Secrets Manager.")
.build()
),
Boolean.TRUE
);
NagSuppressions.addResourceSuppressions(
instance,
List.of(
NagPackSuppression.builder()
.id("AwsSolutions-RDS11")
.reason("Default database ports (5432/3306) are used intentionally - security is enforced via VPC security groups restricting access to application containers only. Non-standard ports provide minimal security benefit (security through obscurity).")
.build(),
NagPackSuppression.builder()
.id("AwsSolutions-IAM4")
.reason("RDS Enhanced Monitoring requires the AWS managed policy AmazonRDSEnhancedMonitoringRole - this is the AWS-recommended approach for RDS monitoring and cannot be replaced with a customer-managed policy.")
.build()
),
Boolean.TRUE
);
// Store database instance and its security group in SystemContext
ctx.rdsDatabase.set(instance);
ctx.dbCredentials.set(databaseSecret);
ctx.dbSecurityGroup.set(dbSecurityGroup); // Store our explicit security group for Fargate to add ingress rules
// Return connection information for application
// Determine port based on engine (CDK Token can't be parsed to int)
int port = getDefaultPort(requirement.engine());
String username = requirement.databaseName() + "admin";
// Store connection string components in SystemContext for applications that need complete URLs
// This is especially needed for distroless containers like Mattermost that can't do shell substitution
ctx.dbConnectionStringComponents.set(Map.of(
"host", instance.getDbInstanceEndpointAddress(),
"port", String.valueOf(port),
"database", requirement.databaseName(),
"username", username,
"engine", requirement.engine()
));
// Create SSM Parameter for PostgreSQL datasource URL (for distroless containers like Mattermost)
// Only create if the application requires it - indicated by instanceId containing "mattermost"
// Uses CloudFormation dynamic reference to resolve the password from Secrets Manager at deploy time
// Format: postgres://user:password@host:port/database?sslmode=require&connect_timeout=10
boolean needsDatasourceParam = instanceId.toLowerCase().contains("mattermost");
if (needsDatasourceParam && ("postgres".equals(requirement.engine()) || "postgresql".equals(requirement.engine()))) {
// Build the datasource URL using Fn.join and dynamic reference for the password
// The dynamic reference {{resolve:secretsmanager:ARN:SecretString:password}} is resolved by CloudFormation
String datasourceUrl = software.amazon.awscdk.Fn.join("", java.util.List.of(
"postgres://",
username,
":",
// Dynamic reference to password - CloudFormation resolves this at deploy time
"{{resolve:secretsmanager:",
databaseSecret.getSecretArn(),
":SecretString:password}}",
"@",
instance.getDbInstanceEndpointAddress(),
":",
String.valueOf(port),
"/",
requirement.databaseName(),
"?sslmode=require&connect_timeout=10"
));
// Store datasource URL in SSM Parameter Store
// SSM parameters can contain dynamic references that get resolved
software.amazon.awscdk.services.ssm.StringParameter datasourceParam =
software.amazon.awscdk.services.ssm.StringParameter.Builder
.create(scope, instanceId + "DatasourceUrl")
.parameterName("/" + stackName + "/" + instanceId + "/datasource-url")
.stringValue(datasourceUrl)
.description("PostgreSQL datasource URL for " + instanceId + " (distroless container)")
.build();
// Store the parameter object in SystemContext for ContainerFactory to use directly
// This avoids parameter lookup issues at synth time
ctx.dbDatasourceParameter.set(datasourceParam);
}
return new DatabaseConnection(
instance.getDbInstanceEndpointAddress(),
port,
requirement.databaseName(),
username,
databaseSecret.getSecretArn(),
requirement.engine(),
requirement.version(),
new ArrayList<>() // No read replicas initially (can be added later)
);
}
/**
* Get database engine from requirement.
*/
private static IInstanceEngine getEngine(String engineName, String version) {
return switch (engineName.toLowerCase()) {
case "postgres", "postgresql" -> DatabaseInstanceEngine.postgres(
PostgresInstanceEngineProps.builder()
.version(mapPostgresVersion(version))
.build()
);
case "mysql" -> DatabaseInstanceEngine.mysql(
MySqlInstanceEngineProps.builder()
.version(mapMySqlVersion(version))
.build()
);
case "mariadb" -> DatabaseInstanceEngine.mariaDb(
MariaDbInstanceEngineProps.builder()
.version(mapMariaDbVersion(version))
.build()
);
default -> throw new IllegalArgumentException(
"Unsupported database engine: " + engineName +
". Supported engines: postgres, mysql, mariadb");
};
}
/**
* Map version string to PostgreSQL engine version.
*/
private static PostgresEngineVersion mapPostgresVersion(String version) {
return switch (version) {
case "11" -> PostgresEngineVersion.VER_11;
case "12" -> PostgresEngineVersion.VER_12;
case "13" -> PostgresEngineVersion.VER_13;
case "14" -> PostgresEngineVersion.VER_14;
case "15" -> PostgresEngineVersion.VER_15;
case "16" -> PostgresEngineVersion.VER_16;
default -> PostgresEngineVersion.of(version, version);
};
}
/**
* Map version string to MySQL engine version.
*/
private static MysqlEngineVersion mapMySqlVersion(String version) {
if ("5.7".equals(version)) {
System.err.println("WARNING: MySQL 5.7 reached end-of-life in October 2023. Consider upgrading to MySQL 8.0 for continued security updates and support.");
}
return switch (version) {
case "5.7" -> MysqlEngineVersion.VER_5_7;
case "8.0" -> MysqlEngineVersion.VER_8_0;
case "8.0.32" -> MysqlEngineVersion.VER_8_0_32;
case "8.0.33" -> MysqlEngineVersion.VER_8_0_33;
case "8.0.34" -> MysqlEngineVersion.VER_8_0_34;
case "8.0.35" -> MysqlEngineVersion.VER_8_0_35;
default -> MysqlEngineVersion.of(version, version);
};
}
/**
* Map version string to MariaDB engine version.
*/
private static MariaDbEngineVersion mapMariaDbVersion(String version) {
return switch (version) {
case "10.6" -> MariaDbEngineVersion.VER_10_6;
case "10.11" -> MariaDbEngineVersion.VER_10_11;
default -> MariaDbEngineVersion.of(version, version);
};
}
/**
* Parse instance type from instance class string.
*
* <p>Converts strings like "db.t3.medium" to InstanceType.</p>
*/
private static InstanceType parseInstanceType(String instanceClass) {
// Extract class and size from instance class (e.g., "db.t3.medium")
String[] parts = instanceClass.split("\\.");
if (parts.length < 3) {
return InstanceType.of(InstanceClass.BURSTABLE3, InstanceSize.MICRO);
}
InstanceClass instanceClassEnum = parseInstanceClass(parts[1]);
InstanceSize instanceSize = parseInstanceSize(parts[2]);
return InstanceType.of(instanceClassEnum, instanceSize);
}
/**
* Parse instance class from string.
*/
private static software.amazon.awscdk.services.ec2.InstanceClass parseInstanceClass(String className) {
return switch (className.toLowerCase()) {
case "t3" -> software.amazon.awscdk.services.ec2.InstanceClass.BURSTABLE3;
case "t4g" -> software.amazon.awscdk.services.ec2.InstanceClass.BURSTABLE4_GRAVITON;
case "m5" -> software.amazon.awscdk.services.ec2.InstanceClass.M5;
case "m6g" -> software.amazon.awscdk.services.ec2.InstanceClass.MEMORY6_GRAVITON;
case "r5" -> software.amazon.awscdk.services.ec2.InstanceClass.R5;
case "r6g" -> software.amazon.awscdk.services.ec2.InstanceClass.MEMORY6_GRAVITON;
default -> software.amazon.awscdk.services.ec2.InstanceClass.BURSTABLE3;
};
}
/**
* Parse instance size from string.
*/
private static software.amazon.awscdk.services.ec2.InstanceSize parseInstanceSize(String size) {
return switch (size.toLowerCase()) {
case "micro" -> software.amazon.awscdk.services.ec2.InstanceSize.MICRO;
case "small" -> software.amazon.awscdk.services.ec2.InstanceSize.SMALL;
case "medium" -> software.amazon.awscdk.services.ec2.InstanceSize.MEDIUM;
case "large" -> software.amazon.awscdk.services.ec2.InstanceSize.LARGE;
case "xlarge" -> software.amazon.awscdk.services.ec2.InstanceSize.XLARGE;
case "2xlarge" -> software.amazon.awscdk.services.ec2.InstanceSize.XLARGE2;
case "4xlarge" -> software.amazon.awscdk.services.ec2.InstanceSize.XLARGE4;
case "8xlarge" -> software.amazon.awscdk.services.ec2.InstanceSize.XLARGE8;
case "12xlarge" -> software.amazon.awscdk.services.ec2.InstanceSize.XLARGE12;
case "16xlarge" -> software.amazon.awscdk.services.ec2.InstanceSize.XLARGE16;
case "24xlarge" -> software.amazon.awscdk.services.ec2.InstanceSize.XLARGE24;
default -> software.amazon.awscdk.services.ec2.InstanceSize.MICRO;
};
}
/**
* Create parameter group with optimized settings for the database engine.
*
* <p>Parameters are engine-specific:</p>
* <ul>
* <li><b>PostgreSQL:</b> log_statement, log_connections, log_disconnections</li>
* <li><b>MySQL/MariaDB:</b> general_log, slow_query_log, log_output</li>
* </ul>
*/
private static IParameterGroup createParameterGroup(
Construct scope, String id, String engine, String version) {
IInstanceEngine instanceEngine = getEngine(engine, version);
Map<String, String> parameters = getEngineParameters(engine);
return ParameterGroup.Builder.create(scope, id + "ParameterGroup")
.engine(instanceEngine)
.description("Optimized parameter group for " + id)
.parameters(parameters)
.removalPolicy(RemovalPolicy.DESTROY)
.build();
}
/**
* Get engine-specific parameter group settings.
*
* <p>Each database engine has different parameter names for audit logging.</p>
*
* @param engine Database engine name
* @return Map of parameter name to value
*/
private static Map<String, String> getEngineParameters(String engine) {
return switch (engine.toLowerCase()) {
case "postgres", "postgresql" -> Map.of(
"log_statement", "ddl", // Log DDL statements for audit
"log_connections", "1", // Log connection attempts
"log_disconnections", "1" // Log disconnections
);
case "mysql" -> Map.of(
"general_log", "1", // Enable general query log
"slow_query_log", "1", // Enable slow query log
"log_output", "FILE", // Log to files (for CloudWatch export)
"long_query_time", "2" // Log queries taking > 2 seconds
);
case "mariadb" -> Map.of(
"general_log", "1", // Enable general query log
"slow_query_log", "1", // Enable slow query log
"log_output", "FILE", // Log to files (for CloudWatch export)
"long_query_time", "2" // Log queries taking > 2 seconds
);
default -> Map.of(); // Empty for unsupported engines
};
}
/**
* Get CloudWatch Logs exports for the database engine.
*
* <p>Enables audit logging for compliance frameworks.</p>
*/
private static List<String> getCloudWatchLogsExports(String engine) {
return switch (engine.toLowerCase()) {
case "postgres", "postgresql" -> List.of("postgresql");
case "mysql" -> List.of("error", "general", "slowquery");
case "mariadb" -> List.of("error", "general", "slowquery");
default -> List.of();
};
}
/**
* Get default port for database engine.
*/
private static int getDefaultPort(String engine) {
return switch (engine.toLowerCase()) {
case "postgres", "postgresql" -> 5432;
case "mysql" -> 3306;
case "mariadb" -> 3306;
default -> 5432;
};
}
/**
* Truncate DB instance identifier to meet RDS constraints.
*
* <p>RDS DBInstanceIdentifier must be:
* <ul>
* <li>1-63 characters long</li>
* <li>Contain only alphanumeric characters and hyphens</li>
* <li>First character must be a letter</li>
* <li>Cannot end with a hyphen</li>
* <li>Cannot contain two consecutive hyphens</li>
* </ul>
*
* @param identifier The proposed identifier
* @param maxLength Maximum allowed length (63 for RDS)
* @return Truncated identifier that meets RDS constraints
*/
private static String truncateDbIdentifier(String identifier, int maxLength) {
if (identifier == null || identifier.isEmpty()) {
return "db-instance";
}
// Already within limit
if (identifier.length() <= maxLength) {
return identifier;
}
// Truncate and ensure it doesn't end with a hyphen
String truncated = identifier.substring(0, maxLength);
while (truncated.endsWith("-") && truncated.length() > 1) {
truncated = truncated.substring(0, truncated.length() - 1);
}
return truncated;
}
}