from pathlib import Path from typing import Optional, Any, Dict, List, Union from typing_extensions import Literal from phi.aws.api_client import AwsApiClient from phi.aws.resource.base import AwsResource from phi.aws.resource.cloudformation.stack import CloudFormationStack from phi.aws.resource.ec2.security_group import SecurityGroup from phi.aws.resource.rds.db_instance import DbInstance from phi.aws.resource.rds.db_subnet_group import DbSubnetGroup from phi.aws.resource.secret.manager import SecretsManager from phi.cli.console import print_info from phi.utils.log import logger class DbCluster(AwsResource): """ Reference: - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/rds.html """ resource_type: Optional[str] = "DbCluster" service_name: str = "rds" # Name of the cluster. name: str # The name of the database engine to be used for this DB cluster. engine: Union[str, Literal["aurora", "aurora-mysql", "aurora-postgresql", "mysql", "postgres"]] # The version number of the database engine to use. # For valid engine_version values, refer to # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/rds.html#RDS.Client.create_db_cluster # Or run: aws rds describe-db-engine-versions --engine postgres --query "DBEngineVersions[].EngineVersion" engine_version: Optional[str] = None # DbInstances to add to this cluster db_instances: Optional[List[DbInstance]] = None # The name for your database of up to 64 alphanumeric characters. # If you do not provide a name, Amazon RDS doesn't create a database in the DB cluster you are creating. # Provide DATABASE_NAME here or as DATABASE_NAME in secrets_file # Valid for: Aurora DB clusters and Multi-AZ DB clusters database_name: Optional[str] = None # The DB cluster identifier. This parameter is stored as a lowercase string. # If None, use name as the db_cluster_identifier # Constraints: # Must contain from 1 to 63 letters, numbers, or hyphens. # First character must be a letter. # Can't end with a hyphen or contain two consecutive hyphens. # Example: my-cluster1 # Valid for: Aurora DB clusters and Multi-AZ DB clusters db_cluster_identifier: Optional[str] = None # The name of the DB cluster parameter group to associate with this DB cluster. # If you do not specify a value, then the default DB cluster parameter group for the specified # DB engine and version is used. # Constraints: If supplied, must match the name of an existing DB cluster parameter group. db_cluster_parameter_group_name: Optional[str] = None # The port number on which the instances in the DB cluster accept connections. # RDS for MySQL and Aurora MySQL # - Default: 3306 # - Valid values: 1150-65535 # RDS for PostgreSQL and Aurora PostgreSQL # - Default: 5432 # - Valid values: 1150-65535 port: Optional[int] = None # The name of the master user for the DB cluster. # Constraints: # Must be 1 to 16 letters or numbers. # First character must be a letter. # Can't be a reserved word for the chosen database engine. # Provide MASTER_USERNAME here or as MASTER_USERNAME in secrets_file master_username: Optional[str] = None # The password for the master database user. This password can contain any printable ASCII character # except "/", """, or "@". # Constraints: Must contain from 8 to 41 characters. # Provide MASTER_USER_PASSWORD here or as MASTER_USER_PASSWORD in secrets_file master_user_password: Optional[str] = None # Read secrets from a file in yaml format secrets_file: Optional[Path] = None # Read secret variables from AWS Secret aws_secret: Optional[SecretsManager] = None # A list of Availability Zones (AZs) where DB instances in the DB cluster can be created. # Valid for: Aurora DB clusters only availability_zones: Optional[List[str]] = None # A DB subnet group to associate with this DB cluster. # This setting is required to create a Multi-AZ DB cluster. # Constraints: Must match the name of an existing DBSubnetGroup. Must not be default. db_subnet_group_name: Optional[str] = None # If db_subnet_group_name is None, # Read the db_subnet_group_name from db_subnet_group db_subnet_group: Optional[DbSubnetGroup] = None # Compute and memory capacity of each DB instance in the Multi-AZ DB cluster, for example db.m6g.xlarge. # Not all DB instance classes are available in all Amazon Web Services Regions, or for all database engines. # This setting is required to create a Multi-AZ DB cluster. db_instance_class: Optional[str] = None # The amount of storage in gibibytes (GiB) to allocate to each DB instance in the Multi-AZ DB cluster. allocated_storage: Optional[int] = None # The storage type to associate with the DB cluster. # This setting is required to create a Multi-AZ DB cluster. # When specified for a Multi-AZ DB cluster, a value for the Iops parameter is required. # Valid Values: # Aurora DB clusters - aurora | aurora-iopt1 # Multi-AZ DB clusters - io1 # Default: # Aurora DB clusters - aurora # Multi-AZ DB clusters - io1 storage_type: Optional[str] = None # The amount of Provisioned IOPS (input/output operations per second) to be initially allocated for each DB # instance in the Multi-AZ DB cluster. iops: Optional[int] = None # Specifies whether the DB cluster is publicly accessible. # When the DB cluster is publicly accessible, its Domain Name System (DNS) endpoint resolves to the private # IP address from within the DB cluster’s virtual private cloud (VPC). It resolves to the public IP address # from outside the DB cluster’s VPC. Access to the DB cluster is ultimately controlled by the security group # it uses. That public access isn’t permitted if the security group assigned to the DB cluster doesn’t permit it. # When the DB cluster isn’t publicly accessible, it is an internal DB cluster with a DNS name that resolves to a # private IP address. # Default: The default behavior varies depending on whether DBSubnetGroupName is specified. # If DBSubnetGroupName isn’t specified, and PubliclyAccessible isn’t specified, the following applies: # If the default VPC in the target Region doesn’t have an internet gateway attached to it, the DB cluster is private # If the default VPC in the target Region has an internet gateway attached to it, the DB cluster is public. # If DBSubnetGroupName is specified, and PubliclyAccessible isn’t specified, the following applies: # If the subnets are part of a VPC that doesn’t have an internet gateway attached to it, the DB cluster is private. # If the subnets are part of a VPC that has an internet gateway attached to it, the DB cluster is public. publicly_accessible: Optional[bool] = None # A list of EC2 VPC security groups to associate with this DB cluster. vpc_security_group_ids: Optional[List[str]] = None # If vpc_security_group_ids is None, # Read the security_group_id from vpc_stack vpc_stack: Optional[CloudFormationStack] = None # Add security_group_ids from db_security_groups db_security_groups: Optional[List[SecurityGroup]] = None # The DB engine mode of the DB cluster, either provisioned or serverless engine_mode: Optional[Literal["provisioned", "serverless"]] = None # For DB clusters in serverless DB engine mode, the scaling properties of the DB cluster. scaling_configuration: Optional[Dict[str, Any]] = None # Contains the scaling configuration of an Aurora Serverless v2 DB cluster. serverless_v2_scaling_configuration: Dict[str, Any] = { "MinCapacity": 0.5, "MaxCapacity": 8, } # A value that indicates whether the DB cluster has deletion protection enabled. # The database can't be deleted when deletion protection is enabled. By default, deletion protection isn't enabled. deletion_protection: Optional[bool] = None # The number of days for which automated backups are retained. # Default: 1 # Constraints: Must be a value from 1 to 35 # Valid for: Aurora DB clusters and Multi-AZ DB clusters backup_retention_period: Optional[int] = None # A value that indicates that the DB cluster should be associated with the specified CharacterSet. # Valid for: Aurora DB clusters only character_set_name: Optional[str] = None # A value that indicates that the DB cluster should be associated with the specified option group. option_group_name: Optional[str] = None # The daily time range during which automated backups are created if automated backups are enabled # using the BackupRetentionPeriod parameter. # The default is a 30-minute window selected at random from an 8-hour block of time for each # Amazon Web Services Region. # Constraints: # Must be in the format hh24:mi-hh24:mi . # Must be in Universal Coordinated Time (UTC). # Must not conflict with the preferred maintenance window. # Must be at least 30 minutes. preferred_backup_window: Optional[str] = None # The weekly time range during which system maintenance can occur, in Universal Coordinated Time (UTC). # Format: ddd:hh24:mi-ddd:hh24:mi # The default is a 30-minute window selected at random from an 8-hour block of time for each # Amazon Web Services Region, occurring on a random day of the week. # Valid Days: Mon, Tue, Wed, Thu, Fri, Sat, Sun. # Constraints: Minimum 30-minute window. preferred_maintenance_window: Optional[str] = None # The Amazon Resource Name (ARN) of the source DB instance or DB cluster # if this DB cluster is created as a read replica. replication_source_identifier: Optional[str] = None # Tags to assign to the DB cluster. tags: Optional[List[Dict[str, str]]] = None # A value that indicates whether the DB cluster is encrypted. storage_encrypted: Optional[bool] = None # The Amazon Web Services KMS key identifier for an encrypted DB cluster. kms_key_id: Optional[str] = None pre_signed_url: Optional[str] = None # A value that indicates whether to enable mapping of Amazon Web Services Identity and Access Management (IAM) # accounts to database accounts. By default, mapping isn't enabled. enable_iam_database_authentication: Optional[bool] = None # The target backtrack window, in seconds. To disable backtracking, set this value to 0. # Default: 0 backtrack_window: Optional[int] = None # The list of log types that need to be enabled for exporting to CloudWatch Logs. # The values in the list depend on the DB engine being used. # RDS for MySQL: Possible values are error , general , and slowquery . # RDS for PostgreSQL: Possible values are postgresql and upgrade . # Aurora MySQL: Possible values are audit , error , general , and slowquery . # Aurora PostgreSQL: Possible value is postgresql . enable_cloudwatch_logs_exports: Optional[List[str]] = None # The global cluster ID of an Aurora cluster that becomes the primary cluster in the new global database cluster. global_cluster_identifier: Optional[str] = None # A value that indicates whether to enable the HTTP endpoint for an Aurora Serverless v1 DB cluster. # By default, the HTTP endpoint is disabled. # When enabled, the HTTP endpoint provides a connectionless web service # API for running SQL queries on the Aurora Serverless v1 DB cluster. # You can also query your database from inside the RDS console with the query editor. enable_http_endpoint: Optional[bool] = None # A value that indicates whether to copy all tags from the DB cluster to snapshots of the DB cluster. # The default is not to copy them. copy_tags_to_snapshot: Optional[bool] = None # The Active Directory directory ID to create the DB cluster in. # For Amazon Aurora DB clusters, Amazon RDS can use Kerberos authentication to authenticate users that connect to # the DB cluster. domain: Optional[str] = None # Specify the name of the IAM role to be used when making API calls to the Directory Service. domain_iam_role_name: Optional[str] = None enable_global_write_forwarding: Optional[bool] = None # A value that indicates whether minor engine upgrades are applied automatically to the DB cluster during the # maintenance window. By default, minor engine upgrades are applied automatically. auto_minor_version_upgrade: Optional[bool] = None # The interval, in seconds, between points when Enhanced Monitoring metrics are collected for the DB cluster. # To turn off collecting Enhanced Monitoring metrics, specify 0. The default is 0. # If MonitoringRoleArn is specified, also set MonitoringInterval to a value other than 0. # Valid Values: 0, 1, 5, 10, 15, 30, 60 monitoring_interval: Optional[int] = None # The Amazon Resource Name (ARN) for the IAM role that permits RDS to send # Enhanced Monitoring metrics to Amazon CloudWatch Logs. monitoring_role_arn: Optional[str] = None enable_performance_insights: Optional[bool] = None performance_insights_kms_key_id: Optional[str] = None performance_insights_retention_period: Optional[int] = None # The network type of the DB cluster. # Valid values: # - IPV4 # - DUAL # The network type is determined by the DBSubnetGroup specified for the DB cluster. # A DBSubnetGroup can support only the IPv4 protocol or the IPv4 and the IPv6 protocols (DUAL ). network_type: Optional[str] = None # Reserved for future use. db_system_id: Optional[str] = None # The ID of the region that contains the source for the db cluster. source_region: Optional[str] = None enable_local_write_forwarding: Optional[bool] = None # Specifies whether to manage the master user password with Amazon Web Services Secrets Manager. # Constraints: # Can’t manage the master user password with Amazon Web Services Secrets Manager if MasterUserPassword is specified. manage_master_user_password: Optional[bool] = None # The Amazon Web Services KMS key identifier to encrypt a secret that is automatically generated and # managed in Amazon Web Services Secrets Manager. master_user_secret_kms_key_id: Optional[str] = None # Parameters for delete function # Skip the creation of a final DB cluster snapshot before the DB cluster is deleted. # If skip_final_snapshot = True, no DB cluster snapshot is created. # If skip_final_snapshot = None | False, a DB cluster snapshot is created before the DB cluster is deleted. # You must specify a FinalDBSnapshotIdentifier parameter # if skip_final_snapshot = None | False skip_final_snapshot: Optional[bool] = True # The DB cluster snapshot identifier of the new DB cluster snapshot created when SkipFinalSnapshot is disabled. final_db_snapshot_identifier: Optional[str] = None # Specifies whether to remove automated backups immediately after the DB cluster is deleted. # The default is to remove automated backups immediately after the DB cluster is deleted. delete_automated_backups: Optional[bool] = None # Parameters for update function new_db_cluster_identifier: Optional[str] = None apply_immediately: Optional[bool] = None cloudwatch_logs_exports: Optional[List[str]] = None allow_major_version_upgrade: Optional[bool] = None db_instance_parameter_group_name: Optional[str] = None rotate_master_user_password: Optional[bool] = None allow_engine_mode_change: Optional[bool] = None # Cache secret_data cached_secret_data: Optional[Dict[str, Any]] = None def get_db_cluster_identifier(self): return self.db_cluster_identifier or self.name def get_master_username(self, aws_client: Optional[AwsApiClient] = None) -> Optional[str]: master_username = self.master_username if master_username is None and self.secrets_file is not None: # read from secrets_file secret_data = self.get_secret_file_data() if secret_data is not None: master_username = secret_data.get("MASTER_USERNAME", master_username) if master_username is None and self.aws_secret is not None: # read from aws_secret logger.debug(f"Reading MASTER_USERNAME from secret: {self.aws_secret.name}") master_username = self.aws_secret.get_secret_value("MASTER_USERNAME", aws_client=aws_client) return master_username def get_master_user_password(self, aws_client: Optional[AwsApiClient] = None) -> Optional[str]: master_user_password = self.master_user_password if master_user_password is None and self.secrets_file is not None: # read from secrets_file secret_data = self.get_secret_file_data() if secret_data is not None: master_user_password = secret_data.get("MASTER_USER_PASSWORD", master_user_password) if master_user_password is None and self.aws_secret is not None: # read from aws_secret logger.debug(f"Reading MASTER_USER_PASSWORD from secret: {self.aws_secret.name}") master_user_password = self.aws_secret.get_secret_value("MASTER_USER_PASSWORD", aws_client=aws_client) return master_user_password def get_database_name(self, aws_client: Optional[AwsApiClient] = None) -> Optional[str]: database_name = self.database_name if database_name is None and self.secrets_file is not None: # read from secrets_file secret_data = self.get_secret_file_data() if secret_data is not None: database_name = secret_data.get("DATABASE_NAME", database_name) if database_name is None: database_name = secret_data.get("DB_NAME", database_name) if database_name is None and self.aws_secret is not None: # read from aws_secret logger.debug(f"Reading DATABASE_NAME from secret: {self.aws_secret.name}") database_name = self.aws_secret.get_secret_value("DATABASE_NAME", aws_client=aws_client) if database_name is None: logger.debug(f"Reading DB_NAME from secret: {self.aws_secret.name}") database_name = self.aws_secret.get_secret_value("DB_NAME", aws_client=aws_client) return database_name def get_db_name(self) -> Optional[str]: # Alias for get_database_name because db_instances use `db_name` and db_clusters use `database_name` return self.get_database_name() def _create(self, aws_client: AwsApiClient) -> bool: """Creates the DbCluster Args: aws_client: The AwsApiClient for the current cluster """ print_info(f"Creating {self.get_resource_type()}: {self.get_resource_name()}") # create a dict of args which are not null, otherwise aws type validation fails not_null_args: Dict[str, Any] = {} # Step 1: Get the VpcSecurityGroupIds vpc_security_group_ids = self.vpc_security_group_ids if vpc_security_group_ids is None and self.vpc_stack is not None: vpc_stack_sg = self.vpc_stack.get_security_group(aws_client=aws_client) if vpc_stack_sg is not None: vpc_security_group_ids = [vpc_stack_sg] if self.db_security_groups is not None: sg_ids = [] for sg in self.db_security_groups: sg_id = sg.get_security_group_id(aws_client) if sg_id is not None: sg_ids.append(sg_id) if len(sg_ids) > 0: if vpc_security_group_ids is None: vpc_security_group_ids = [] vpc_security_group_ids.extend(sg_ids) if vpc_security_group_ids is not None: logger.debug(f"Using SecurityGroups: {vpc_security_group_ids}") not_null_args["VpcSecurityGroupIds"] = vpc_security_group_ids # Step 2: Get the DbSubnetGroupName db_subnet_group_name = self.db_subnet_group_name if db_subnet_group_name is None and self.db_subnet_group is not None: db_subnet_group_name = self.db_subnet_group.name logger.debug(f"Using DbSubnetGroup: {db_subnet_group_name}") if db_subnet_group_name is not None: not_null_args["DBSubnetGroupName"] = db_subnet_group_name database_name = self.get_database_name() if database_name: not_null_args["DatabaseName"] = database_name master_username = self.get_master_username() if master_username: not_null_args["MasterUsername"] = master_username master_user_password = self.get_master_user_password() if master_user_password: not_null_args["MasterUserPassword"] = master_user_password if self.availability_zones: not_null_args["AvailabilityZones"] = self.availability_zones if self.backup_retention_period: not_null_args["BackupRetentionPeriod"] = self.backup_retention_period if self.character_set_name: not_null_args["CharacterSetName"] = self.character_set_name if self.db_cluster_parameter_group_name: not_null_args["DBClusterParameterGroupName"] = self.db_cluster_parameter_group_name if self.engine_version: not_null_args["EngineVersion"] = self.engine_version if self.port: not_null_args["Port"] = self.port if self.option_group_name: not_null_args["OptionGroupName"] = self.option_group_name if self.preferred_backup_window: not_null_args["PreferredBackupWindow"] = self.preferred_backup_window if self.preferred_maintenance_window: not_null_args["PreferredMaintenanceWindow"] = self.preferred_maintenance_window if self.replication_source_identifier: not_null_args["ReplicationSourceIdentifier"] = self.replication_source_identifier if self.tags: not_null_args["Tags"] = self.tags if self.storage_encrypted: not_null_args["StorageEncrypted"] = self.storage_encrypted if self.kms_key_id: not_null_args["KmsKeyId"] = self.kms_key_id if self.enable_iam_database_authentication: not_null_args["EnableIAMDbClusterAuthentication"] = self.enable_iam_database_authentication if self.backtrack_window: not_null_args["BacktrackWindow"] = self.backtrack_window if self.enable_cloudwatch_logs_exports: not_null_args["EnableCloudwatchLogsExports"] = self.enable_cloudwatch_logs_exports if self.engine_mode: not_null_args["EngineMode"] = self.engine_mode if self.scaling_configuration: not_null_args["ScalingConfiguration"] = self.scaling_configuration if self.deletion_protection: not_null_args["DeletionProtection"] = self.deletion_protection if self.global_cluster_identifier: not_null_args["GlobalClusterIdentifier"] = self.global_cluster_identifier if self.enable_http_endpoint: not_null_args["EnableHttpEndpoint"] = self.enable_http_endpoint if self.copy_tags_to_snapshot: not_null_args["CopyTagsToSnapshot"] = self.copy_tags_to_snapshot if self.domain: not_null_args["Domain"] = self.domain if self.domain_iam_role_name: not_null_args["DomainIAMRoleName"] = self.domain_iam_role_name if self.enable_global_write_forwarding: not_null_args["EnableGlobalWriteForwarding"] = self.enable_global_write_forwarding if self.db_instance_class: not_null_args["DBClusterInstanceClass"] = self.db_instance_class if self.allocated_storage: not_null_args["AllocatedStorage"] = self.allocated_storage if self.storage_type: not_null_args["StorageType"] = self.storage_type if self.iops: not_null_args["Iops"] = self.iops if self.publicly_accessible: not_null_args["PubliclyAccessible"] = self.publicly_accessible if self.auto_minor_version_upgrade: not_null_args["AutoMinorVersionUpgrade"] = self.auto_minor_version_upgrade if self.monitoring_interval: not_null_args["MonitoringInterval"] = self.monitoring_interval if self.monitoring_role_arn: not_null_args["MonitoringRoleArn"] = self.monitoring_role_arn if self.enable_performance_insights: not_null_args["EnablePerformanceInsights"] = self.enable_performance_insights if self.performance_insights_kms_key_id: not_null_args["PerformanceInsightsKMSKeyId"] = self.performance_insights_kms_key_id if self.performance_insights_retention_period: not_null_args["PerformanceInsightsRetentionPeriod"] = self.performance_insights_retention_period if self.serverless_v2_scaling_configuration: not_null_args["ServerlessV2ScalingConfiguration"] = self.serverless_v2_scaling_configuration if self.network_type: not_null_args["NetworkType"] = self.network_type if self.db_system_id: not_null_args["DBSystemId"] = self.db_system_id if self.source_region: not_null_args["SourceRegion"] = self.source_region if self.enable_local_write_forwarding: not_null_args["EnableLocalWriteForwarding"] = self.enable_local_write_forwarding if self.manage_master_user_password: not_null_args["ManageMasterUserPassword"] = self.manage_master_user_password if self.master_user_secret_kms_key_id: not_null_args["MasterUserSecretKmsKeyId"] = self.master_user_secret_kms_key_id # Step 3: Create DBCluster service_client = self.get_service_client(aws_client) try: create_response = service_client.create_db_cluster( DBClusterIdentifier=self.get_db_cluster_identifier(), Engine=self.engine, **not_null_args, ) logger.debug(f"Response: {create_response}") resource_dict = create_response.get("DBCluster", {}) # Validate database creation if resource_dict is not None: logger.debug(f"DBCluster created: {self.get_db_cluster_identifier()}") self.active_resource = resource_dict return True except Exception as e: logger.error(f"{self.get_resource_type()} could not be created.") logger.error(e) return False def post_create(self, aws_client: AwsApiClient) -> bool: db_instances_created = [] if self.db_instances is not None: for db_instance in self.db_instances: db_instance.db_cluster_identifier = self.get_db_cluster_identifier() if db_instance._create(aws_client): # type: ignore db_instances_created.append(db_instance) # Wait for DBCluster to be created if self.wait_for_create: try: print_info(f"Waiting for {self.get_resource_type()} to be active.") waiter = self.get_service_client(aws_client).get_waiter("db_cluster_available") waiter.wait( DBClusterIdentifier=self.get_db_cluster_identifier(), WaiterConfig={ "Delay": self.waiter_delay, "MaxAttempts": self.waiter_max_attempts, }, ) except Exception as e: logger.error("Waiter failed.") logger.error(e) # Wait for DbInstances to be created for db_instance in db_instances_created: db_instance.post_create(aws_client) return True def _read(self, aws_client: AwsApiClient) -> Optional[Any]: """Returns the DbCluster Args: aws_client: The AwsApiClient for the current cluster """ logger.debug(f"Reading {self.get_resource_type()}: {self.get_resource_name()}") from botocore.exceptions import ClientError service_client = self.get_service_client(aws_client) try: resource_identifier = self.get_db_cluster_identifier() describe_response = service_client.describe_db_clusters(DBClusterIdentifier=resource_identifier) logger.debug(f"DbCluster: {describe_response}") resources_list = describe_response.get("DBClusters", None) if resources_list is not None and isinstance(resources_list, list): for _resource in resources_list: _identifier = _resource.get("DBClusterIdentifier", None) if _identifier == resource_identifier: self.active_resource = _resource break except ClientError as ce: logger.debug(f"ClientError: {ce}") except Exception as e: logger.error(f"Error reading {self.get_resource_type()}.") logger.error(e) return self.active_resource def _delete(self, aws_client: AwsApiClient) -> bool: """Deletes the DbCluster Args: aws_client: The AwsApiClient for the current cluster """ print_info(f"Deleting {self.get_resource_type()}: {self.get_resource_name()}") service_client = self.get_service_client(aws_client) self.active_resource = None # Step 1: Delete DbInstances if self.db_instances is not None: for db_instance in self.db_instances: db_instance._delete(aws_client) # Step 2: Delete DbCluster # create a dict of args which are not null, otherwise aws type validation fails not_null_args: Dict[str, Any] = {} if self.final_db_snapshot_identifier: not_null_args["FinalDBSnapshotIdentifier"] = self.final_db_snapshot_identifier if self.delete_automated_backups: not_null_args["DeleteAutomatedBackups"] = self.delete_automated_backups try: db_cluster_identifier = self.get_db_cluster_identifier() delete_response = service_client.delete_db_cluster( DBClusterIdentifier=db_cluster_identifier, SkipFinalSnapshot=self.skip_final_snapshot, **not_null_args, ) logger.debug(f"Response: {delete_response}") resource_dict = delete_response.get("DBCluster", {}) # Validate database deletion if resource_dict is not None: logger.debug(f"DBCluster deleted: {self.get_db_cluster_identifier()}") self.active_resource = resource_dict return True except Exception as e: logger.error(f"{self.get_resource_type()} could not be deleted.") logger.error("Please try again or delete resources manually.") logger.error(e) return False def post_delete(self, aws_client: AwsApiClient) -> bool: # Wait for DbCluster to be deleted if self.wait_for_delete: try: print_info(f"Waiting for {self.get_resource_type()} to be deleted.") waiter = self.get_service_client(aws_client).get_waiter("db_cluster_deleted") waiter.wait( DBClusterIdentifier=self.get_db_cluster_identifier(), WaiterConfig={ "Delay": self.waiter_delay, "MaxAttempts": self.waiter_max_attempts, }, ) except Exception as e: logger.error("Waiter failed.") logger.error(e) return True def _update(self, aws_client: AwsApiClient) -> bool: """Updates the DbCluster""" print_info(f"Updating {self.get_resource_type()}: {self.get_resource_name()}") # Step 1: Get existing DBInstance db_cluster = self.read(aws_client) # create a dict of args which are not null, otherwise aws type validation fails not_null_args: Dict[str, Any] = { "ApplyImmediately": self.apply_immediately, } vpc_security_group_ids = self.vpc_security_group_ids if vpc_security_group_ids is None and self.vpc_stack is not None: vpc_stack_sg = self.vpc_stack.get_security_group(aws_client=aws_client) if vpc_stack_sg is not None: vpc_security_group_ids = [vpc_stack_sg] if self.db_security_groups is not None: sg_ids = [] for sg in self.db_security_groups: sg_id = sg.get_security_group_id(aws_client) if sg_id is not None: sg_ids.append(sg_id) if len(sg_ids) > 0: if vpc_security_group_ids is None: vpc_security_group_ids = [] vpc_security_group_ids.extend(sg_ids) # Check if vpc_security_group_ids has changed existing_vpc_security_group = db_cluster.get("VpcSecurityGroups", []) existing_vpc_security_group_ids = [] for existing_sg in existing_vpc_security_group: existing_vpc_security_group_ids.append(existing_sg.get("VpcSecurityGroupId", None)) if vpc_security_group_ids is not None and vpc_security_group_ids != existing_vpc_security_group_ids: logger.info(f"Updating SecurityGroups: {vpc_security_group_ids}") not_null_args["VpcSecurityGroupIds"] = vpc_security_group_ids master_user_password = self.get_master_user_password() if master_user_password: not_null_args["MasterUserPassword"] = master_user_password if self.new_db_cluster_identifier: not_null_args["NewDBClusterIdentifier"] = self.new_db_cluster_identifier if self.backup_retention_period: not_null_args["BackupRetentionPeriod"] = self.backup_retention_period if self.db_cluster_parameter_group_name: not_null_args["DBClusterParameterGroupName"] = self.db_cluster_parameter_group_name if self.port: not_null_args["Port"] = self.port if self.option_group_name: not_null_args["OptionGroupName"] = self.option_group_name if self.preferred_backup_window: not_null_args["PreferredBackupWindow"] = self.preferred_backup_window if self.preferred_maintenance_window: not_null_args["PreferredMaintenanceWindow"] = self.preferred_maintenance_window if self.enable_iam_database_authentication: not_null_args["EnableIAMDbClusterAuthentication"] = self.enable_iam_database_authentication if self.backtrack_window: not_null_args["BacktrackWindow"] = self.backtrack_window if self.cloudwatch_logs_exports: not_null_args["CloudwatchLogsExportConfiguration"] = self.cloudwatch_logs_exports if self.engine_version: not_null_args["EngineVersion"] = self.engine_version if self.allow_major_version_upgrade: not_null_args["AllowMajorVersionUpgrade"] = self.allow_major_version_upgrade if self.db_instance_parameter_group_name: not_null_args["DBInstanceParameterGroupName"] = self.db_instance_parameter_group_name if self.domain: not_null_args["Domain"] = self.domain if self.domain_iam_role_name: not_null_args["DomainIAMRoleName"] = self.domain_iam_role_name if self.scaling_configuration: not_null_args["ScalingConfiguration"] = self.scaling_configuration if self.deletion_protection: not_null_args["DeletionProtection"] = self.deletion_protection if self.enable_http_endpoint: not_null_args["EnableHttpEndpoint"] = self.enable_http_endpoint if self.copy_tags_to_snapshot: not_null_args["CopyTagsToSnapshot"] = self.copy_tags_to_snapshot if self.enable_global_write_forwarding: not_null_args["EnableGlobalWriteForwarding"] = self.enable_global_write_forwarding if self.db_instance_class: not_null_args["DBClusterInstanceClass"] = self.db_instance_class if self.allocated_storage: not_null_args["AllocatedStorage"] = self.allocated_storage if self.storage_type: not_null_args["StorageType"] = self.storage_type if self.iops: not_null_args["Iops"] = self.iops if self.auto_minor_version_upgrade: not_null_args["AutoMinorVersionUpgrade"] = self.auto_minor_version_upgrade if self.monitoring_interval: not_null_args["MonitoringInterval"] = self.monitoring_interval if self.monitoring_role_arn: not_null_args["MonitoringRoleArn"] = self.monitoring_role_arn if self.enable_performance_insights: not_null_args["EnablePerformanceInsights"] = self.enable_performance_insights if self.performance_insights_kms_key_id: not_null_args["PerformanceInsightsKMSKeyId"] = self.performance_insights_kms_key_id if self.performance_insights_retention_period: not_null_args["PerformanceInsightsRetentionPeriod"] = self.performance_insights_retention_period if self.serverless_v2_scaling_configuration: not_null_args["ServerlessV2ScalingConfiguration"] = self.serverless_v2_scaling_configuration if self.network_type: not_null_args["NetworkType"] = self.network_type if self.manage_master_user_password: not_null_args["ManageMasterUserPassword"] = self.manage_master_user_password if self.rotate_master_user_password: not_null_args["RotateMasterUserPassword"] = self.rotate_master_user_password if self.master_user_secret_kms_key_id: not_null_args["MasterUserSecretKmsKeyId"] = self.master_user_secret_kms_key_id if self.engine_mode: not_null_args["EngineMode"] = self.engine_mode if self.allow_engine_mode_change: not_null_args["AllowEngineModeChange"] = self.allow_engine_mode_change # Step 2: Update DBCluster service_client = self.get_service_client(aws_client) try: update_response = service_client.modify_db_cluster( DBClusterIdentifier=self.get_db_cluster_identifier(), **not_null_args, ) logger.debug(f"Response: {update_response}") resource_dict = update_response.get("DBCluster", {}) # Validate resource update if resource_dict is not None: print_info(f"DBCluster updated: {self.get_resource_name()}") self.active_resource = update_response return True except Exception as e: logger.error(f"{self.get_resource_type()} could not be updated.") logger.error(e) return False def get_db_endpoint(self, aws_client: Optional[AwsApiClient] = None) -> Optional[str]: """Returns the DbCluster endpoint Returns: The DbCluster endpoint """ logger.debug(f"Getting endpoint for {self.get_resource_name()}") _db_endpoint: Optional[str] = None if self.active_resource: _db_endpoint = self.active_resource.get("Endpoint") if _db_endpoint is None: client: AwsApiClient = aws_client or self.get_aws_client() resource = self._read(aws_client=client) if resource is not None: _db_endpoint = resource.get("Endpoint") if _db_endpoint is None: resource = self.read_resource_from_file() if resource is not None: _db_endpoint = resource.get("Endpoint") logger.debug(f"DBCluster Endpoint: {_db_endpoint}") return _db_endpoint def get_db_reader_endpoint(self, aws_client: Optional[AwsApiClient] = None) -> Optional[str]: """Returns the DbCluster reader endpoint Returns: The DbCluster reader endpoint """ logger.debug(f"Getting endpoint for {self.get_resource_name()}") _db_endpoint: Optional[str] = None if self.active_resource: _db_endpoint = self.active_resource.get("ReaderEndpoint") if _db_endpoint is None: client: AwsApiClient = aws_client or self.get_aws_client() resource = self._read(aws_client=client) if resource is not None: _db_endpoint = resource.get("ReaderEndpoint") if _db_endpoint is None: resource = self.read_resource_from_file() if resource is not None: _db_endpoint = resource.get("ReaderEndpoint") logger.debug(f"DBCluster ReaderEndpoint: {_db_endpoint}") return _db_endpoint def get_db_port(self, aws_client: Optional[AwsApiClient] = None) -> Optional[str]: """Returns the DbCluster port Returns: The DbCluster port """ logger.debug(f"Getting port for {self.get_resource_name()}") _db_port: Optional[str] = None if self.active_resource: _db_port = self.active_resource.get("Port") if _db_port is None: client: AwsApiClient = aws_client or self.get_aws_client() resource = self._read(aws_client=client) if resource is not None: _db_port = resource.get("Port") if _db_port is None: resource = self.read_resource_from_file() if resource is not None: _db_port = resource.get("Port") logger.debug(f"DBCluster Port: {_db_port}") return _db_port