diff --git a/.github/workflows/tests_terraform_examples.yml b/.github/workflows/tests_terraform_examples.yml index d83ef22ccb21..26d59d99b4ce 100644 --- a/.github/workflows/tests_terraform_examples.yml +++ b/.github/workflows/tests_terraform_examples.yml @@ -45,6 +45,7 @@ jobs: docker run --rm -t --name motoserver -e TEST_SERVER_MODE=true -e AWS_SECRET_ACCESS_KEY=server_secret -e AWS_ACCESS_KEY_ID=server_key -v `pwd`:/moto -p 5000:5000 -v /var/run/docker.sock:/var/run/docker.sock python:3.10-slim /moto/scripts/ci_moto_server.sh & python scripts/ci_wait_for_server.py - name: Run tests + if: ${{ matrix.service != 'rds' }} run: | mkdir ~/.aws && touch ~/.aws/credentials && echo -e "[default]\naws_access_key_id = test\naws_secret_access_key = test" > ~/.aws/credentials cd other_langs/terraform/${{ matrix.service }} @@ -56,3 +57,11 @@ jobs: sleep 30 terraform plan -detailed-exitcode terraform apply -destroy --auto-approve + - name: Run tests + if: ${{ matrix.service == 'rds' }} + run: | + mkdir ~/.aws && touch ~/.aws/credentials && echo -e "[default]\naws_access_key_id = test\naws_secret_access_key = test" > ~/.aws/credentials + cd other_langs/terraform/${{ matrix.service }} + terraform init + terraform apply --auto-approve + terraform apply -destroy --auto-approve diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index ffdc5862e121..f80ef52af228 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -6460,7 +6460,7 @@ ## rds
-38% implemented +42% implemented - [ ] add_role_to_db_cluster - [ ] add_role_to_db_instance @@ -6505,7 +6505,7 @@ - [X] delete_db_instance - [ ] delete_db_instance_automated_backup - [X] delete_db_parameter_group -- [ ] delete_db_proxy +- [X] delete_db_proxy - [ ] delete_db_proxy_endpoint - [ ] delete_db_security_group - [ ] delete_db_shard_group @@ -6516,7 +6516,7 @@ - [ ] delete_integration - [X] delete_option_group - [ ] delete_tenant_database -- [ ] deregister_db_proxy_targets +- [X] deregister_db_proxy_targets - [ ] describe_account_attributes - [ ] describe_blue_green_deployments - [ ] describe_certificates @@ -6536,8 +6536,8 @@ - [ ] describe_db_parameters - [X] describe_db_proxies - [ ] describe_db_proxy_endpoints -- [ ] describe_db_proxy_target_groups -- [ ] describe_db_proxy_targets +- [X] describe_db_proxy_target_groups +- [X] describe_db_proxy_targets - [ ] describe_db_recommendations - [ ] describe_db_security_groups - [ ] describe_db_shard_groups @@ -6580,7 +6580,7 @@ - [X] modify_db_parameter_group - [ ] modify_db_proxy - [ ] modify_db_proxy_endpoint -- [ ] modify_db_proxy_target_group +- [X] modify_db_proxy_target_group - [ ] modify_db_recommendation - [ ] modify_db_shard_group - [ ] modify_db_snapshot @@ -6597,7 +6597,7 @@ - [ ] reboot_db_cluster - [X] reboot_db_instance - [ ] reboot_db_shard_group -- [ ] register_db_proxy_targets +- [X] register_db_proxy_targets - [X] remove_from_global_cluster - [ ] remove_role_from_db_cluster - [ ] remove_role_from_db_instance diff --git a/docs/docs/services/rds.rst b/docs/docs/services/rds.rst index bc0f6ecc4c6f..f2a8bab77ff4 100644 --- a/docs/docs/services/rds.rst +++ b/docs/docs/services/rds.rst @@ -57,7 +57,7 @@ rds - [X] delete_db_instance - [ ] delete_db_instance_automated_backup - [X] delete_db_parameter_group -- [ ] delete_db_proxy +- [X] delete_db_proxy - [ ] delete_db_proxy_endpoint - [ ] delete_db_security_group - [ ] delete_db_shard_group @@ -68,7 +68,7 @@ rds - [ ] delete_integration - [X] delete_option_group - [ ] delete_tenant_database -- [ ] deregister_db_proxy_targets +- [X] deregister_db_proxy_targets - [ ] describe_account_attributes - [ ] describe_blue_green_deployments - [ ] describe_certificates @@ -92,8 +92,8 @@ rds - [ ] describe_db_proxy_endpoints -- [ ] describe_db_proxy_target_groups -- [ ] describe_db_proxy_targets +- [X] describe_db_proxy_target_groups +- [X] describe_db_proxy_targets - [ ] describe_db_recommendations - [ ] describe_db_security_groups - [ ] describe_db_shard_groups @@ -140,7 +140,7 @@ rds - [X] modify_db_parameter_group - [ ] modify_db_proxy - [ ] modify_db_proxy_endpoint -- [ ] modify_db_proxy_target_group +- [X] modify_db_proxy_target_group - [ ] modify_db_recommendation - [ ] modify_db_shard_group - [ ] modify_db_snapshot @@ -157,7 +157,7 @@ rds - [ ] reboot_db_cluster - [X] reboot_db_instance - [ ] reboot_db_shard_group -- [ ] register_db_proxy_targets +- [X] register_db_proxy_targets - [X] remove_from_global_cluster - [ ] remove_role_from_db_cluster - [ ] remove_role_from_db_instance diff --git a/moto/rds/models.py b/moto/rds/models.py index 2a768a91a486..51229c102ea9 100644 --- a/moto/rds/models.py +++ b/moto/rds/models.py @@ -1,4 +1,5 @@ import copy +import math import os import re import string @@ -121,6 +122,71 @@ def arn(self) -> str: return f"arn:{self.partition}:rds:{self.region}:{self.account_id}:{self.resource_type}:{self.name}" +class ProxyTarget(RDSBaseModel): + resource_type = "proxy-target" + + def __init__( + self, + backend: "RDSBackend", + resource_id: str, + endpoint: Optional[str], + type: str, + ): + super().__init__(backend) + self.endpoint = endpoint + self.rds_resource_id = resource_id + self.type = type + self.role = "" + + +class ProxyTargetGroup(RDSBaseModel): + resource_type = "target-group" + + def __init__( + self, + backend: "RDSBackend", + name: str, + proxy_name: str, + ): + super().__init__(backend) + self._name = f"prx-tg-{random.get_random_string(length=17, lower_case=True)}" + self.group_name = name + self.proxy_name = proxy_name + self.targets: List[ProxyTarget] = [] + + self.max_connections = 100 + self.max_idle_connections = 50 + self.borrow_timeout = 120 + self.session_pinning_filters: List[str] = [] + + self.created_date = iso_8601_datetime_with_milliseconds() + self.updated_date = iso_8601_datetime_with_milliseconds() + + @property + def name(self) -> str: + return self._name + + def to_xml(self) -> str: + template = Template("""{{ group.proxy_name }} + {{ group.group_name }} + {{ group.arn }} + true + available + + {{ group.max_connections }} + {{ group.max_idle_connections }} + {{ group.borrow_timeout }} + + {% for filter in group.session_pinning_filters %} + {{ filter }} + {% endfor %} + + + {{ group.created_date }} + {{ group.updated_date }}""") + return template.render(group=self) + + class GlobalCluster(RDSBaseModel): resource_type = "global-cluster" @@ -729,7 +795,7 @@ def __init__( self.instance_create_time = iso_8601_datetime_with_milliseconds() self.publicly_accessible = kwargs.get("publicly_accessible") if self.publicly_accessible is None: - self.publicly_accessible = True + self.publicly_accessible = False self.copy_tags_to_snapshot = kwargs.get("copy_tags_to_snapshot") if self.copy_tags_to_snapshot is None: self.copy_tags_to_snapshot = False @@ -750,6 +816,11 @@ def __init__( ].describe_db_subnet_groups(self.db_subnet_group_name)[0] self.security_groups = kwargs.get("security_groups", []) self.vpc_security_group_ids = kwargs.get("vpc_security_group_ids", []) + if not self.vpc_security_group_ids: + ec2_backend = ec2_backends[self.account_id][self.region] + default_vpc = ec2_backend.default_vpc + default_sg = ec2_backend.get_default_security_group(default_vpc.id) + self.vpc_security_group_ids.append(default_sg.id) # type: ignore self.preferred_maintenance_window = kwargs.get("preferred_maintenance_window") self.preferred_backup_window = kwargs.get("preferred_backup_window") msg = valid_preferred_maintenance_window( @@ -1550,7 +1621,7 @@ def __init__( self.auth = auth self.role_arn = role_arn self.vpc_subnet_ids = vpc_subnet_ids - self.vpc_security_group_ids = vpc_security_group_ids + self.vpc_security_group_ids = vpc_security_group_ids or [] self.require_tls = require_tls if idle_client_timeout is None: self.idle_client_timeout = 1800 @@ -1577,6 +1648,9 @@ def __init__( vpcs.append(subnet.vpc_id) if subnet.vpc_id != vpcs[0]: raise InvalidSubnet(subnet_identifier=subnet.id) + if not self.vpc_security_group_ids: + default_sg = ec2_backend.get_default_security_group(vpcs[0]) + self.vpc_security_group_ids.append(default_sg.id) # type: ignore self.vpc_id = ec2_backend.describe_subnets(subnet_ids=[self.vpc_subnet_ids[0]])[ 0 @@ -1587,18 +1661,30 @@ def __init__( ) self.endpoint = f"{self.db_proxy_name}.db-proxy-{self.url_identifier}.{self.region}.rds.amazonaws.com" + self.proxy_target_groups = { + "default": ProxyTargetGroup( + backend=self.backend, name="default", proxy_name=db_proxy_name + ) + } + + self.unique_id = f"prx-{random.get_random_string(17, lower_case=True)}" + @property def name(self) -> str: return self.db_proxy_name + @property + def arn(self) -> str: + return f"arn:{self.partition}:rds:{self.region}:{self.account_id}:{self.resource_type}:{self.unique_id}" + def to_xml(self) -> str: template = Template( """ {{ dbproxy.require_tls }} - {% if dbproxy.VpcSecurityGroupIds %} - {% for vpcsecuritygroupid in dbproxy.VpcSecurityGroupIds %} - {{ vpcsecuritygroupid }} + {% if dbproxy.vpc_security_group_ids %} + {% for sg in dbproxy.vpc_security_group_ids %} + {{ sg }} {% endfor %} {% endif %} @@ -2558,6 +2644,13 @@ def _find_resource(self, resource_type: str, resource_name: str) -> Any: if resource_type == getattr(resource_class, "resource_type", ""): if resource_name in resources: # type: ignore return resources[resource_name] # type: ignore + # The resource_name is the last part of the ARN + # Usually that's the name - but for DBProxies, the last part of the ARN is a random identifier + # So we can't just use the dict-keys - we have to manually check the ARN + if resource_type == "db-proxy": + for resource in self.db_proxies.values(): + if resource.arn.endswith(resource_name): + return resource def list_tags_for_resource(self, arn: str) -> List[Dict[str, str]]: if self.arn_regex.match(arn): @@ -2869,6 +2962,86 @@ def describe_db_proxies( raise DBProxyNotFoundFault(db_proxy_name) return db_proxies + def deregister_db_proxy_targets( + self, + db_proxy_name: str, + target_group_name: str, + db_cluster_identifiers: List[str], + db_instance_identifiers: List[str], + ) -> None: + db_proxy = self.db_proxies[db_proxy_name] + target_group = db_proxy.proxy_target_groups[target_group_name or "default"] + target_group.targets = [ + t + for t in target_group.targets + if t.rds_resource_id not in db_cluster_identifiers + and t.rds_resource_id not in db_instance_identifiers + ] + + def register_db_proxy_targets( + self, + db_proxy_name: str, + target_group_name: str, + db_cluster_identifiers: List[str], + db_instance_identifiers: List[str], + ) -> List[ProxyTarget]: + db_proxy = self.db_proxies[db_proxy_name] + target_group = db_proxy.proxy_target_groups[target_group_name or "default"] + new_targets = [] + for cluster_id in db_cluster_identifiers: + cluster = self.clusters[cluster_id] + target = ProxyTarget( + backend=self, + resource_id=cluster_id, + endpoint=cluster.endpoint, + type="TRACKED_CLUSTER", + ) + new_targets.append(target) + for instance_id in db_instance_identifiers: + target = ProxyTarget( + backend=self, + resource_id=instance_id, + endpoint=None, + type="RDS_INSTANCE", + ) + new_targets.append(target) + target_group.targets.extend(new_targets) + return new_targets + + def delete_db_proxy(self, proxy_name: str) -> DBProxy: + return self.db_proxies.pop(proxy_name) + + def describe_db_proxy_targets(self, proxy_name: str) -> List[ProxyTarget]: + proxy = self.db_proxies[proxy_name] + target_group = proxy.proxy_target_groups["default"] + return target_group.targets + + def describe_db_proxy_target_groups( + self, proxy_name: str + ) -> List[ProxyTargetGroup]: + proxy = self.db_proxies[proxy_name] + return list(proxy.proxy_target_groups.values()) + + def modify_db_proxy_target_group( + self, proxy_name: str, config: Dict[str, Any] + ) -> ProxyTargetGroup: + proxy = self.db_proxies[proxy_name] + target_group = proxy.proxy_target_groups["default"] + if max_connections := config.get("MaxConnectionsPercent"): + target_group.max_connections = max_connections + if max_idle := config.get("MaxIdleConnectionsPercent"): + target_group.max_idle_connections = max_idle + else: + target_group.max_idle_connections = math.floor( + int(target_group.max_connections) / 2 + ) + target_group.borrow_timeout = config.get( + "ConnectionBorrowTimeout", target_group.borrow_timeout + ) + if "SessionPinningFilters" in config: + target_group.session_pinning_filters = config["SessionPinningFilters"] + return target_group + class OptionGroup(RDSBaseModel): resource_type = "og" diff --git a/moto/rds/responses.py b/moto/rds/responses.py index 8be586ff009f..7b8911369c9c 100644 --- a/moto/rds/responses.py +++ b/moto/rds/responses.py @@ -24,7 +24,7 @@ def _get_db_kwargs(self) -> Dict[str, Any]: "allocated_storage": self._get_int_param("AllocatedStorage"), "availability_zone": self._get_param("AvailabilityZone"), "backup_retention_period": self._get_param("BackupRetentionPeriod"), - "copy_tags_to_snapshot": self._get_param("CopyTagsToSnapshot"), + "copy_tags_to_snapshot": self._get_bool_param("CopyTagsToSnapshot"), "db_instance_class": self._get_param("DBInstanceClass"), "db_cluster_identifier": self._get_param("DBClusterIdentifier"), "db_instance_identifier": self._get_param("DBInstanceIdentifier"), @@ -57,7 +57,7 @@ def _get_db_kwargs(self) -> Dict[str, Any]: "preferred_maintenance_window": self._get_param( "PreferredMaintenanceWindow", "wed:06:38-wed:07:08" ).lower(), - "publicly_accessible": self._get_param("PubliclyAccessible"), + "publicly_accessible": self._get_bool_param("PubliclyAccessible"), "security_groups": self._get_multi_param( "DBSecurityGroups.DBSecurityGroupName" ), @@ -79,7 +79,7 @@ def _get_modify_db_cluster_kwargs(self) -> Dict[str, Any]: "availability_zone": self._get_param("AvailabilityZone"), "backup_retention_period": self._get_param("BackupRetentionPeriod"), "backtrack_window": self._get_param("BacktrackWindow"), - "copy_tags_to_snapshot": self._get_param("CopyTagsToSnapshot"), + "copy_tags_to_snapshot": self._get_bool_param("CopyTagsToSnapshot"), "db_instance_class": self._get_param("DBInstanceClass"), "db_cluster_identifier": self._get_param("DBClusterIdentifier"), "new_db_cluster_identifier": self._get_param("NewDBClusterIdentifier"), @@ -112,7 +112,7 @@ def _get_modify_db_cluster_kwargs(self) -> Dict[str, Any]: "preferred_maintenance_window": self._get_param( "PreferredMaintenanceWindow" ), - "publicly_accessible": self._get_param("PubliclyAccessible"), + "publicly_accessible": self._get_bool_param("PubliclyAccessible"), "security_groups": self._get_multi_param( "DBSecurityGroups.DBSecurityGroupName" ), @@ -137,7 +137,7 @@ def _get_db_replica_kwargs(self) -> Dict[str, Any]: "iops": self._get_int_param("Iops"), # OptionGroupName "port": self._get_param("Port"), - "publicly_accessible": self._get_param("PubliclyAccessible"), + "publicly_accessible": self._get_bool_param("PubliclyAccessible"), "source_db_identifier": self._get_param("SourceDBInstanceIdentifier"), "storage_type": self._get_param("StorageType"), } @@ -869,6 +869,63 @@ def create_db_proxy(self) -> str: template = self.response_template(CREATE_DB_PROXY_TEMPLATE) return template.render(dbproxy=db_proxy) + def register_db_proxy_targets(self) -> str: + db_proxy_name = self._get_param("DBProxyName") + target_group_name = self._get_param("TargetGroupName") + db_cluster_identifiers = self._get_params().get("DBClusterIdentifiers", []) + db_instance_identifiers = self._get_params().get("DBInstanceIdentifiers", []) + targets = self.backend.register_db_proxy_targets( + db_proxy_name=db_proxy_name, + target_group_name=target_group_name, + db_cluster_identifiers=db_cluster_identifiers, + db_instance_identifiers=db_instance_identifiers, + ) + + template = self.response_template(REGISTER_DB_PROXY_TARGET) + return template.render(targets=targets) + + def deregister_db_proxy_targets(self) -> str: + db_proxy_name = self._get_param("DBProxyName") + target_group_name = self._get_param("TargetGroupName") + db_cluster_identifiers = self._get_params().get("DBClusterIdentifiers", []) + db_instance_identifiers = self._get_params().get("DBInstanceIdentifiers", []) + self.backend.deregister_db_proxy_targets( + db_proxy_name=db_proxy_name, + target_group_name=target_group_name, + db_cluster_identifiers=db_cluster_identifiers, + db_instance_identifiers=db_instance_identifiers, + ) + + template = self.response_template(DEREGISTER_DB_PROXY_TARGET) + return template.render() + + def describe_db_proxy_targets(self) -> str: + proxy_name = self._get_param("DBProxyName") + targets = self.backend.describe_db_proxy_targets(proxy_name=proxy_name) + template = self.response_template(DESCRIBE_DB_PROXY_TARGETS) + return template.render(targets=targets) + + def delete_db_proxy(self) -> str: + proxy_name = self._get_param("DBProxyName") + proxy = self.backend.delete_db_proxy(proxy_name=proxy_name) + template = self.response_template(DELETE_DB_PROXY_TEMPLATE) + return template.render(dbproxy=proxy) + + def describe_db_proxy_target_groups(self) -> str: + proxy_name = self._get_param("DBProxyName") + groups = self.backend.describe_db_proxy_target_groups(proxy_name=proxy_name) + template = self.response_template(DESCRIBE_DB_PROXY_TARGET_GROUPS) + return template.render(groups=groups) + + def modify_db_proxy_target_group(self) -> str: + proxy_name = self._get_param("DBProxyName") + config = self._get_params().get("ConnectionPoolConfig", {}) + group = self.backend.modify_db_proxy_target_group( + proxy_name=proxy_name, config=config + ) + template = self.response_template(MODIFY_DB_PROXY_TARGET_GROUP) + return template.render(group=group) + def _paginate(self, resources: List[Any]) -> Tuple[List[Any], Optional[str]]: from moto.rds.exceptions import InvalidParameterValue @@ -1717,3 +1774,79 @@ def _paginate(self, resources: List[Any]) -> Tuple[List[Any], Optional[str]]: {% endif %} """ + + +DEREGISTER_DB_PROXY_TARGET = """ + + +""" + + +REGISTER_DB_PROXY_TARGET = """ + + + {% for target in targets %} + + {{ target.rds_resource_id }} + 5432 + {{ target.type }} + + REGISTERING + + {% if target.endpoint %}{{ target.endpoint }}{% endif %} + + {% endfor %} + + +""" + + +DESCRIBE_DB_PROXY_TARGETS = """ + + + {% for target in targets %} + + {{ target.rds_resource_id }} + 5432 + {{ target.type }} + + AVAILABLE + + {% if target.endpoint %}{{ target.endpoint }}{% endif %} + + {% endfor %} + + + +""" + + +DELETE_DB_PROXY_TEMPLATE = """ + + + {{ dbproxy.to_xml() }} + + +""" + + +DESCRIBE_DB_PROXY_TARGET_GROUPS = """ + + + {% for group in groups %} + + {{ group.to_xml() }} + + {% endfor %} + + +""" + + +MODIFY_DB_PROXY_TARGET_GROUP = """ + + + {{ group.to_xml() }} + + +""" diff --git a/other_langs/terraform/rds/providers.tf b/other_langs/terraform/rds/providers.tf index 2da69969bf08..228ab7576a25 100644 --- a/other_langs/terraform/rds/providers.tf +++ b/other_langs/terraform/rds/providers.tf @@ -6,7 +6,10 @@ provider "aws" { skip_requesting_account_id = true endpoints { - rds = "http://localhost:5000" + ec2 = "http://localhost:5000" + iam = "http://localhost:5000" + rds = "http://localhost:5000" + secretsmanager = "http://localhost:5000" } access_key = "my-access-key" diff --git a/other_langs/terraform/rds/proxy.tf b/other_langs/terraform/rds/proxy.tf new file mode 100644 index 000000000000..22912af702da --- /dev/null +++ b/other_langs/terraform/rds/proxy.tf @@ -0,0 +1,94 @@ +data "aws_vpc" "default" { + default = true +} + +data "aws_subnets" "all" { + filter { + name = "vpc-id" + values = [data.aws_vpc.default.id] + } +} + +resource "random_string" "this" { + length = 20 + special = true + upper = true +} + +resource "aws_secretsmanager_secret" "test" { + name = "rds/test/test_admin" +} + +resource "aws_secretsmanager_secret_version" "test" { + secret_id = aws_secretsmanager_secret.test.id + secret_string = jsonencode({ + username = "test_admin" + password = "${random_string.this.result}" + }) +} + +data "aws_iam_policy_document" "rds_trust_policy" { + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["rds.amazonaws.com"] + } + + actions = [ + "sts:AssumeRole", + ] + } +} + +resource "aws_iam_role" "proxy" { + assume_role_policy = data.aws_iam_policy_document.rds_trust_policy.json + name = "rds-proxy-${aws_db_instance.test.db_name}" +} + +resource "aws_db_instance" "test" { + allocated_storage = 10 + db_name = "testdb" + engine = "postgres" + engine_version = "15" + instance_class = "db.t3.micro" + username = "test" + password = random_string.this.result + skip_final_snapshot = true +} + +resource "aws_db_proxy" "main" { + name = aws_db_instance.test.db_name + debug_logging = false + engine_family = "POSTGRESQL" + idle_client_timeout = 1800 + require_tls = true + role_arn = aws_iam_role.proxy.arn + vpc_security_group_ids = [] + vpc_subnet_ids = data.aws_subnets.all.ids + + auth { + auth_scheme = "SECRETS" + description = "Secrets through IAM Authentication" + iam_auth = "REQUIRED" + secret_arn = aws_secretsmanager_secret.test.arn + } +} + +resource "aws_db_proxy_default_target_group" "main" { + db_proxy_name = aws_db_proxy.main.name + + connection_pool_config { + connection_borrow_timeout = 120 + max_connections_percent = 100 + max_idle_connections_percent = 50 + session_pinning_filters = ["EXCLUDE_VARIABLE_SETS"] + } +} + +resource "aws_db_proxy_target" "main" { + db_instance_identifier = aws_db_instance.test.identifier + db_proxy_name = aws_db_proxy.main.name + target_group_name = aws_db_proxy_default_target_group.main.name +} diff --git a/tests/test_rds/test_rds_proxy.py b/tests/test_rds/test_rds_proxy.py index 602289a5e7e6..b2fe6aee5c07 100644 --- a/tests/test_rds/test_rds_proxy.py +++ b/tests/test_rds/test_rds_proxy.py @@ -39,14 +39,12 @@ def test_create_db_proxy(): ) db_proxy = resp["DBProxy"] assert db_proxy["DBProxyName"] == "testrdsproxy" - assert ( - db_proxy["DBProxyArn"] - == f"arn:aws:rds:us-west-2:{ACCOUNT_ID}:db-proxy:testrdsproxy" + assert db_proxy["DBProxyArn"].startswith( + f"arn:aws:rds:us-west-2:{ACCOUNT_ID}:db-proxy:" ) assert db_proxy["Status"] == "available" assert db_proxy["EngineFamily"] == "MYSQL" assert db_proxy["VpcId"] == vpc_id - assert db_proxy["VpcSecurityGroupIds"] == [] assert db_proxy["VpcSubnetIds"] == [subnet_id, subnet_id_2] assert db_proxy["Auth"] == [ { @@ -99,14 +97,12 @@ def test_describe_db_proxies(): response = rds_client.describe_db_proxies(DBProxyName="testrdsproxydescribe") db_proxy = response["DBProxies"][0] assert db_proxy["DBProxyName"] == "testrdsproxydescribe" - assert ( - db_proxy["DBProxyArn"] - == f"arn:aws:rds:us-west-2:{ACCOUNT_ID}:db-proxy:testrdsproxydescribe" + assert db_proxy["DBProxyArn"].startswith( + f"arn:aws:rds:us-west-2:{ACCOUNT_ID}:db-proxy:prx-" ) assert db_proxy["Status"] == "available" assert db_proxy["EngineFamily"] == "MYSQL" assert db_proxy["VpcId"] == vpc_id - assert db_proxy["VpcSecurityGroupIds"] == [] assert db_proxy["VpcSubnetIds"] == [subnet_id, subnet_id_2] assert db_proxy["Auth"] == [ { diff --git a/tests/test_rds/test_rds_proxy_target_groups.py b/tests/test_rds/test_rds_proxy_target_groups.py new file mode 100644 index 000000000000..a38b70617b82 --- /dev/null +++ b/tests/test_rds/test_rds_proxy_target_groups.py @@ -0,0 +1,245 @@ +import json +from time import sleep +from uuid import uuid4 + +import boto3 +import pytest +from botocore.exceptions import ClientError + +from moto import mock_aws +from tests import allow_aws_request + +DEFAULT_REGION = "us-east-1" + +ASSUME_ROLE_POLICY = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "rds.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], +} + + +@pytest.fixture(scope="module") +def mockaws(): + if allow_aws_request(): + yield + else: + with mock_aws(): + yield + + +@pytest.fixture(scope="module") +def secrets_arn(): + client = boto3.client("secretsmanager", DEFAULT_REGION) + secret_name = f"moto-test-{str(uuid4())[0:6]}" + + try: + secret = client.create_secret(Name=secret_name, SecretString="ss") + yield secret["ARN"] + finally: + client.delete_secret(SecretId=secret_name) + + +@pytest.fixture(scope="module") +def vpc_id(mockaws): # pylint: disable=redefined-outer-name + ec2_client = boto3.client("ec2", region_name=DEFAULT_REGION) + _vpc_id = None + try: + _vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"] + yield _vpc_id + finally: + ec2_client.delete_vpc(VpcId=_vpc_id) + + +@pytest.fixture(scope="module") +def subnet_id1(vpc_id): # pylint: disable=redefined-outer-name + ec2_client = boto3.client("ec2", region_name=DEFAULT_REGION) + try: + subnet = ec2_client.create_subnet( + CidrBlock="10.0.1.0/24", VpcId=vpc_id, AvailabilityZone="us-east-1a" + ) + subnet_id = subnet["Subnet"]["SubnetId"] + yield subnet_id + finally: + ec2_client.delete_subnet(SubnetId=subnet_id) + + +@pytest.fixture(scope="module") +def subnet_id2(vpc_id): # pylint: disable=redefined-outer-name + ec2_client = boto3.client("ec2", region_name=DEFAULT_REGION) + try: + subnet = ec2_client.create_subnet( + CidrBlock="10.0.2.0/24", VpcId=vpc_id, AvailabilityZone="us-east-1b" + ) + subnet_id = subnet["Subnet"]["SubnetId"] + yield subnet_id + finally: + ec2_client.delete_subnet(SubnetId=subnet_id) + + +@pytest.fixture(scope="module") +def role_arn(mockaws): # pylint: disable=redefined-outer-name + role_name = f"moto-test-{str(uuid4())[0:6]}" + iam = boto3.client("iam", region_name=DEFAULT_REGION) + try: + role = iam.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=json.dumps(ASSUME_ROLE_POLICY), + )["Role"] + yield role["Arn"] + finally: + iam.delete_role(RoleName=role_name) + + +@pytest.fixture(scope="module") +def proxy_name(subnet_id1, subnet_id2, role_arn, secrets_arn): # pylint: disable=redefined-outer-name + name = f"moto-test-{str(uuid4())[0:6]}" + rds_client = boto3.client("rds", region_name=DEFAULT_REGION) + proxy_creation_succeeded = False + try: + resp = rds_client.create_db_proxy( + DBProxyName=name, + EngineFamily="POSTGRESQL", + Auth=[ + { + # "UserName": "user1", + # "AuthScheme": "SECRETS", + "IAMAuth": "DISABLED", + "SecretArn": secrets_arn, + } + ], + RoleArn=role_arn, + VpcSubnetIds=[subnet_id1, subnet_id2], + ) + status = resp["DBProxy"]["Status"] + while status.lower() == "creating": + sleep(5) + status = rds_client.describe_db_proxies(DBProxyName=name)["DBProxies"][0][ + "Status" + ] + proxy_creation_succeeded = True + + yield name + finally: + if proxy_creation_succeeded: + rds_client.delete_db_proxy(DBProxyName=name) + deleted = False + while not deleted: + try: + rds_client.describe_db_proxies(DBProxyName=name) + sleep(10) + except (ClientError, IndexError): + deleted = True + + +@pytest.fixture(scope="module") +def db_cluster_id(mockaws): # pylint: disable=redefined-outer-name + cluster_name = f"moto-test-{str(uuid4())[0:6]}" + rds_client = boto3.client("rds", region_name=DEFAULT_REGION) + cluster_creation_succeeded = False + try: + cluster_id = rds_client.create_db_cluster( + DBClusterIdentifier=cluster_name, + Engine="aurora-postgresql", + MasterUsername="root", + MasterUserPassword="hunter21", + )["DBCluster"]["DBClusterIdentifier"] + rds_client.get_waiter("db_cluster_available").wait( + DBClusterIdentifier=cluster_id + ) + cluster_creation_succeeded = True + yield cluster_id + finally: + if cluster_creation_succeeded: + rds_client.delete_db_cluster( + DBClusterIdentifier=cluster_id, SkipFinalSnapshot=True + ) + + +def test_default_proxy_targets(account_id, proxy_name): # pylint: disable=redefined-outer-name + rds_client = boto3.client("rds", region_name=DEFAULT_REGION) + resp = rds_client.describe_db_proxy_targets(DBProxyName=proxy_name) + + assert resp["Targets"] == [] + + groups = rds_client.describe_db_proxy_target_groups( + DBProxyName=proxy_name, + TargetGroupName="default", + )["TargetGroups"] + assert len(groups) == 1 + groups[0].pop("CreatedDate") + groups[0].pop("UpdatedDate") + target_group_arn = groups[0].pop("TargetGroupArn") + assert target_group_arn.startswith( + f"arn:aws:rds:us-east-1:{account_id}:target-group:prx-tg-" + ) # 17 more chars (lowercase + digits) + assert groups[0] == { + "DBProxyName": proxy_name, + "TargetGroupName": "default", + "IsDefault": True, + "Status": "available", + "ConnectionPoolConfig": { + "MaxConnectionsPercent": 100, + "MaxIdleConnectionsPercent": 50, + "ConnectionBorrowTimeout": 120, + "SessionPinningFilters": [], + }, + } + + +def test_register_db_proxy(account_id, proxy_name, db_cluster_id): # pylint: disable=redefined-outer-name + rds_client = boto3.client("rds", region_name=DEFAULT_REGION) + + resp = rds_client.register_db_proxy_targets( + DBProxyName=proxy_name, + DBClusterIdentifiers=[db_cluster_id], + )["DBProxyTargets"] + resp[0].pop("Endpoint", None) + assert resp == [ + { + "RdsResourceId": db_cluster_id, + "Port": 5432, + "Type": "TRACKED_CLUSTER", + "TargetHealth": {"State": "REGISTERING"}, + } + ] + + resp = rds_client.describe_db_proxy_targets(DBProxyName=proxy_name)["Targets"] + resp[0].pop("Endpoint", None) + resp[0].pop("TargetHealth", None) + assert resp == [ + {"RdsResourceId": db_cluster_id, "Port": 5432, "Type": "TRACKED_CLUSTER"} + ] + + rds_client.deregister_db_proxy_targets( + DBProxyName=proxy_name, + DBClusterIdentifiers=[db_cluster_id], + ) + + resp = rds_client.describe_db_proxy_targets(DBProxyName=proxy_name) + assert resp["Targets"] == [] + + +def test_modify_group(proxy_name): # pylint: disable=redefined-outer-name + rds_client = boto3.client("rds", region_name=DEFAULT_REGION) + + rds_client.modify_db_proxy_target_group( + TargetGroupName="default", + DBProxyName=proxy_name, + ConnectionPoolConfig={ + "MaxConnectionsPercent": 27, + "SessionPinningFilters": ["filter1"], + }, + ) + + group = rds_client.describe_db_proxy_target_groups( + DBProxyName=proxy_name, + TargetGroupName="default", + )["TargetGroups"][0] + assert group["ConnectionPoolConfig"]["MaxConnectionsPercent"] == 27 + assert group["ConnectionPoolConfig"]["MaxIdleConnectionsPercent"] == 13 + assert group["ConnectionPoolConfig"]["SessionPinningFilters"] == ["filter1"]