EC2: Cross-account transit gateway peering attachments (#6851)
This commit is contained in:
parent
bc9c8ef8ab
commit
fea098310a
@ -436,6 +436,14 @@ class InvalidParameterValueError(EC2ClientError):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidParameterValueErrorPeeringAttachment(EC2ClientError):
|
||||||
|
def __init__(self, operation: str, transit_gateway_attachment_id: str):
|
||||||
|
super().__init__(
|
||||||
|
"InvalidParameterValue",
|
||||||
|
f"Cannot {operation} {transit_gateway_attachment_id} as the source of the peering request.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InvalidParameterValueErrorTagSpotFleetRequest(EC2ClientError):
|
class InvalidParameterValueErrorTagSpotFleetRequest(EC2ClientError):
|
||||||
def __init__(self, resource_type: str):
|
def __init__(self, resource_type: str):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
from typing import Any, Dict, List, Optional
|
from collections import defaultdict
|
||||||
|
import weakref
|
||||||
|
from typing import Any, Dict, List, Optional, Iterator
|
||||||
from moto.core.utils import iso_8601_datetime_with_milliseconds, utcnow
|
from moto.core.utils import iso_8601_datetime_with_milliseconds, utcnow
|
||||||
from moto.utilities.utils import merge_multiple_dicts, filter_resources
|
from moto.utilities.utils import merge_multiple_dicts, filter_resources
|
||||||
from .core import TaggedEC2Resource
|
from .core import TaggedEC2Resource
|
||||||
from .vpc_peering_connections import PeeringConnectionStatus
|
from .vpc_peering_connections import PeeringConnectionStatus
|
||||||
|
from ..exceptions import InvalidParameterValueErrorPeeringAttachment
|
||||||
from ..utils import random_transit_gateway_attachment_id, describe_tag_filter
|
from ..utils import random_transit_gateway_attachment_id, describe_tag_filter
|
||||||
|
|
||||||
|
|
||||||
@ -99,8 +102,20 @@ class TransitGatewayPeeringAttachment(TransitGatewayAttachment):
|
|||||||
|
|
||||||
|
|
||||||
class TransitGatewayAttachmentBackend:
|
class TransitGatewayAttachmentBackend:
|
||||||
|
backend_refs = defaultdict(set) # type: ignore
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.transit_gateway_attachments: Dict[str, TransitGatewayAttachment] = {}
|
self.transit_gateway_attachments: Dict[str, TransitGatewayAttachment] = {}
|
||||||
|
self.backend_refs[self.__class__].add(weakref.ref(self))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_peering_attachment_backend_refs(
|
||||||
|
cls,
|
||||||
|
) -> Iterator["TransitGatewayAttachmentBackend"]:
|
||||||
|
for backend_ref in cls.backend_refs[cls]:
|
||||||
|
backend = backend_ref()
|
||||||
|
if backend is not None:
|
||||||
|
yield backend
|
||||||
|
|
||||||
def create_transit_gateway_vpn_attachment(
|
def create_transit_gateway_vpn_attachment(
|
||||||
self,
|
self,
|
||||||
@ -276,11 +291,24 @@ class TransitGatewayAttachmentBackend:
|
|||||||
tags=tags,
|
tags=tags,
|
||||||
region_name=self.region_name, # type: ignore[attr-defined]
|
region_name=self.region_name, # type: ignore[attr-defined]
|
||||||
)
|
)
|
||||||
transit_gateway_peering_attachment.status.accept()
|
|
||||||
transit_gateway_peering_attachment.state = "available"
|
|
||||||
self.transit_gateway_attachments[
|
self.transit_gateway_attachments[
|
||||||
transit_gateway_peering_attachment.id
|
transit_gateway_peering_attachment.id
|
||||||
] = transit_gateway_peering_attachment
|
] = transit_gateway_peering_attachment
|
||||||
|
|
||||||
|
# If the peer is not same as the current account or region, create attachment in peer backend
|
||||||
|
if self.account_id != peer_account_id or self.region_name != peer_region: # type: ignore[attr-defined]
|
||||||
|
for backend in self._get_peering_attachment_backend_refs():
|
||||||
|
if (
|
||||||
|
backend.account_id == peer_account_id # type: ignore[attr-defined]
|
||||||
|
and backend.region_name == peer_region # type: ignore[attr-defined]
|
||||||
|
):
|
||||||
|
backend.transit_gateway_attachments[
|
||||||
|
transit_gateway_peering_attachment.id
|
||||||
|
] = transit_gateway_peering_attachment
|
||||||
|
|
||||||
|
transit_gateway_peering_attachment.status.pending()
|
||||||
|
transit_gateway_peering_attachment.state = "pendingAcceptance"
|
||||||
return transit_gateway_peering_attachment
|
return transit_gateway_peering_attachment
|
||||||
|
|
||||||
def describe_transit_gateway_peering_attachments(
|
def describe_transit_gateway_peering_attachments(
|
||||||
@ -319,6 +347,23 @@ class TransitGatewayAttachmentBackend:
|
|||||||
transit_gateway_attachment = self.transit_gateway_attachments[
|
transit_gateway_attachment = self.transit_gateway_attachments[
|
||||||
transit_gateway_attachment_id
|
transit_gateway_attachment_id
|
||||||
]
|
]
|
||||||
|
|
||||||
|
requester_account_id = transit_gateway_attachment.requester_tgw_info["ownerId"] # type: ignore[attr-defined]
|
||||||
|
requester_region_name = transit_gateway_attachment.requester_tgw_info["region"] # type: ignore[attr-defined]
|
||||||
|
accepter_account_id = transit_gateway_attachment.accepter_tgw_info["ownerId"] # type: ignore[attr-defined]
|
||||||
|
accepter_region_name = transit_gateway_attachment.accepter_tgw_info["region"] # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
# For cross-account peering, must be accepted by the accepter
|
||||||
|
if requester_account_id != accepter_account_id and self.account_id != accepter_account_id: # type: ignore[attr-defined]
|
||||||
|
raise InvalidParameterValueErrorPeeringAttachment(
|
||||||
|
"accept", transit_gateway_attachment_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if requester_region_name != accepter_region_name and self.region_name != accepter_region_name: # type: ignore[attr-defined]
|
||||||
|
raise InvalidParameterValueErrorPeeringAttachment(
|
||||||
|
"accept", transit_gateway_attachment_id
|
||||||
|
)
|
||||||
|
|
||||||
transit_gateway_attachment.state = "available"
|
transit_gateway_attachment.state = "available"
|
||||||
# Bit dodgy - we just assume that we act on a TransitGatewayPeeringAttachment
|
# Bit dodgy - we just assume that we act on a TransitGatewayPeeringAttachment
|
||||||
# We could just as easily have another sub-class of TransitGatewayAttachment on our hands, which does not have a status-attribute
|
# We could just as easily have another sub-class of TransitGatewayAttachment on our hands, which does not have a status-attribute
|
||||||
@ -331,6 +376,22 @@ class TransitGatewayAttachmentBackend:
|
|||||||
transit_gateway_attachment = self.transit_gateway_attachments[
|
transit_gateway_attachment = self.transit_gateway_attachments[
|
||||||
transit_gateway_attachment_id
|
transit_gateway_attachment_id
|
||||||
]
|
]
|
||||||
|
|
||||||
|
requester_account_id = transit_gateway_attachment.requester_tgw_info["ownerId"] # type: ignore[attr-defined]
|
||||||
|
requester_region_name = transit_gateway_attachment.requester_tgw_info["region"] # type: ignore[attr-defined]
|
||||||
|
accepter_account_id = transit_gateway_attachment.accepter_tgw_info["ownerId"] # type: ignore[attr-defined]
|
||||||
|
accepter_region_name = transit_gateway_attachment.requester_tgw_info["region"] # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
if requester_account_id != accepter_account_id and self.account_id != accepter_account_id: # type: ignore[attr-defined]
|
||||||
|
raise InvalidParameterValueErrorPeeringAttachment(
|
||||||
|
"reject", transit_gateway_attachment_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if requester_region_name != accepter_region_name and self.region_name != accepter_region_name: # type: ignore[attr-defined]
|
||||||
|
raise InvalidParameterValueErrorPeeringAttachment(
|
||||||
|
"reject", transit_gateway_attachment_id
|
||||||
|
)
|
||||||
|
|
||||||
transit_gateway_attachment.state = "rejected"
|
transit_gateway_attachment.state = "rejected"
|
||||||
transit_gateway_attachment.status.reject() # type: ignore[attr-defined]
|
transit_gateway_attachment.status.reject() # type: ignore[attr-defined]
|
||||||
return transit_gateway_attachment
|
return transit_gateway_attachment
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
|
import pytest
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
|
|
||||||
from moto import mock_ec2, settings
|
from moto import mock_ec2, settings
|
||||||
from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
|
from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
|
||||||
from unittest import SkipTest
|
from unittest import SkipTest, mock
|
||||||
|
|
||||||
|
|
||||||
@mock_ec2
|
@mock_ec2
|
||||||
@ -89,7 +94,7 @@ def test_describe_transit_gateway_peering_attachment_by_filters():
|
|||||||
)["TransitGatewayPeeringAttachments"]
|
)["TransitGatewayPeeringAttachments"]
|
||||||
assert [a["TransitGatewayAttachmentId"] for a in find_3] == [attchmnt3]
|
assert [a["TransitGatewayAttachmentId"] for a in find_3] == [attchmnt3]
|
||||||
|
|
||||||
filters = [{"Name": "state", "Values": ["available"]}]
|
filters = [{"Name": "state", "Values": ["pendingAcceptance"]}]
|
||||||
find_all = retrieve_all_attachments(ec2, filters)
|
find_all = retrieve_all_attachments(ec2, filters)
|
||||||
all_ids = [a["TransitGatewayAttachmentId"] for a in find_all]
|
all_ids = [a["TransitGatewayAttachmentId"] for a in find_all]
|
||||||
assert attchmnt1 in all_ids
|
assert attchmnt1 in all_ids
|
||||||
@ -105,7 +110,7 @@ def test_describe_transit_gateway_peering_attachment_by_filters():
|
|||||||
|
|
||||||
find_available = ec2.describe_transit_gateway_peering_attachments(
|
find_available = ec2.describe_transit_gateway_peering_attachments(
|
||||||
TransitGatewayAttachmentIds=[attchmnt1, attchmnt2],
|
TransitGatewayAttachmentIds=[attchmnt1, attchmnt2],
|
||||||
Filters=[{"Name": "state", "Values": ["available"]}],
|
Filters=[{"Name": "state", "Values": ["pendingAcceptance"]}],
|
||||||
)["TransitGatewayPeeringAttachments"]
|
)["TransitGatewayPeeringAttachments"]
|
||||||
assert [a["TransitGatewayAttachmentId"] for a in find_available] == [attchmnt1]
|
assert [a["TransitGatewayAttachmentId"] for a in find_available] == [attchmnt1]
|
||||||
|
|
||||||
@ -119,7 +124,9 @@ def test_create_and_accept_transit_gateway_peering_attachment():
|
|||||||
gateway_id2 = ec2.create_transit_gateway(Description="my second gateway")[
|
gateway_id2 = ec2.create_transit_gateway(Description="my second gateway")[
|
||||||
"TransitGateway"
|
"TransitGateway"
|
||||||
]["TransitGatewayId"]
|
]["TransitGatewayId"]
|
||||||
attchment_id = create_peering_attachment(ec2, gateway_id1, gateway_id2)
|
attchment_id = create_peering_attachment(
|
||||||
|
ec2, gateway_id1, gateway_id2, peer_region="us-west-1"
|
||||||
|
)
|
||||||
|
|
||||||
ec2.accept_transit_gateway_peering_attachment(
|
ec2.accept_transit_gateway_peering_attachment(
|
||||||
TransitGatewayAttachmentId=attchment_id
|
TransitGatewayAttachmentId=attchment_id
|
||||||
@ -176,12 +183,86 @@ def test_create_and_delete_transit_gateway_peering_attachment():
|
|||||||
assert attachment["State"] == "deleted"
|
assert attachment["State"] == "deleted"
|
||||||
|
|
||||||
|
|
||||||
def create_peering_attachment(ec2, gateway_id1, gateway_id2):
|
@mock_ec2
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"account1,account2",
|
||||||
|
[
|
||||||
|
pytest.param("111111111111", "111111111111", id="within account"),
|
||||||
|
pytest.param("111111111111", "222222222222", id="across accounts"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
settings.TEST_SERVER_MODE, reason="Cannot set account ID in server mode"
|
||||||
|
)
|
||||||
|
def test_transit_gateway_peering_attachments_cross_region(account1, account2):
|
||||||
|
# create transit gateways
|
||||||
|
with mock.patch.dict(os.environ, {"MOTO_ACCOUNT_ID": account1}):
|
||||||
|
ec2_us = boto3.client("ec2", "us-west-1")
|
||||||
|
gateway_us = ec2_us.create_transit_gateway()["TransitGateway"][
|
||||||
|
"TransitGatewayId"
|
||||||
|
]
|
||||||
|
|
||||||
|
with mock.patch.dict(os.environ, {"MOTO_ACCOUNT_ID": account2}):
|
||||||
|
ec2_eu = boto3.client("ec2", "eu-central-1")
|
||||||
|
gateway_eu = ec2_eu.create_transit_gateway()["TransitGateway"][
|
||||||
|
"TransitGatewayId"
|
||||||
|
]
|
||||||
|
|
||||||
|
# create peering
|
||||||
|
with mock.patch.dict(os.environ, {"MOTO_ACCOUNT_ID": account1}):
|
||||||
|
attachment_id = create_peering_attachment(
|
||||||
|
ec2_us,
|
||||||
|
gateway_us,
|
||||||
|
gateway_eu,
|
||||||
|
peer_account=account2,
|
||||||
|
peer_region="eu-central-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ensure peering can be described by the accepter
|
||||||
|
with mock.patch.dict(os.environ, {"MOTO_ACCOUNT_ID": account2}):
|
||||||
|
response = ec2_eu.describe_transit_gateway_peering_attachments(
|
||||||
|
TransitGatewayAttachmentIds=[attachment_id]
|
||||||
|
)["TransitGatewayPeeringAttachments"][0]
|
||||||
|
assert response["TransitGatewayAttachmentId"] == attachment_id
|
||||||
|
assert response["RequesterTgwInfo"]["OwnerId"] == account1
|
||||||
|
assert response["RequesterTgwInfo"]["Region"] == "us-west-1"
|
||||||
|
assert response["AccepterTgwInfo"]["OwnerId"] == account2
|
||||||
|
assert response["AccepterTgwInfo"]["Region"] == "eu-central-1"
|
||||||
|
|
||||||
|
# ensure accepting in requester account/region raises
|
||||||
|
with mock.patch.dict(os.environ, {"MOTO_ACCOUNT_ID": account1}):
|
||||||
|
with pytest.raises(ClientError) as exc:
|
||||||
|
ec2_us.accept_transit_gateway_peering_attachment(
|
||||||
|
TransitGatewayAttachmentId=attachment_id
|
||||||
|
)
|
||||||
|
assert exc.value.response["Error"]["Code"] == "InvalidParameterValue"
|
||||||
|
assert (
|
||||||
|
exc.value.response["Error"]["Message"]
|
||||||
|
== f"Cannot accept {attachment_id} as the source of the peering request."
|
||||||
|
)
|
||||||
|
|
||||||
|
with mock.patch.dict(os.environ, {"MOTO_ACCOUNT_ID": account2}):
|
||||||
|
# ensure peering can be accepted by the accepter
|
||||||
|
response = ec2_eu.accept_transit_gateway_peering_attachment(
|
||||||
|
TransitGatewayAttachmentId=attachment_id
|
||||||
|
)
|
||||||
|
assert response["TransitGatewayPeeringAttachment"]["State"] == "available"
|
||||||
|
|
||||||
|
# ensure peering can be deleted by the accepter
|
||||||
|
response = ec2_eu.delete_transit_gateway_peering_attachment(
|
||||||
|
TransitGatewayAttachmentId=attachment_id
|
||||||
|
)
|
||||||
|
assert response["TransitGatewayPeeringAttachment"]["State"] == "deleted"
|
||||||
|
|
||||||
|
|
||||||
|
def create_peering_attachment(
|
||||||
|
ec2, gateway_id1, gateway_id2, peer_account=ACCOUNT_ID, peer_region="us-east-1"
|
||||||
|
):
|
||||||
return ec2.create_transit_gateway_peering_attachment(
|
return ec2.create_transit_gateway_peering_attachment(
|
||||||
TransitGatewayId=gateway_id1,
|
TransitGatewayId=gateway_id1,
|
||||||
PeerTransitGatewayId=gateway_id2,
|
PeerTransitGatewayId=gateway_id2,
|
||||||
PeerAccountId=ACCOUNT_ID,
|
PeerAccountId=peer_account,
|
||||||
PeerRegion="us-east-1",
|
PeerRegion=peer_region,
|
||||||
)["TransitGatewayPeeringAttachment"]["TransitGatewayAttachmentId"]
|
)["TransitGatewayPeeringAttachment"]["TransitGatewayAttachmentId"]
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user