Route53Resolver: Add resolver rule association-related APIs (#4611)
This commit is contained in:
parent
e4e58f777d
commit
74666c1271
@ -3703,12 +3703,12 @@
|
||||
|
||||
## route53resolver
|
||||
<details>
|
||||
<summary>21% implemented</summary>
|
||||
<summary>27% implemented</summary>
|
||||
|
||||
- [ ] associate_firewall_rule_group
|
||||
- [ ] associate_resolver_endpoint_ip_address
|
||||
- [ ] associate_resolver_query_log_config
|
||||
- [ ] associate_resolver_rule
|
||||
- [X] associate_resolver_rule
|
||||
- [ ] create_firewall_domain_list
|
||||
- [ ] create_firewall_rule
|
||||
- [ ] create_firewall_rule_group
|
||||
@ -3724,7 +3724,7 @@
|
||||
- [ ] disassociate_firewall_rule_group
|
||||
- [ ] disassociate_resolver_endpoint_ip_address
|
||||
- [ ] disassociate_resolver_query_log_config
|
||||
- [ ] disassociate_resolver_rule
|
||||
- [X] disassociate_resolver_rule
|
||||
- [ ] get_firewall_config
|
||||
- [ ] get_firewall_domain_list
|
||||
- [ ] get_firewall_rule_group
|
||||
@ -3737,7 +3737,7 @@
|
||||
- [ ] get_resolver_query_log_config_association
|
||||
- [ ] get_resolver_query_log_config_policy
|
||||
- [X] get_resolver_rule
|
||||
- [ ] get_resolver_rule_association
|
||||
- [X] get_resolver_rule_association
|
||||
- [ ] get_resolver_rule_policy
|
||||
- [ ] import_firewall_domains
|
||||
- [ ] list_firewall_configs
|
||||
@ -3752,7 +3752,7 @@
|
||||
- [X] list_resolver_endpoints
|
||||
- [ ] list_resolver_query_log_config_associations
|
||||
- [ ] list_resolver_query_log_configs
|
||||
- [ ] list_resolver_rule_associations
|
||||
- [X] list_resolver_rule_associations
|
||||
- [X] list_resolver_rules
|
||||
- [X] list_tags_for_resource
|
||||
- [ ] put_firewall_rule_group_policy
|
||||
|
@ -30,7 +30,7 @@ route53resolver
|
||||
- [ ] associate_firewall_rule_group
|
||||
- [ ] associate_resolver_endpoint_ip_address
|
||||
- [ ] associate_resolver_query_log_config
|
||||
- [ ] associate_resolver_rule
|
||||
- [X] associate_resolver_rule
|
||||
- [ ] create_firewall_domain_list
|
||||
- [ ] create_firewall_rule
|
||||
- [ ] create_firewall_rule_group
|
||||
@ -54,7 +54,7 @@ route53resolver
|
||||
- [ ] disassociate_firewall_rule_group
|
||||
- [ ] disassociate_resolver_endpoint_ip_address
|
||||
- [ ] disassociate_resolver_query_log_config
|
||||
- [ ] disassociate_resolver_rule
|
||||
- [X] disassociate_resolver_rule
|
||||
- [ ] get_firewall_config
|
||||
- [ ] get_firewall_domain_list
|
||||
- [ ] get_firewall_rule_group
|
||||
@ -69,7 +69,7 @@ route53resolver
|
||||
- [ ] get_resolver_query_log_config_association
|
||||
- [ ] get_resolver_query_log_config_policy
|
||||
- [X] get_resolver_rule
|
||||
- [ ] get_resolver_rule_association
|
||||
- [X] get_resolver_rule_association
|
||||
- [ ] get_resolver_rule_policy
|
||||
- [ ] import_firewall_domains
|
||||
- [ ] list_firewall_configs
|
||||
@ -88,7 +88,7 @@ route53resolver
|
||||
|
||||
- [ ] list_resolver_query_log_config_associations
|
||||
- [ ] list_resolver_query_log_configs
|
||||
- [ ] list_resolver_rule_associations
|
||||
- [X] list_resolver_rule_associations
|
||||
- [X] list_resolver_rules
|
||||
- [X] list_tags_for_resource
|
||||
List all tags for the given resource.
|
||||
|
@ -77,6 +77,15 @@ class ResourceExistsException(JsonRESTError):
|
||||
super().__init__("ResourceExistsException", message)
|
||||
|
||||
|
||||
class ResourceInUseException(JsonRESTError):
|
||||
"""The resource has other resources associated with it."""
|
||||
|
||||
code = 400
|
||||
|
||||
def __init__(self, message):
|
||||
super().__init__("ResourceInUseException", message)
|
||||
|
||||
|
||||
class ResourceNotFoundException(JsonRESTError):
|
||||
"""The specified resource doesn't exist."""
|
||||
|
||||
|
@ -17,6 +17,7 @@ from moto.route53resolver.exceptions import (
|
||||
InvalidRequestException,
|
||||
LimitExceededException,
|
||||
ResourceExistsException,
|
||||
ResourceInUseException,
|
||||
ResourceNotFoundException,
|
||||
TagValidationException,
|
||||
)
|
||||
@ -29,6 +30,46 @@ from moto.utilities.tagging_service import TaggingService
|
||||
CAMEL_TO_SNAKE_PATTERN = re.compile(r"(?<!^)(?=[A-Z])")
|
||||
|
||||
|
||||
class ResolverRuleAssociation(BaseModel): # pylint: disable=too-few-public-methods
|
||||
"""Representation of a fake Route53 Resolver Rules Association."""
|
||||
|
||||
MAX_TAGS_PER_RESOLVER_ENDPOINT = 200
|
||||
MAX_RULE_ASSOCIATIONS_PER_REGION = 2000
|
||||
|
||||
# There are two styles of filter names and either will be transformed
|
||||
# into lowercase snake.
|
||||
FILTER_NAMES = [
|
||||
"name",
|
||||
"resolver_rule_id",
|
||||
"status",
|
||||
"vpc_id",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self, region, resolver_rule_association_id, resolver_rule_id, vpc_id, name=None
|
||||
): # pylint: disable=too-many-arguments
|
||||
self.region = region
|
||||
self.resolver_rule_id = resolver_rule_id
|
||||
self.name = name
|
||||
self.vpc_id = vpc_id
|
||||
|
||||
# Constructed members.
|
||||
self.id = resolver_rule_association_id # pylint: disable=invalid-name
|
||||
self.status = "COMPLETE"
|
||||
self.status_message = ""
|
||||
|
||||
def description(self):
|
||||
"""Return dictionary of relevant info for resolver rule association."""
|
||||
return {
|
||||
"Id": self.id,
|
||||
"ResolverRuleId": self.resolver_rule_id,
|
||||
"Name": self.name,
|
||||
"VPCId": self.vpc_id,
|
||||
"Status": self.status,
|
||||
"StatusMessage": self.status_message,
|
||||
}
|
||||
|
||||
|
||||
class ResolverRule(BaseModel): # pylint: disable=too-many-instance-attributes
|
||||
"""Representation of a fake Route53 Resolver Rule."""
|
||||
|
||||
@ -252,6 +293,7 @@ class Route53ResolverBackend(BaseBackend):
|
||||
self.region_name = region_name
|
||||
self.resolver_endpoints = {} # Key is self-generated ID (endpoint_id)
|
||||
self.resolver_rules = {} # Key is self-generated ID (rule_id)
|
||||
self.resolver_rule_associations = {} # Key is resolver_rule_association_id)
|
||||
self.tagger = TaggingService()
|
||||
|
||||
def reset(self):
|
||||
@ -267,6 +309,48 @@ class Route53ResolverBackend(BaseBackend):
|
||||
service_region, zones, "route53resolver"
|
||||
)
|
||||
|
||||
def associate_resolver_rule(self, region, resolver_rule_id, name, vpc_id):
|
||||
"""Return description for a newly created resolver rule association."""
|
||||
validate_args(
|
||||
[("resolverRuleId", resolver_rule_id), ("name", name), ("vPCId", vpc_id)]
|
||||
)
|
||||
|
||||
associations = [
|
||||
x for x in self.resolver_rule_associations.values() if x.region == region
|
||||
]
|
||||
if len(associations) > ResolverRuleAssociation.MAX_RULE_ASSOCIATIONS_PER_REGION:
|
||||
# This error message was not verified to be the same for AWS.
|
||||
raise LimitExceededException(
|
||||
f"Account '{ACCOUNT_ID}' has exceeded 'max-rule-association'"
|
||||
)
|
||||
|
||||
if resolver_rule_id not in self.resolver_rules:
|
||||
raise ResourceNotFoundException(
|
||||
f"Resolver rule with ID '{resolver_rule_id}' does not exist."
|
||||
)
|
||||
|
||||
vpcs = ec2_backends[region].describe_vpcs()
|
||||
if vpc_id not in [x.id for x in vpcs]:
|
||||
raise InvalidParameterException(f"The vpc ID '{vpc_id}' does not exist")
|
||||
|
||||
# Can't duplicate resolver rule, vpc id associations.
|
||||
for association in self.resolver_rule_associations.values():
|
||||
if (
|
||||
resolver_rule_id == association.resolver_rule_id
|
||||
and vpc_id == association.vpc_id
|
||||
):
|
||||
raise InvalidRequestException(
|
||||
f"Cannot associate rules with same domain name with same "
|
||||
f"VPC. Conflict with resolver rule '{resolver_rule_id}'"
|
||||
)
|
||||
|
||||
rule_association_id = f"rslvr-rrassoc-{get_random_hex(17)}"
|
||||
rule_association = ResolverRuleAssociation(
|
||||
region, rule_association_id, resolver_rule_id, vpc_id, name
|
||||
)
|
||||
self.resolver_rule_associations[rule_association_id] = rule_association
|
||||
return rule_association
|
||||
|
||||
@staticmethod
|
||||
def _verify_subnet_ips(region, ip_addresses):
|
||||
"""Perform additional checks on the IPAddresses.
|
||||
@ -493,6 +577,20 @@ class Route53ResolverBackend(BaseBackend):
|
||||
def delete_resolver_endpoint(self, resolver_endpoint_id):
|
||||
"""Delete a resolver endpoint."""
|
||||
self._validate_resolver_endpoint_id(resolver_endpoint_id)
|
||||
|
||||
# Can't delete an endpoint if there are rules associated with it.
|
||||
rules = [
|
||||
x.id
|
||||
for x in self.resolver_rules.values()
|
||||
if x.resolver_endpoint_id == resolver_endpoint_id
|
||||
]
|
||||
if rules:
|
||||
raise InvalidRequestException(
|
||||
f"Cannot delete resolver endpoint unless its related resolver "
|
||||
f"rules are deleted. The following rules still exist for "
|
||||
f"this resolver endpoint: {','.join(rules)}"
|
||||
)
|
||||
|
||||
self.tagger.delete_all_tags_for_resource(resolver_endpoint_id)
|
||||
resolver_endpoint = self.resolver_endpoints.pop(resolver_endpoint_id)
|
||||
resolver_endpoint.status = "DELETING"
|
||||
@ -512,6 +610,19 @@ class Route53ResolverBackend(BaseBackend):
|
||||
def delete_resolver_rule(self, resolver_rule_id):
|
||||
"""Delete a resolver rule."""
|
||||
self._validate_resolver_rule_id(resolver_rule_id)
|
||||
|
||||
# Can't delete an rule unless VPC's are disassociated.
|
||||
associations = [
|
||||
x.id
|
||||
for x in self.resolver_rule_associations.values()
|
||||
if x.resolver_rule_id == resolver_rule_id
|
||||
]
|
||||
if associations:
|
||||
raise ResourceInUseException(
|
||||
"Please disassociate this resolver rule from VPC first "
|
||||
"before deleting"
|
||||
)
|
||||
|
||||
self.tagger.delete_all_tags_for_resource(resolver_rule_id)
|
||||
resolver_rule = self.resolver_rules.pop(resolver_rule_id)
|
||||
resolver_rule.status = "DELETING"
|
||||
@ -520,6 +631,36 @@ class Route53ResolverBackend(BaseBackend):
|
||||
)
|
||||
return resolver_rule
|
||||
|
||||
def disassociate_resolver_rule(self, resolver_rule_id, vpc_id):
|
||||
"""Removes association between a resolver rule and a VPC."""
|
||||
validate_args([("resolverRuleId", resolver_rule_id), ("vPCId", vpc_id)])
|
||||
|
||||
# Non-existent rule or vpc ids?
|
||||
if resolver_rule_id not in self.resolver_rules:
|
||||
raise ResourceNotFoundException(
|
||||
f"Resolver rule with ID '{resolver_rule_id}' does not exist"
|
||||
)
|
||||
|
||||
# Find the matching association for this rule and vpc.
|
||||
rule_association_id = None
|
||||
for association in self.resolver_rule_associations.values():
|
||||
if (
|
||||
resolver_rule_id == association.resolver_rule_id
|
||||
and vpc_id == association.vpc_id
|
||||
):
|
||||
rule_association_id = association.id
|
||||
break
|
||||
else:
|
||||
raise ResourceNotFoundException(
|
||||
f"Resolver Rule Association between Resolver Rule "
|
||||
f"'{resolver_rule_id}' and VPC '{vpc_id}' does not exist"
|
||||
)
|
||||
|
||||
rule_association = self.resolver_rule_associations.pop(rule_association_id)
|
||||
rule_association.status = "DELETING"
|
||||
rule_association.status_message = "Deleting Association"
|
||||
return rule_association
|
||||
|
||||
def get_resolver_endpoint(self, resolver_endpoint_id):
|
||||
"""Return info for specified resolver endpoint."""
|
||||
self._validate_resolver_endpoint_id(resolver_endpoint_id)
|
||||
@ -530,6 +671,15 @@ class Route53ResolverBackend(BaseBackend):
|
||||
self._validate_resolver_rule_id(resolver_rule_id)
|
||||
return self.resolver_rules[resolver_rule_id]
|
||||
|
||||
def get_resolver_rule_association(self, resolver_rule_association_id):
|
||||
"""Return info for specified resolver rule association."""
|
||||
validate_args([("resolverRuleAssociationId", resolver_rule_association_id)])
|
||||
if resolver_rule_association_id not in self.resolver_rule_associations:
|
||||
raise ResourceNotFoundException(
|
||||
f"ResolverRuleAssociation '{resolver_rule_association_id}' does not Exist"
|
||||
)
|
||||
return self.resolver_rule_associations[resolver_rule_association_id]
|
||||
|
||||
@paginate(pagination_model=PAGINATION_MODEL)
|
||||
def list_resolver_endpoint_ip_addresses(
|
||||
self, resolver_endpoint_id, next_token=None, max_results=None,
|
||||
@ -550,10 +700,12 @@ class Route53ResolverBackend(BaseBackend):
|
||||
for rr_filter in filters:
|
||||
filter_name = rr_filter["Name"]
|
||||
if "_" not in filter_name:
|
||||
if filter_name == "HostVPCId":
|
||||
filter_name = "host_vpc_id"
|
||||
elif filter_name == "HostVpcId":
|
||||
if "Vpc" in filter_name:
|
||||
filter_name = "WRONG"
|
||||
elif filter_name == "HostVPCId":
|
||||
filter_name = "host_vpc_id"
|
||||
elif filter_name == "VPCId":
|
||||
filter_name = "vpc_id"
|
||||
elif filter_name in ["Type", "TYPE"]:
|
||||
filter_name = "rule_type"
|
||||
elif not filter_name.isupper():
|
||||
@ -624,6 +776,25 @@ class Route53ResolverBackend(BaseBackend):
|
||||
rules.append(rule)
|
||||
return rules
|
||||
|
||||
@paginate(pagination_model=PAGINATION_MODEL)
|
||||
def list_resolver_rule_associations(
|
||||
self, filters, next_token=None, max_results=None,
|
||||
): # pylint: disable=unused-argument
|
||||
"""List all resolver rule associations, using filters if specified."""
|
||||
if not filters:
|
||||
filters = []
|
||||
|
||||
self._add_field_name_to_filter(filters)
|
||||
self._validate_filters(filters, ResolverRuleAssociation.FILTER_NAMES)
|
||||
|
||||
rules = []
|
||||
for rule in sorted(
|
||||
self.resolver_rule_associations.values(), key=lambda x: x.name
|
||||
):
|
||||
if self._matches_all_filters(rule, filters):
|
||||
rules.append(rule)
|
||||
return rules
|
||||
|
||||
def _matched_arn(self, resource_arn):
|
||||
"""Given ARN, raise exception if there is no corresponding resource."""
|
||||
for resolver_endpoint in self.resolver_endpoints.values():
|
||||
|
@ -16,6 +16,21 @@ class Route53ResolverResponse(BaseResponse):
|
||||
"""Return backend instance specific for this region."""
|
||||
return route53resolver_backends[self.region]
|
||||
|
||||
def associate_resolver_rule(self):
|
||||
"""Associate a Resolver rule with a VPC."""
|
||||
resolver_rule_id = self._get_param("ResolverRuleId")
|
||||
name = self._get_param("Name")
|
||||
vpc_id = self._get_param("VPCId")
|
||||
resolver_rule_association = self.route53resolver_backend.associate_resolver_rule(
|
||||
region=self.region,
|
||||
resolver_rule_id=resolver_rule_id,
|
||||
name=name,
|
||||
vpc_id=vpc_id,
|
||||
)
|
||||
return json.dumps(
|
||||
{"ResolverRuleAssociation": resolver_rule_association.description()}
|
||||
)
|
||||
|
||||
def create_resolver_endpoint(self):
|
||||
"""Create an inbound or outbound Resolver endpoint."""
|
||||
creator_request_id = self._get_param("CreatorRequestId")
|
||||
@ -72,6 +87,17 @@ class Route53ResolverResponse(BaseResponse):
|
||||
)
|
||||
return json.dumps({"ResolverRule": resolver_rule.description()})
|
||||
|
||||
def disassociate_resolver_rule(self):
|
||||
"""Remove the association between a Resolver rule and a VPC."""
|
||||
vpc_id = self._get_param("VPCId")
|
||||
resolver_rule_id = self._get_param("ResolverRuleId")
|
||||
resolver_rule_association = self.route53resolver_backend.disassociate_resolver_rule(
|
||||
vpc_id=vpc_id, resolver_rule_id=resolver_rule_id,
|
||||
)
|
||||
return json.dumps(
|
||||
{"ResolverRuleAssociation": resolver_rule_association.description()}
|
||||
)
|
||||
|
||||
def get_resolver_endpoint(self):
|
||||
"""Return info about a specific Resolver endpoint."""
|
||||
resolver_endpoint_id = self._get_param("ResolverEndpointId")
|
||||
@ -88,6 +114,16 @@ class Route53ResolverResponse(BaseResponse):
|
||||
)
|
||||
return json.dumps({"ResolverRule": resolver_rule.description()})
|
||||
|
||||
def get_resolver_rule_association(self):
|
||||
"""Return info about association between a Resolver rule and a VPC."""
|
||||
resolver_rule_association_id = self._get_param("ResolverRuleAssociationId")
|
||||
resolver_rule_association = self.route53resolver_backend.get_resolver_rule_association(
|
||||
resolver_rule_association_id=resolver_rule_association_id
|
||||
)
|
||||
return json.dumps(
|
||||
{"ResolverRuleAssociation": resolver_rule_association.description()}
|
||||
)
|
||||
|
||||
def list_resolver_endpoint_ip_addresses(self):
|
||||
"""Returns list of IP addresses for specified Resolver endpoint."""
|
||||
resolver_endpoint_id = self._get_param("ResolverEndpointId")
|
||||
@ -159,6 +195,30 @@ class Route53ResolverResponse(BaseResponse):
|
||||
response["NextToken"] = next_token
|
||||
return json.dumps(response)
|
||||
|
||||
def list_resolver_rule_associations(self):
|
||||
"""Returns list of all Resolver associations, filtered if specified."""
|
||||
filters = self._get_param("Filters")
|
||||
next_token = self._get_param("NextToken")
|
||||
max_results = self._get_param("MaxResults", 10)
|
||||
validate_args([("maxResults", max_results)])
|
||||
try:
|
||||
(
|
||||
associations,
|
||||
next_token,
|
||||
) = self.route53resolver_backend.list_resolver_rule_associations(
|
||||
filters, next_token=next_token, max_results=max_results
|
||||
)
|
||||
except InvalidToken as exc:
|
||||
raise InvalidNextTokenException() from exc
|
||||
|
||||
response = {
|
||||
"ResolverRuleAssociations": [x.description() for x in associations],
|
||||
"MaxResults": max_results,
|
||||
}
|
||||
if next_token:
|
||||
response["NextToken"] = next_token
|
||||
return json.dumps(response)
|
||||
|
||||
def list_tags_for_resource(self):
|
||||
"""Lists all tags for the given resource."""
|
||||
resource_arn = self._get_param("ResourceArn")
|
||||
|
@ -19,6 +19,12 @@ PAGINATION_MODEL = {
|
||||
"limit_default": 100,
|
||||
"page_ending_range_keys": ["id"],
|
||||
},
|
||||
"list_resolver_rule_associations": {
|
||||
"input_token": "next_token",
|
||||
"limit_key": "max_results",
|
||||
"limit_default": 100,
|
||||
"page_ending_range_keys": ["id"],
|
||||
},
|
||||
"list_tags_for_resource": {
|
||||
"input_token": "next_token",
|
||||
"limit_key": "max_results",
|
||||
|
@ -24,10 +24,12 @@ def validate_args(validators):
|
||||
"maxResults": validate_max_results,
|
||||
"name": validate_name,
|
||||
"resolverEndpointId": validate_endpoint_id,
|
||||
"resolverRuleAssociationId": validate_rule_association_id,
|
||||
"resolverRuleId": validate_rule_id,
|
||||
"ruleType": validate_rule_type,
|
||||
"securityGroupIds": validate_security_group_ids,
|
||||
"targetIps.port": validate_target_port,
|
||||
"vPCId": validate_vpc_id,
|
||||
}
|
||||
|
||||
err_msgs = []
|
||||
@ -94,6 +96,13 @@ def validate_name(value):
|
||||
return ""
|
||||
|
||||
|
||||
def validate_rule_association_id(value):
|
||||
"""Raise exception if resolver rule association id has invalid length."""
|
||||
if value and len(value) > 64:
|
||||
return "have length less than or equal to 64"
|
||||
return ""
|
||||
|
||||
|
||||
def validate_rule_id(value):
|
||||
"""Raise exception if resolver rule id has invalid length."""
|
||||
if value and len(value) > 64:
|
||||
@ -133,3 +142,10 @@ def validate_target_port(value):
|
||||
if value and value["Port"] > 65535:
|
||||
return "have value less than or equal to 65535"
|
||||
return ""
|
||||
|
||||
|
||||
def validate_vpc_id(value):
|
||||
"""Raise exception if VPC id has invalid length."""
|
||||
if len(value) > 64:
|
||||
return "have length less than or equal to 64"
|
||||
return ""
|
||||
|
@ -416,10 +416,12 @@ def test_route53resolver_delete_resolver_endpoint():
|
||||
assert endpoint["CreationTime"] == created_endpoint["CreationTime"]
|
||||
|
||||
|
||||
@mock_ec2
|
||||
@mock_route53resolver
|
||||
def test_route53resolver_bad_delete_resolver_endpoint():
|
||||
"""Test delete_resolver_endpoint API calls with a bad ID."""
|
||||
client = boto3.client("route53resolver", region_name=TEST_REGION)
|
||||
ec2_client = boto3.client("ec2", region_name=TEST_REGION)
|
||||
random_num = get_random_hex(10)
|
||||
|
||||
# Use a resolver endpoint id that is too long.
|
||||
@ -441,6 +443,25 @@ def test_route53resolver_bad_delete_resolver_endpoint():
|
||||
assert err["Code"] == "ResourceNotFoundException"
|
||||
assert f"Resolver endpoint with ID '{random_num}' does not exist" in err["Message"]
|
||||
|
||||
# Create an endpoint and a rule referencing that endpoint. Verify the
|
||||
# endpoint can't be deleted due to that rule.
|
||||
endpoint = create_test_endpoint(client, ec2_client)
|
||||
resolver_rule = client.create_resolver_rule(
|
||||
CreatorRequestId=random_num,
|
||||
RuleType="FORWARD",
|
||||
DomainName=f"X{random_num}.com",
|
||||
ResolverEndpointId=endpoint["Id"],
|
||||
)
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.delete_resolver_endpoint(ResolverEndpointId=endpoint["Id"])
|
||||
err = exc.value.response["Error"]
|
||||
assert err["Code"] == "InvalidRequestException"
|
||||
assert (
|
||||
f"Cannot delete resolver endpoint unless its related resolver rules "
|
||||
f"are deleted. The following rules still exist for this resolver "
|
||||
f"endpoint: {resolver_rule['ResolverRule']['Id']}"
|
||||
) in err["Message"]
|
||||
|
||||
|
||||
@mock_ec2
|
||||
@mock_route53resolver
|
||||
|
@ -11,7 +11,7 @@ from moto.core import ACCOUNT_ID
|
||||
from moto.core.utils import get_random_hex
|
||||
from moto.ec2 import mock_ec2
|
||||
|
||||
from .test_route53resolver_endpoint import TEST_REGION, create_test_endpoint
|
||||
from .test_route53resolver_endpoint import TEST_REGION, create_test_endpoint, create_vpc
|
||||
|
||||
|
||||
def create_test_rule(client, name=None, tags=None):
|
||||
@ -299,10 +299,12 @@ def test_route53resolver_delete_resolver_rule():
|
||||
assert rule["CreationTime"] == created_rule["CreationTime"]
|
||||
|
||||
|
||||
@mock_ec2
|
||||
@mock_route53resolver
|
||||
def test_route53resolver_bad_delete_resolver_rule():
|
||||
"""Test delete_resolver_rule API calls with a bad ID."""
|
||||
client = boto3.client("route53resolver", region_name=TEST_REGION)
|
||||
ec2_client = boto3.client("ec2", region_name=TEST_REGION)
|
||||
random_num = get_random_hex(10)
|
||||
|
||||
# Use a resolver rule id that is too long.
|
||||
@ -324,6 +326,18 @@ def test_route53resolver_bad_delete_resolver_rule():
|
||||
assert err["Code"] == "ResourceNotFoundException"
|
||||
assert f"Resolver rule with ID '{random_num}' does not exist" in err["Message"]
|
||||
|
||||
# Verify a rule can't be deleted if VPCs are associated with it.
|
||||
test_rule = create_test_rule(client)
|
||||
vpc_id = create_vpc(ec2_client)
|
||||
client.associate_resolver_rule(ResolverRuleId=test_rule["Id"], VPCId=vpc_id)
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.delete_resolver_rule(ResolverRuleId=test_rule["Id"])
|
||||
err = exc.value.response["Error"]
|
||||
assert err["Code"] == "ResourceInUseException"
|
||||
assert (
|
||||
"Please disassociate this resolver rule from VPC first before deleting"
|
||||
) in err["Message"]
|
||||
|
||||
|
||||
@mock_route53resolver
|
||||
def test_route53resolver_get_resolver_rule():
|
||||
|
@ -0,0 +1,412 @@
|
||||
"""Unit tests for route53resolver rule association-related APIs."""
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
import pytest
|
||||
|
||||
from moto import mock_route53resolver
|
||||
from moto.core.utils import get_random_hex
|
||||
from moto.ec2 import mock_ec2
|
||||
|
||||
from .test_route53resolver_endpoint import TEST_REGION, create_vpc
|
||||
from .test_route53resolver_rule import create_test_rule
|
||||
|
||||
|
||||
def create_test_rule_association(
|
||||
client, ec2_client, resolver_rule_id=None, name=None, vpc_id=None
|
||||
):
|
||||
"""Create a Resolver Rule Association for testing purposes."""
|
||||
if not resolver_rule_id:
|
||||
resolver_rule_id = create_test_rule(client)["Id"]
|
||||
name = name if name else "R" + get_random_hex(10)
|
||||
if not vpc_id:
|
||||
vpc_id = create_vpc(ec2_client)
|
||||
return client.associate_resolver_rule(
|
||||
ResolverRuleId=resolver_rule_id, Name=name, VPCId=vpc_id
|
||||
)["ResolverRuleAssociation"]
|
||||
|
||||
|
||||
@mock_route53resolver
|
||||
def test_route53resolver_invalid_associate_resolver_rule_args():
|
||||
"""Test invalid arguments to the associate_resolver_rule API."""
|
||||
client = boto3.client("route53resolver", region_name=TEST_REGION)
|
||||
random_num = get_random_hex(10)
|
||||
|
||||
# Verify ValidationException error messages are accumulated properly:
|
||||
# - resolver rule ID that exceeds the allowed length of 64.
|
||||
# - name that exceeds the allowed length of 64.
|
||||
# - vpc_id that exceeds the allowed length of 64.
|
||||
long_id = random_num * 7
|
||||
long_name = random_num * 6 + "abcde"
|
||||
long_vpc_id = random_num * 6 + "fghij"
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.associate_resolver_rule(
|
||||
ResolverRuleId=long_id, Name=long_name, VPCId=long_vpc_id,
|
||||
)
|
||||
err = exc.value.response["Error"]
|
||||
assert err["Code"] == "ValidationException"
|
||||
assert "3 validation errors detected" in err["Message"]
|
||||
assert (
|
||||
f"Value '{long_id}' at 'resolverRuleId' failed to satisfy "
|
||||
f"constraint: Member must have length less than or equal to 64"
|
||||
) in err["Message"]
|
||||
assert (
|
||||
f"Value '{long_name}' at 'name' failed to satisfy constraint: "
|
||||
f"Member must have length less than or equal to 64"
|
||||
) in err["Message"]
|
||||
assert (
|
||||
f"Value '{long_vpc_id}' at 'vPCId' failed to satisfy "
|
||||
f"constraint: Member must have length less than or equal to 64"
|
||||
) in err["Message"]
|
||||
|
||||
|
||||
@mock_ec2
|
||||
@mock_route53resolver
|
||||
def test_route53resolver_associate_resolver_rule():
|
||||
"""Test good associate_resolver_rule API calls."""
|
||||
client = boto3.client("route53resolver", region_name=TEST_REGION)
|
||||
ec2_client = boto3.client("ec2", region_name=TEST_REGION)
|
||||
|
||||
resolver_rule_id = create_test_rule(client)["Id"]
|
||||
name = "X" + get_random_hex(10)
|
||||
vpc_id = create_vpc(ec2_client)
|
||||
rule_association = client.associate_resolver_rule(
|
||||
ResolverRuleId=resolver_rule_id, Name=name, VPCId=vpc_id,
|
||||
)["ResolverRuleAssociation"]
|
||||
assert rule_association["Id"].startswith("rslvr-rrassoc-")
|
||||
assert rule_association["ResolverRuleId"] == resolver_rule_id
|
||||
assert rule_association["Name"] == name
|
||||
assert rule_association["VPCId"] == vpc_id
|
||||
assert rule_association["Status"] == "COMPLETE"
|
||||
assert "StatusMessage" in rule_association
|
||||
|
||||
|
||||
@mock_ec2
|
||||
@mock_route53resolver
|
||||
def test_route53resolver_other_associate_resolver_rule_errors():
|
||||
"""Test good associate_resolver_rule API calls."""
|
||||
client = boto3.client("route53resolver", region_name=TEST_REGION)
|
||||
ec2_client = boto3.client("ec2", region_name=TEST_REGION)
|
||||
|
||||
# Resolver referenced by resolver_rule_id doesn't exist.
|
||||
with pytest.raises(ClientError) as exc:
|
||||
create_test_rule_association(client, ec2_client, resolver_rule_id="foo")
|
||||
err = exc.value.response["Error"]
|
||||
assert err["Code"] == "ResourceNotFoundException"
|
||||
assert "Resolver rule with ID 'foo' does not exist" in err["Message"]
|
||||
|
||||
# Invalid vpc_id
|
||||
with pytest.raises(ClientError) as exc:
|
||||
create_test_rule_association(client, ec2_client, vpc_id="foo")
|
||||
err = exc.value.response["Error"]
|
||||
assert err["Code"] == "InvalidParameterException"
|
||||
assert "The vpc ID 'foo' does not exist" in err["Message"]
|
||||
|
||||
# Same resolver_rule_id and vpc_id pair for an association.
|
||||
resolver_rule_id = create_test_rule(client)["Id"]
|
||||
vpc_id = create_vpc(ec2_client)
|
||||
create_test_rule_association(
|
||||
client, ec2_client, resolver_rule_id=resolver_rule_id, vpc_id=vpc_id
|
||||
)
|
||||
with pytest.raises(ClientError) as exc:
|
||||
create_test_rule_association(
|
||||
client, ec2_client, resolver_rule_id=resolver_rule_id, vpc_id=vpc_id
|
||||
)
|
||||
err = exc.value.response["Error"]
|
||||
assert err["Code"] == "InvalidRequestException"
|
||||
assert (
|
||||
f"Cannot associate rules with same domain name with same VPC. "
|
||||
f"Conflict with resolver rule '{resolver_rule_id}'"
|
||||
) in err["Message"]
|
||||
|
||||
# Not testing "Too many rule associations" as it takes too long to create
|
||||
# 2000 VPCs and rule associations.
|
||||
|
||||
|
||||
@mock_ec2
|
||||
@mock_route53resolver
|
||||
def test_route53resolver_disassociate_resolver_rule():
|
||||
"""Test good disassociate_resolver_rule API calls."""
|
||||
client = boto3.client("route53resolver", region_name=TEST_REGION)
|
||||
ec2_client = boto3.client("ec2", region_name=TEST_REGION)
|
||||
created_association = create_test_rule_association(client, ec2_client)
|
||||
|
||||
# Disassociate the resolver rule and verify the response.
|
||||
response = client.disassociate_resolver_rule(
|
||||
ResolverRuleId=created_association["ResolverRuleId"],
|
||||
VPCId=created_association["VPCId"],
|
||||
)
|
||||
association = response["ResolverRuleAssociation"]
|
||||
assert association["Id"] == created_association["Id"]
|
||||
assert association["ResolverRuleId"] == created_association["ResolverRuleId"]
|
||||
assert association["Name"] == created_association["Name"]
|
||||
assert association["VPCId"] == created_association["VPCId"]
|
||||
assert association["Status"] == "DELETING"
|
||||
assert "Deleting" in association["StatusMessage"]
|
||||
|
||||
|
||||
@mock_ec2
|
||||
@mock_route53resolver
|
||||
def test_route53resolver_bad_disassociate_resolver_rule():
|
||||
"""Test disassociate_resolver_rule API calls with a bad ID."""
|
||||
client = boto3.client("route53resolver", region_name=TEST_REGION)
|
||||
ec2_client = boto3.client("ec2", region_name=TEST_REGION)
|
||||
random_num = get_random_hex(10)
|
||||
|
||||
# Use a resolver rule id and vpc id that is too long.
|
||||
long_id = "0123456789" * 6 + "xxxxx"
|
||||
long_vpc_id = random_num * 6 + "12345"
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.disassociate_resolver_rule(ResolverRuleId=long_id, VPCId=long_vpc_id)
|
||||
err = exc.value.response["Error"]
|
||||
assert err["Code"] == "ValidationException"
|
||||
assert "2 validation errors detected" in err["Message"]
|
||||
assert (
|
||||
f"Value '{long_id}' at 'resolverRuleId' failed to satisfy "
|
||||
f"constraint: Member must have length less than or equal to 64"
|
||||
) in err["Message"]
|
||||
assert (
|
||||
f"Value '{long_vpc_id}' at 'vPCId' failed to satisfy "
|
||||
f"constraint: Member must have length less than or equal to 64"
|
||||
) in err["Message"]
|
||||
|
||||
# Create a test association.
|
||||
test_association = create_test_rule_association(client, ec2_client)
|
||||
test_rule_id = test_association["ResolverRuleId"]
|
||||
test_vpc_id = test_association["VPCId"]
|
||||
|
||||
# Disassociate from a non-existent resolver rule id.
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.disassociate_resolver_rule(ResolverRuleId=random_num, VPCId=test_vpc_id)
|
||||
err = exc.value.response["Error"]
|
||||
assert err["Code"] == "ResourceNotFoundException"
|
||||
assert f"Resolver rule with ID '{random_num}' does not exist" in err["Message"]
|
||||
|
||||
# Disassociate using a non-existent vpc id.
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.disassociate_resolver_rule(ResolverRuleId=test_rule_id, VPCId=random_num)
|
||||
err = exc.value.response["Error"]
|
||||
assert err["Code"] == "ResourceNotFoundException"
|
||||
assert (
|
||||
f"Resolver Rule Association between Resolver Rule "
|
||||
f"'{test_rule_id}' and VPC '{random_num}' does not exist"
|
||||
) in err["Message"]
|
||||
|
||||
# Disassociate successfully, then test that it's not possible to
|
||||
# disassociate again.
|
||||
client.disassociate_resolver_rule(ResolverRuleId=test_rule_id, VPCId=test_vpc_id)
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.disassociate_resolver_rule(
|
||||
ResolverRuleId=test_rule_id, VPCId=test_vpc_id
|
||||
)
|
||||
err = exc.value.response["Error"]
|
||||
assert err["Code"] == "ResourceNotFoundException"
|
||||
assert (
|
||||
f"Resolver Rule Association between Resolver Rule "
|
||||
f"'{test_rule_id}' and VPC '{test_vpc_id}' does not exist"
|
||||
) in err["Message"]
|
||||
|
||||
|
||||
@mock_ec2
|
||||
@mock_route53resolver
|
||||
def test_route53resolver_get_resolver_rule_association():
|
||||
"""Test good get_resolver_rule_association API calls."""
|
||||
client = boto3.client("route53resolver", region_name=TEST_REGION)
|
||||
ec2_client = boto3.client("ec2", region_name=TEST_REGION)
|
||||
|
||||
# Create a good association for testing purposes.
|
||||
created_association = create_test_rule_association(client, ec2_client)
|
||||
|
||||
# Now get the resolver rule association and verify the response.
|
||||
response = client.get_resolver_rule_association(
|
||||
ResolverRuleAssociationId=created_association["Id"]
|
||||
)
|
||||
association = response["ResolverRuleAssociation"]
|
||||
assert association["Id"] == created_association["Id"]
|
||||
assert association["ResolverRuleId"] == created_association["ResolverRuleId"]
|
||||
assert association["Name"] == created_association["Name"]
|
||||
assert association["VPCId"] == created_association["VPCId"]
|
||||
assert association["Status"] == created_association["Status"]
|
||||
assert association["StatusMessage"] == created_association["StatusMessage"]
|
||||
|
||||
|
||||
@mock_route53resolver
|
||||
def test_route53resolver_bad_get_resolver_rule_association():
|
||||
"""Test get_resolver_rule_association API calls with a bad ID."""
|
||||
client = boto3.client("route53resolver", region_name=TEST_REGION)
|
||||
random_num = get_random_hex(10)
|
||||
|
||||
# Use a resolver rule association id that is too long.
|
||||
long_id = "0123456789" * 6 + "xxxxx"
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.get_resolver_rule_association(ResolverRuleAssociationId=long_id)
|
||||
err = exc.value.response["Error"]
|
||||
assert err["Code"] == "ValidationException"
|
||||
assert "1 validation error detected" in err["Message"]
|
||||
assert (
|
||||
f"Value '{long_id}' at 'resolverRuleAssociationId' failed to satisfy "
|
||||
f"constraint: Member must have length less than or equal to 64"
|
||||
) in err["Message"]
|
||||
|
||||
# Get a non-existent resolver rule association.
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.get_resolver_rule_association(ResolverRuleAssociationId=random_num)
|
||||
err = exc.value.response["Error"]
|
||||
assert err["Code"] == "ResourceNotFoundException"
|
||||
assert f"ResolverRuleAssociation '{random_num}' does not Exist" in err["Message"]
|
||||
|
||||
|
||||
@mock_ec2
|
||||
@mock_route53resolver
|
||||
def test_route53resolver_list_resolver_rule_associations():
|
||||
"""Test good list_resolver_rule_associations API calls."""
|
||||
client = boto3.client("route53resolver", region_name=TEST_REGION)
|
||||
ec2_client = boto3.client("ec2", region_name=TEST_REGION)
|
||||
random_num = get_random_hex(10)
|
||||
|
||||
# List rule associations when there are none.
|
||||
response = client.list_resolver_rule_associations()
|
||||
assert len(response["ResolverRuleAssociations"]) == 0
|
||||
assert response["MaxResults"] == 10
|
||||
assert "NextToken" not in response
|
||||
|
||||
# Create 4 associations, verify all are listed when no filters, max_results.
|
||||
for idx in range(4):
|
||||
create_test_rule_association(client, ec2_client, name=f"A{idx}-{random_num}")
|
||||
response = client.list_resolver_rule_associations()
|
||||
associations = response["ResolverRuleAssociations"]
|
||||
assert len(associations) == 4
|
||||
assert response["MaxResults"] == 10
|
||||
for idx in range(4):
|
||||
assert associations[idx]["Name"].startswith(f"A{idx}")
|
||||
|
||||
# Set max_results to return 1 association, use next_token to get
|
||||
# remaining 3.
|
||||
response = client.list_resolver_rule_associations(MaxResults=1)
|
||||
associations = response["ResolverRuleAssociations"]
|
||||
assert len(associations) == 1
|
||||
assert response["MaxResults"] == 1
|
||||
assert "NextToken" in response
|
||||
assert associations[0]["Name"].startswith("A0")
|
||||
|
||||
response = client.list_resolver_rule_associations(NextToken=response["NextToken"])
|
||||
associations = response["ResolverRuleAssociations"]
|
||||
assert len(associations) == 3
|
||||
assert response["MaxResults"] == 10
|
||||
assert "NextToken" not in response
|
||||
for idx, association in enumerate(associations):
|
||||
assert association["Name"].startswith(f"A{idx + 1}")
|
||||
|
||||
|
||||
@mock_ec2
|
||||
@mock_route53resolver
|
||||
def test_route53resolver_list_resolver_rule_associations_filters():
|
||||
"""Test good list_resolver_rule_associations API calls that use filters."""
|
||||
client = boto3.client("route53resolver", region_name=TEST_REGION)
|
||||
ec2_client = boto3.client("ec2", region_name=TEST_REGION)
|
||||
random_num = get_random_hex(10)
|
||||
|
||||
# Create some rule associations for testing purposes
|
||||
vpc_id1 = create_vpc(ec2_client)
|
||||
vpc_id2 = create_vpc(ec2_client)
|
||||
associations = []
|
||||
rule_ids = ["zero_offset"]
|
||||
for idx in range(1, 5):
|
||||
association = create_test_rule_association(
|
||||
client,
|
||||
ec2_client,
|
||||
name=f"F{idx}-{random_num}",
|
||||
vpc_id=vpc_id1 if idx % 2 else vpc_id2,
|
||||
)
|
||||
associations.append(association)
|
||||
rule_ids.append(association["ResolverRuleId"])
|
||||
|
||||
# Try all the valid filter names, including some of the old style names.
|
||||
response = client.list_resolver_rule_associations(
|
||||
Filters=[{"Name": "ResolverRuleId", "Values": [rule_ids[3]]}]
|
||||
)
|
||||
assert len(response["ResolverRuleAssociations"]) == 1
|
||||
assert response["ResolverRuleAssociations"][0]["ResolverRuleId"] == rule_ids[3]
|
||||
|
||||
response = client.list_resolver_rule_associations(
|
||||
Filters=[{"Name": "RESOLVER_RULE_ID", "Values": [rule_ids[2], rule_ids[4]]}]
|
||||
)
|
||||
assert len(response["ResolverRuleAssociations"]) == 2
|
||||
assert response["ResolverRuleAssociations"][0]["ResolverRuleId"] == rule_ids[2]
|
||||
assert response["ResolverRuleAssociations"][1]["ResolverRuleId"] == rule_ids[4]
|
||||
|
||||
response = client.list_resolver_rule_associations(
|
||||
Filters=[{"Name": "VPCId", "Values": [vpc_id2]}]
|
||||
)
|
||||
assert len(response["ResolverRuleAssociations"]) == 2
|
||||
|
||||
response = client.list_resolver_rule_associations(
|
||||
Filters=[{"Name": "Name", "Values": [f"F1-{random_num}"]}]
|
||||
)
|
||||
assert len(response["ResolverRuleAssociations"]) == 1
|
||||
assert response["ResolverRuleAssociations"][0]["Name"] == f"F1-{random_num}"
|
||||
|
||||
response = client.list_resolver_rule_associations(
|
||||
Filters=[
|
||||
{"Name": "VPC_ID", "Values": [vpc_id1]},
|
||||
{"Name": "NAME", "Values": [f"F3-{random_num}"]},
|
||||
]
|
||||
)
|
||||
assert len(response["ResolverRuleAssociations"]) == 1
|
||||
assert response["ResolverRuleAssociations"][0]["Name"] == f"F3-{random_num}"
|
||||
|
||||
response = client.list_resolver_rule_associations(
|
||||
Filters=[{"Name": "Status", "Values": ["COMPLETE"]}]
|
||||
)
|
||||
assert len(response["ResolverRuleAssociations"]) == 4
|
||||
response = client.list_resolver_rule_associations(
|
||||
Filters=[{"Name": "Status", "Values": ["CREATING"]}]
|
||||
)
|
||||
assert len(response["ResolverRuleAssociations"]) == 0
|
||||
|
||||
|
||||
@mock_route53resolver
|
||||
def test_route53resolver_bad_list_resolver_rule_associations_filters():
|
||||
"""Test bad list_resolver_rule_associations API calls that use filters."""
|
||||
client = boto3.client("route53resolver", region_name=TEST_REGION)
|
||||
|
||||
# botocore barfs on an empty "Values":
|
||||
# TypeError: list_resolver_rule_associations() only accepts keyword arguments.
|
||||
# client.list_resolver_rule_associations([{"Name": "Direction", "Values": []}])
|
||||
# client.list_resolver_rule_associations([{"Values": []}])
|
||||
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.list_resolver_rule_associations(
|
||||
Filters=[{"Name": "foo", "Values": ["bar"]}]
|
||||
)
|
||||
err = exc.value.response["Error"]
|
||||
assert err["Code"] == "InvalidParameterException"
|
||||
assert "The filter 'foo' is invalid" in err["Message"]
|
||||
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.list_resolver_rule_associations(
|
||||
Filters=[{"Name": "VpcId", "Values": ["bar"]}]
|
||||
)
|
||||
err = exc.value.response["Error"]
|
||||
assert err["Code"] == "InvalidParameterException"
|
||||
assert "The filter 'VpcId' is invalid" in err["Message"]
|
||||
|
||||
|
||||
@mock_ec2
|
||||
@mock_route53resolver
|
||||
def test_route53resolver_bad_list_resolver_rule_associations():
|
||||
"""Test bad list_resolver_rule_associations API calls."""
|
||||
client = boto3.client("route53resolver", region_name=TEST_REGION)
|
||||
ec2_client = boto3.client("ec2", region_name=TEST_REGION)
|
||||
|
||||
# Bad max_results.
|
||||
create_test_rule_association(client, ec2_client)
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.list_resolver_rule_associations(MaxResults=250)
|
||||
err = exc.value.response["Error"]
|
||||
assert err["Code"] == "ValidationException"
|
||||
assert "1 validation error detected" in err["Message"]
|
||||
assert (
|
||||
"Value '250' at 'maxResults' failed to satisfy constraint: Member "
|
||||
"must have length less than or equal to 100"
|
||||
) in err["Message"]
|
Loading…
Reference in New Issue
Block a user