Route53Resolver: Add resolver rule association-related APIs (#4611)

This commit is contained in:
kbalk 2021-11-23 05:00:50 -05:00 committed by GitHub
parent e4e58f777d
commit 74666c1271
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 722 additions and 13 deletions

View File

@ -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

View File

@ -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.

View File

@ -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."""

View File

@ -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():

View File

@ -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")

View File

@ -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",

View File

@ -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 ""

View File

@ -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

View File

@ -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():

View File

@ -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"]