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

View File

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

View File

@ -185,7 +185,7 @@ def test_route_tables_filters_associations():
)["RouteTables"] )["RouteTables"]
association1_route_tables.should.have.length_of(1) association1_route_tables.should.have.length_of(1)
association1_route_tables[0]["RouteTableId"].should.equal(route_table1.id) 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 # Filter by route table ID
route_table2_route_tables = client.describe_route_tables( route_table2_route_tables = client.describe_route_tables(
@ -193,7 +193,7 @@ def test_route_tables_filters_associations():
)["RouteTables"] )["RouteTables"]
route_table2_route_tables.should.have.length_of(1) 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]["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 # Filter by subnet ID
subnet_route_tables = client.describe_route_tables( subnet_route_tables = client.describe_route_tables(
@ -201,7 +201,7 @@ def test_route_tables_filters_associations():
)["RouteTables"] )["RouteTables"]
subnet_route_tables.should.have.length_of(1) subnet_route_tables.should.have.length_of(1)
subnet_route_tables[0]["RouteTableId"].should.equal(route_table1.id) 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 @mock_ec2
@ -215,7 +215,7 @@ def test_route_table_associations():
# Refresh # Refresh
r = client.describe_route_tables(RouteTableIds=[route_table.id])["RouteTables"][0] 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 # Associate
association_id = client.associate_route_table( association_id = client.associate_route_table(
@ -224,12 +224,12 @@ def test_route_table_associations():
# Refresh # Refresh
r = client.describe_route_tables(RouteTableIds=[route_table.id])["RouteTables"][0] 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"][0]["RouteTableAssociationId"].should.equal(association_id)
r["Associations"][1]["Main"].should.equal(False) r["Associations"][0]["Main"].should.equal(False)
r["Associations"][1]["RouteTableId"].should.equal(route_table.id) r["Associations"][0]["RouteTableId"].should.equal(route_table.id)
r["Associations"][1]["SubnetId"].should.equal(subnet.id) r["Associations"][0]["SubnetId"].should.equal(subnet.id)
# Associate is idempotent # Associate is idempotent
association_id_idempotent = client.associate_route_table( association_id_idempotent = client.associate_route_table(
@ -247,16 +247,9 @@ def test_route_table_associations():
# Disassociate # Disassociate
client.disassociate_route_table(AssociationId=association_id) 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 = client.describe_route_tables(RouteTableIds=[route_table.id])["RouteTables"][0]
r["Associations"].should.have.length_of(1) r["Associations"].should.have.length_of(0)
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")
# Error: Disassociate with invalid association ID # Error: Disassociate with invalid association ID
with pytest.raises(ClientError) as ex: 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])[ route_table1 = client.describe_route_tables(RouteTableIds=[route_table1_id])[
"RouteTables" "RouteTables"
][0] ][0]
route_table1["Associations"].should.have.length_of(1) route_table1["Associations"].should.have.length_of(0)
# Associate # Associate
association_id1 = client.associate_route_table( association_id1 = client.associate_route_table(
@ -317,15 +310,15 @@ def test_route_table_replace_route_table_association():
][0] ][0]
# Validate # Validate
route_table1["Associations"].should.have.length_of(2) route_table1["Associations"].should.have.length_of(1)
route_table2["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 association_id1
) )
route_table1["Associations"][1]["Main"].should.equal(False) route_table1["Associations"][0]["Main"].should.equal(False)
route_table1["Associations"][1]["RouteTableId"].should.equal(route_table1_id) route_table1["Associations"][0]["RouteTableId"].should.equal(route_table1_id)
route_table1["Associations"][1]["SubnetId"].should.equal(subnet.id) route_table1["Associations"][0]["SubnetId"].should.equal(subnet.id)
# Replace Association # Replace Association
association_id2 = client.replace_route_table_association( association_id2 = client.replace_route_table_association(
@ -341,15 +334,15 @@ def test_route_table_replace_route_table_association():
][0] ][0]
# Validate # Validate
route_table1["Associations"].should.have.length_of(1) route_table1["Associations"].should.have.length_of(0)
route_table2["Associations"].should.have.length_of(2) route_table2["Associations"].should.have.length_of(1)
route_table2["Associations"][1]["RouteTableAssociationId"].should.equal( route_table2["Associations"][0]["RouteTableAssociationId"].should.equal(
association_id2 association_id2
) )
route_table2["Associations"][1]["Main"].should.equal(False) route_table2["Associations"][0]["Main"].should.equal(False)
route_table2["Associations"][1]["RouteTableId"].should.equal(route_table2_id) route_table2["Associations"][0]["RouteTableId"].should.equal(route_table2_id)
route_table2["Associations"][1]["SubnetId"].should.equal(subnet.id) route_table2["Associations"][0]["SubnetId"].should.equal(subnet.id)
# Replace Association is idempotent # Replace Association is idempotent
association_id_idempotent = client.replace_route_table_association( 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") 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 @mock_ec2
def test_route_table_get_by_tag(): def test_route_table_get_by_tag():
ec2 = boto3.resource("ec2", region_name="eu-central-1") ec2 = boto3.resource("ec2", region_name="eu-central-1")
@ -950,14 +989,12 @@ def test_associate_route_table_by_gateway():
] ]
)["RouteTables"] )["RouteTables"]
# First assocation is the main verify[0]["Associations"].should.have.length_of(1)
verify[0]["Associations"][0]["Main"].should.equal(True)
# Second is our Gateway verify[0]["Associations"][0]["Main"].should.equal(False)
verify[0]["Associations"][1]["Main"].should.equal(False) verify[0]["Associations"][0]["GatewayId"].should.equal(igw_id)
verify[0]["Associations"][1]["GatewayId"].should.equal(igw_id) verify[0]["Associations"][0]["RouteTableAssociationId"].should.equal(assoc_id)
verify[0]["Associations"][1]["RouteTableAssociationId"].should.equal(assoc_id) verify[0]["Associations"][0].doesnt.have.key("SubnetId")
verify[0]["Associations"][1].doesnt.have.key("SubnetId")
@mock_ec2 @mock_ec2
@ -977,11 +1014,9 @@ def test_associate_route_table_by_subnet():
] ]
)["RouteTables"] )["RouteTables"]
# First assocation is the main verify[0]["Associations"].should.have.length_of(1)
verify[0]["Associations"][0].should.have.key("Main").equals(True)
# Second is our Gateway verify[0]["Associations"][0]["Main"].should.equal(False)
verify[0]["Associations"][1]["Main"].should.equal(False) verify[0]["Associations"][0]["SubnetId"].should.equals(subnet_id)
verify[0]["Associations"][1]["SubnetId"].should.equals(subnet_id) verify[0]["Associations"][0]["RouteTableAssociationId"].should.equal(assoc_id)
verify[0]["Associations"][1]["RouteTableAssociationId"].should.equal(assoc_id) verify[0]["Associations"][0].doesnt.have.key("GatewayId")
verify[0]["Associations"][1].doesnt.have.key("GatewayId")