EC2 - Fix 3 issues with route table associations (#5509)

This commit is contained in:
Radosław Piliszek 2022-10-01 00:36:55 +02:00 committed by GitHub
parent 3118090fdc
commit 3679521d76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 112 additions and 54 deletions

View File

@ -25,8 +25,10 @@ class RouteTable(TaggedEC2Resource, CloudFormationModel):
self.ec2_backend = ec2_backend
self.id = route_table_id
self.vpc_id = vpc_id
self.main = main
self.main_association = random_subnet_association_id()
if main:
self.main_association_id = random_subnet_association_id()
else:
self.main_association_id = None
self.associations = {}
self.routes = {}
@ -64,7 +66,7 @@ class RouteTable(TaggedEC2Resource, CloudFormationModel):
if filter_name == "association.main":
# Note: Boto only supports 'true'.
# https://github.com/boto/boto/issues/1742
if self.main:
if self.main_association_id is not None:
return "true"
else:
return "false"
@ -75,7 +77,7 @@ class RouteTable(TaggedEC2Resource, CloudFormationModel):
elif filter_name == "association.route-table-id":
return self.id
elif filter_name == "association.route-table-association-id":
return self.associations.keys()
return self.all_associations_ids
elif filter_name == "association.subnet-id":
return self.associations.values()
elif filter_name == "route.gateway-id":
@ -87,6 +89,14 @@ class RouteTable(TaggedEC2Resource, CloudFormationModel):
else:
return super().get_filter_value(filter_name, "DescribeRouteTables")
@property
def all_associations_ids(self):
# NOTE(yoctozepto): Doing an explicit copy to not touch the original.
all_associations = set(self.associations)
if self.main_association_id is not None:
all_associations.add(self.main_association_id)
return all_associations
class RouteTableBackend:
def __init__(self):
@ -186,7 +196,7 @@ class RouteTableBackend:
def replace_route_table_association(self, association_id, route_table_id):
# Idempotent if association already exists.
new_route_table = self.get_route_table(route_table_id)
if association_id in new_route_table.associations:
if association_id in new_route_table.all_associations_ids:
return association_id
# Find route table which currently has the association, error if none.
@ -195,11 +205,19 @@ class RouteTableBackend:
)
if not route_tables_by_association_id:
raise InvalidAssociationIdError(association_id)
previous_route_table = route_tables_by_association_id[0]
# Remove existing association, create new one.
previous_route_table = route_tables_by_association_id[0]
subnet_id = previous_route_table.associations.pop(association_id, None)
return self.associate_route_table(route_table_id, subnet_id)
new_association_id = random_subnet_association_id()
if previous_route_table.main_association_id == association_id:
previous_route_table.main_association_id = None
new_route_table.main_association_id = new_association_id
else:
association_target_id = previous_route_table.associations.pop(
association_id
)
new_route_table.associations[new_association_id] = association_target_id
return new_association_id
# TODO: refractor to isloate class methods from backend logic

View File

@ -251,14 +251,16 @@ DESCRIBE_ROUTE_TABLES_RESPONSE = """
{% endfor %}
</routeSet>
<associationSet>
{% if route_table.main_association_id is not none %}
<item>
<routeTableAssociationId>{{ route_table.main_association }}</routeTableAssociationId>
<routeTableAssociationId>{{ route_table.main_association_id }}</routeTableAssociationId>
<routeTableId>{{ route_table.id }}</routeTableId>
<main>true</main>
<associationState>
<state>associated</state>
</associationState>
</item>
{% endif %}
{% for association_id,subnet_id in route_table.associations.items() %}
<item>
<routeTableAssociationId>{{ association_id }}</routeTableAssociationId>
@ -324,5 +326,8 @@ REPLACE_ROUTE_TABLE_ASSOCIATION_RESPONSE = """
<ReplaceRouteTableAssociationResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<newAssociationId>{{ association_id }}</newAssociationId>
<associationState>
<state>associated</state>
</associationState>
</ReplaceRouteTableAssociationResponse>
"""

View File

@ -185,7 +185,7 @@ def test_route_tables_filters_associations():
)["RouteTables"]
association1_route_tables.should.have.length_of(1)
association1_route_tables[0]["RouteTableId"].should.equal(route_table1.id)
association1_route_tables[0]["Associations"].should.have.length_of(3)
association1_route_tables[0]["Associations"].should.have.length_of(2)
# Filter by route table ID
route_table2_route_tables = client.describe_route_tables(
@ -193,7 +193,7 @@ def test_route_tables_filters_associations():
)["RouteTables"]
route_table2_route_tables.should.have.length_of(1)
route_table2_route_tables[0]["RouteTableId"].should.equal(route_table2.id)
route_table2_route_tables[0]["Associations"].should.have.length_of(2)
route_table2_route_tables[0]["Associations"].should.have.length_of(1)
# Filter by subnet ID
subnet_route_tables = client.describe_route_tables(
@ -201,7 +201,7 @@ def test_route_tables_filters_associations():
)["RouteTables"]
subnet_route_tables.should.have.length_of(1)
subnet_route_tables[0]["RouteTableId"].should.equal(route_table1.id)
subnet_route_tables[0]["Associations"].should.have.length_of(3)
subnet_route_tables[0]["Associations"].should.have.length_of(2)
@mock_ec2
@ -215,7 +215,7 @@ def test_route_table_associations():
# Refresh
r = client.describe_route_tables(RouteTableIds=[route_table.id])["RouteTables"][0]
r["Associations"].should.have.length_of(1)
r["Associations"].should.have.length_of(0)
# Associate
association_id = client.associate_route_table(
@ -224,12 +224,12 @@ def test_route_table_associations():
# Refresh
r = client.describe_route_tables(RouteTableIds=[route_table.id])["RouteTables"][0]
r["Associations"].should.have.length_of(2)
r["Associations"].should.have.length_of(1)
r["Associations"][1]["RouteTableAssociationId"].should.equal(association_id)
r["Associations"][1]["Main"].should.equal(False)
r["Associations"][1]["RouteTableId"].should.equal(route_table.id)
r["Associations"][1]["SubnetId"].should.equal(subnet.id)
r["Associations"][0]["RouteTableAssociationId"].should.equal(association_id)
r["Associations"][0]["Main"].should.equal(False)
r["Associations"][0]["RouteTableId"].should.equal(route_table.id)
r["Associations"][0]["SubnetId"].should.equal(subnet.id)
# Associate is idempotent
association_id_idempotent = client.associate_route_table(
@ -247,16 +247,9 @@ def test_route_table_associations():
# Disassociate
client.disassociate_route_table(AssociationId=association_id)
# Refresh - The default (main) route should be there
# Validate
r = client.describe_route_tables(RouteTableIds=[route_table.id])["RouteTables"][0]
r["Associations"].should.have.length_of(1)
r["Associations"][0].should.have.key("Main").equal(True)
r["Associations"][0].should.have.key("RouteTableAssociationId")
r["Associations"][0].should.have.key("RouteTableId").equals(route_table.id)
r["Associations"][0].should.have.key("AssociationState").equals(
{"State": "associated"}
)
r["Associations"][0].shouldnt.have.key("SubnetId")
r["Associations"].should.have.length_of(0)
# Error: Disassociate with invalid association ID
with pytest.raises(ClientError) as ex:
@ -301,7 +294,7 @@ def test_route_table_replace_route_table_association():
route_table1 = client.describe_route_tables(RouteTableIds=[route_table1_id])[
"RouteTables"
][0]
route_table1["Associations"].should.have.length_of(1)
route_table1["Associations"].should.have.length_of(0)
# Associate
association_id1 = client.associate_route_table(
@ -317,15 +310,15 @@ def test_route_table_replace_route_table_association():
][0]
# Validate
route_table1["Associations"].should.have.length_of(2)
route_table2["Associations"].should.have.length_of(1)
route_table1["Associations"].should.have.length_of(1)
route_table2["Associations"].should.have.length_of(0)
route_table1["Associations"][1]["RouteTableAssociationId"].should.equal(
route_table1["Associations"][0]["RouteTableAssociationId"].should.equal(
association_id1
)
route_table1["Associations"][1]["Main"].should.equal(False)
route_table1["Associations"][1]["RouteTableId"].should.equal(route_table1_id)
route_table1["Associations"][1]["SubnetId"].should.equal(subnet.id)
route_table1["Associations"][0]["Main"].should.equal(False)
route_table1["Associations"][0]["RouteTableId"].should.equal(route_table1_id)
route_table1["Associations"][0]["SubnetId"].should.equal(subnet.id)
# Replace Association
association_id2 = client.replace_route_table_association(
@ -341,15 +334,15 @@ def test_route_table_replace_route_table_association():
][0]
# Validate
route_table1["Associations"].should.have.length_of(1)
route_table2["Associations"].should.have.length_of(2)
route_table1["Associations"].should.have.length_of(0)
route_table2["Associations"].should.have.length_of(1)
route_table2["Associations"][1]["RouteTableAssociationId"].should.equal(
route_table2["Associations"][0]["RouteTableAssociationId"].should.equal(
association_id2
)
route_table2["Associations"][1]["Main"].should.equal(False)
route_table2["Associations"][1]["RouteTableId"].should.equal(route_table2_id)
route_table2["Associations"][1]["SubnetId"].should.equal(subnet.id)
route_table2["Associations"][0]["Main"].should.equal(False)
route_table2["Associations"][0]["RouteTableId"].should.equal(route_table2_id)
route_table2["Associations"][0]["SubnetId"].should.equal(subnet.id)
# Replace Association is idempotent
association_id_idempotent = client.replace_route_table_association(
@ -376,6 +369,52 @@ def test_route_table_replace_route_table_association():
ex.value.response["Error"]["Code"].should.equal("InvalidRouteTableID.NotFound")
@mock_ec2
def test_route_table_replace_route_table_association_for_main():
client = boto3.client("ec2", region_name="us-east-1")
ec2 = boto3.resource("ec2", region_name="us-east-1")
vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
new_route_table_id = ec2.create_route_table(VpcId=vpc.id).id
# Get main route table details
main_route_table = client.describe_route_tables(
Filters=[
{"Name": "vpc-id", "Values": [vpc.id]},
{"Name": "association.main", "Values": ["true"]},
]
)["RouteTables"][0]
main_route_table_id = main_route_table["RouteTableId"]
main_route_table_association_id = main_route_table["Associations"][0][
"RouteTableAssociationId"
]
# Replace Association
new_association = client.replace_route_table_association(
AssociationId=main_route_table_association_id, RouteTableId=new_route_table_id
)
new_association_id = new_association["NewAssociationId"]
# Validate the format
new_association["AssociationState"]["State"].should.equal("associated")
# Refresh
main_route_table = client.describe_route_tables(
RouteTableIds=[main_route_table_id]
)["RouteTables"][0]
new_route_table = client.describe_route_tables(RouteTableIds=[new_route_table_id])[
"RouteTables"
][0]
# Validate
main_route_table["Associations"].should.have.length_of(0)
new_route_table["Associations"].should.have.length_of(1)
new_route_table["Associations"][0]["RouteTableAssociationId"].should.equal(
new_association_id
)
new_route_table["Associations"][0]["Main"].should.equal(True)
@mock_ec2
def test_route_table_get_by_tag():
ec2 = boto3.resource("ec2", region_name="eu-central-1")
@ -950,14 +989,12 @@ def test_associate_route_table_by_gateway():
]
)["RouteTables"]
# First assocation is the main
verify[0]["Associations"][0]["Main"].should.equal(True)
verify[0]["Associations"].should.have.length_of(1)
# Second is our Gateway
verify[0]["Associations"][1]["Main"].should.equal(False)
verify[0]["Associations"][1]["GatewayId"].should.equal(igw_id)
verify[0]["Associations"][1]["RouteTableAssociationId"].should.equal(assoc_id)
verify[0]["Associations"][1].doesnt.have.key("SubnetId")
verify[0]["Associations"][0]["Main"].should.equal(False)
verify[0]["Associations"][0]["GatewayId"].should.equal(igw_id)
verify[0]["Associations"][0]["RouteTableAssociationId"].should.equal(assoc_id)
verify[0]["Associations"][0].doesnt.have.key("SubnetId")
@mock_ec2
@ -977,11 +1014,9 @@ def test_associate_route_table_by_subnet():
]
)["RouteTables"]
# First assocation is the main
verify[0]["Associations"][0].should.have.key("Main").equals(True)
verify[0]["Associations"].should.have.length_of(1)
# Second is our Gateway
verify[0]["Associations"][1]["Main"].should.equal(False)
verify[0]["Associations"][1]["SubnetId"].should.equals(subnet_id)
verify[0]["Associations"][1]["RouteTableAssociationId"].should.equal(assoc_id)
verify[0]["Associations"][1].doesnt.have.key("GatewayId")
verify[0]["Associations"][0]["Main"].should.equal(False)
verify[0]["Associations"][0]["SubnetId"].should.equals(subnet_id)
verify[0]["Associations"][0]["RouteTableAssociationId"].should.equal(assoc_id)
verify[0]["Associations"][0].doesnt.have.key("GatewayId")