Skip to content
This repository was archived by the owner on Mar 23, 2026. It is now read-only.

Commit ee0d438

Browse files
authored
Apigw/fix vpc links return value (#13532)
1 parent 532d755 commit ee0d438

File tree

6 files changed

+384
-13
lines changed

6 files changed

+384
-13
lines changed

localstack-core/localstack/services/apigateway/legacy/provider.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
UsagePlans,
9595
VpcLink,
9696
VpcLinks,
97+
VpcLinkStatus,
9798
)
9899
from localstack.aws.connect import connect_to
99100
from localstack.aws.forwarder import create_aws_request_context
@@ -1821,11 +1822,31 @@ def create_vpc_link(
18211822
tags: MapOfStringToString = None,
18221823
**kwargs,
18231824
) -> VpcLink:
1825+
# TODO add tag support
1826+
if not name:
1827+
raise BadRequestException("Name cannot be empty")
1828+
for arn in target_arns:
1829+
try:
1830+
parse_arn(arn)
1831+
except InvalidArnException:
1832+
raise BadRequestException(f"Invalid ARN. ARN is not well formed {arn}")
1833+
18241834
region_details = get_apigateway_store(context=context)
18251835
link_id = short_uid()
1826-
entry = {"id": link_id, "status": "AVAILABLE"}
1836+
entry = {
1837+
"id": link_id,
1838+
"status": VpcLinkStatus.PENDING,
1839+
"name": name,
1840+
"description": description,
1841+
"targetArns": target_arns,
1842+
}
18271843
region_details.vpc_links[link_id] = entry
18281844
result = to_vpc_link_response_json(entry)
1845+
1846+
# update the status and status message of the store object
1847+
entry["status"] = VpcLinkStatus.AVAILABLE
1848+
entry["statusMessage"] = "Your vpc link is ready for use"
1849+
18291850
return VpcLink(**result)
18301851

18311852
def get_vpc_links(
@@ -1845,7 +1866,7 @@ def get_vpc_link(self, context: RequestContext, vpc_link_id: String, **kwargs) -
18451866
region_details = get_apigateway_store(context=context)
18461867
vpc_link = region_details.vpc_links.get(vpc_link_id)
18471868
if vpc_link is None:
1848-
raise NotFoundException(f'VPC link ID "{vpc_link_id}" not found')
1869+
raise NotFoundException("Invalid Vpc Link identifier specified")
18491870
result = to_vpc_link_response_json(vpc_link)
18501871
return VpcLink(**result)
18511872

@@ -1859,7 +1880,7 @@ def update_vpc_link(
18591880
region_details = get_apigateway_store(context=context)
18601881
vpc_link = region_details.vpc_links.get(vpc_link_id)
18611882
if vpc_link is None:
1862-
raise NotFoundException(f'VPC link ID "{vpc_link_id}" not found')
1883+
raise NotFoundException("Invalid Vpc Link identifier specified")
18631884
result = apply_json_patch_safe(vpc_link, patch_operations)
18641885
result = to_vpc_link_response_json(result)
18651886
return VpcLink(**result)
@@ -1868,7 +1889,7 @@ def delete_vpc_link(self, context: RequestContext, vpc_link_id: String, **kwargs
18681889
region_details = get_apigateway_store(context=context)
18691890
vpc_link = region_details.vpc_links.pop(vpc_link_id, None)
18701891
if vpc_link is None:
1871-
raise NotFoundException(f'VPC link ID "{vpc_link_id}" not found for deletion')
1892+
raise NotFoundException("Invalid Vpc Link identifier specified")
18721893

18731894
# request validators
18741895

tests/aws/services/apigateway/conftest.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from itertools import count
2+
from typing import TYPE_CHECKING
23

34
import pytest
45
from botocore.config import Config
@@ -19,6 +20,9 @@
1920
)
2021
from tests.aws.services.lambda_.test_lambda import TEST_LAMBDA_PYTHON_ECHO_STATUS_CODE
2122

23+
if TYPE_CHECKING:
24+
from mypy_boto3_ec2.type_defs import VpcTypeDef
25+
2226
# default name used for created REST API stages
2327
DEFAULT_STAGE_NAME = "dev"
2428

@@ -297,3 +301,12 @@ def transform_response(response: dict) -> dict:
297301
return response
298302

299303
return transform_response
304+
305+
306+
@pytest.fixture
307+
def default_vpc(aws_client) -> "VpcTypeDef":
308+
vpcs = aws_client.ec2.describe_vpcs()
309+
for vpc in vpcs["Vpcs"]:
310+
if vpc.get("IsDefault"):
311+
return vpc
312+
raise Exception("Default VPC not found")

tests/aws/services/apigateway/test_apigateway_api.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
import os.path
44
import time
55
from operator import itemgetter
6+
from typing import TYPE_CHECKING, Unpack
67

78
import pytest
89
from botocore.config import Config
910
from botocore.exceptions import ClientError
1011
from localstack_snapshot.snapshots.transformer import SortingTransformer
1112

1213
from localstack.aws.api.apigateway import PutMode
14+
from localstack.aws.connect import ServiceLevelClientFactory
1315
from localstack.testing.aws.util import is_aws_cloud
1416
from localstack.testing.pytest import markers
1517
from localstack.utils.files import load_file
@@ -24,6 +26,10 @@
2426
)
2527
from tests.aws.services.apigateway.conftest import is_next_gen_api
2628

29+
if TYPE_CHECKING:
30+
from mypy_boto3_apigateway.type_defs import CreateVpcLinkRequestTypeDef, VpcLinkResponseTypeDef
31+
32+
2733
LOG = logging.getLogger(__name__)
2834

2935
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -98,6 +104,28 @@ def _factory(*args, **kwargs):
98104
delete_rest_api_retry(apigateway_client, rest_api_id)
99105

100106

107+
@pytest.fixture
108+
def apigw_create_vpc_link(aws_client):
109+
vpc_links: list[tuple[ServiceLevelClientFactory, str]] = []
110+
111+
def _create_vpc_link(
112+
client: ServiceLevelClientFactory | None = None,
113+
**kwargs: Unpack["CreateVpcLinkRequestTypeDef"],
114+
) -> "VpcLinkResponseTypeDef":
115+
client = client or aws_client
116+
vpc_link = client.apigateway.create_vpc_link(**kwargs)
117+
vpc_links.append((client, vpc_link["id"]))
118+
return vpc_link
119+
120+
yield _create_vpc_link
121+
122+
for _client, vpc_link_id in vpc_links:
123+
try:
124+
_client.apigateway.delete_vpc_link(vpcLinkId=vpc_link_id)
125+
except ClientError as e:
126+
LOG.error("Error deleting VPC link: %s", e)
127+
128+
101129
class TestApiGatewayApiRestApi:
102130
@markers.aws.validated
103131
def test_list_and_delete_apis(self, apigw_create_rest_api, snapshot, aws_client):
@@ -2652,6 +2680,135 @@ def test_update_gateway_response(
26522680
)
26532681

26542682

2683+
class TestApiGatewayVpcLink:
2684+
@markers.aws.validated
2685+
@markers.snapshot.skip_snapshot_verify(paths=["$.update_vpc_link.tags", "$.get_vpc_link.tags"])
2686+
def test_vpc_link_lifecycle(
2687+
self,
2688+
aws_client,
2689+
snapshot,
2690+
cleanups,
2691+
default_vpc,
2692+
region_name,
2693+
apigw_create_vpc_link,
2694+
account_id,
2695+
):
2696+
snapshot.add_transformer(snapshot.transform.key_value("nlb-arn"))
2697+
2698+
retries = 240 if is_aws_cloud() else 3
2699+
sleep = 3 if is_aws_cloud() else 1
2700+
2701+
if is_aws_cloud():
2702+
vpc_id = default_vpc["VpcId"]
2703+
subnets = aws_client.ec2.describe_subnets(
2704+
Filters=[{"Name": "vpc-id", "Values": [vpc_id]}]
2705+
)["Subnets"]
2706+
# require at least 2 subnets for the NLB
2707+
assert len(subnets) >= 2
2708+
nlb = aws_client.elbv2.create_load_balancer(
2709+
Name=f"nlb-{short_uid()}",
2710+
Type="network",
2711+
Subnets=[subnets[0]["SubnetId"], subnets[1]["SubnetId"]],
2712+
)["LoadBalancers"][0]
2713+
nlb_arn = nlb["LoadBalancerArn"]
2714+
cleanups.append(lambda: aws_client.elbv2.delete_load_balancer(LoadBalancerArn=nlb_arn))
2715+
waiter = aws_client.elbv2.get_waiter("load_balancer_available")
2716+
waiter.wait(
2717+
LoadBalancerArns=[nlb_arn], WaiterConfig={"Delay": sleep, "MaxAttempts": retries}
2718+
)
2719+
else:
2720+
# ElbV2 is not available in community, so we just use a dummy arn
2721+
nlb_arn = f"arn:aws:elasticloadbalancing:{region_name}:{account_id}:loadbalancer/net/my-load-balancer/50dc6c495c0c9188"
2722+
2723+
snapshot.match("nlb-arn", nlb_arn)
2724+
2725+
# create vpc link
2726+
vpc_link_name = f"test-vpc-link-{short_uid()}"
2727+
create_vpc_link_response = apigw_create_vpc_link(name=vpc_link_name, targetArns=[nlb_arn])
2728+
snapshot.match("create_vpc_link", create_vpc_link_response)
2729+
vpc_link_id = create_vpc_link_response["id"]
2730+
2731+
# get vpc link
2732+
# AWS needs some time to make the VPC link available
2733+
def _wait_for_vpc_link_available():
2734+
get_vpc_link_response = aws_client.apigateway.get_vpc_link(vpcLinkId=vpc_link_id)
2735+
assert get_vpc_link_response["status"] == "AVAILABLE"
2736+
return get_vpc_link_response
2737+
2738+
vpc_link_response = retry(_wait_for_vpc_link_available, retries=retries, sleep=sleep)
2739+
snapshot.match("get_vpc_link", vpc_link_response)
2740+
2741+
# get vpc links
2742+
get_vpc_links_response = aws_client.apigateway.get_vpc_links()
2743+
# for the snapshot to be stable, we need to filter for the VPC link we created
2744+
get_vpc_links_response["items"] = [
2745+
item for item in get_vpc_links_response["items"] if item["id"] == vpc_link_id
2746+
]
2747+
snapshot.match("get_vpc_links", get_vpc_links_response)
2748+
2749+
# update vpc link
2750+
patch_operations = [
2751+
{"op": "replace", "path": "/name", "value": f"{vpc_link_name}-updated"},
2752+
]
2753+
update_vpc_link_response = aws_client.apigateway.update_vpc_link(
2754+
vpcLinkId=vpc_link_id, patchOperations=patch_operations
2755+
)
2756+
snapshot.match("update_vpc_link", update_vpc_link_response)
2757+
2758+
delete_response = aws_client.apigateway.delete_vpc_link(vpcLinkId=vpc_link_id)
2759+
snapshot.match("delete_vpc_link", delete_response)
2760+
2761+
def _wait_for_deleted():
2762+
try:
2763+
vpc_link = aws_client.apigateway.get_vpc_link(vpcLinkId=vpc_link_id)
2764+
# this assertion shouldn't happen, but this will ensure failure if the vpc link is still being deleted
2765+
assert vpc_link["status"] == "DELETED"
2766+
except aws_client.apigateway.exceptions.NotFoundException:
2767+
pass
2768+
2769+
# waiting for delete, as it takes a long time and would prevent NLB deletion
2770+
retry(_wait_for_deleted, retries=retries, sleep=sleep)
2771+
2772+
@markers.aws.validated
2773+
def test_create_vpc_link_invalid_parameters(self, aws_client, snapshot):
2774+
with pytest.raises(ClientError) as e:
2775+
aws_client.apigateway.create_vpc_link(
2776+
name=f"test-vpc-link-{short_uid()}",
2777+
targetArns=["invalid-arn"],
2778+
)
2779+
snapshot.match("create_vpc_link_invalid_target_arn", e.value.response)
2780+
2781+
with pytest.raises(ClientError) as e:
2782+
aws_client.apigateway.create_vpc_link(
2783+
name="",
2784+
targetArns=[],
2785+
)
2786+
snapshot.match("create_vpc_link_empty_name", e.value.response)
2787+
2788+
@markers.aws.validated
2789+
def test_get_vpc_link_invalid_id(self, aws_client, snapshot):
2790+
with pytest.raises(ClientError) as e:
2791+
aws_client.apigateway.get_vpc_link(vpcLinkId="invalid-id")
2792+
snapshot.match("get_vpc_link_invalid_id", e.value.response)
2793+
2794+
@markers.aws.validated
2795+
def test_delete_vpc_link_invalid_id(self, aws_client, snapshot):
2796+
with pytest.raises(ClientError) as e:
2797+
aws_client.apigateway.delete_vpc_link(vpcLinkId="invalid-id")
2798+
snapshot.match("delete_vpc_link_invalid_id", e.value.response)
2799+
2800+
@markers.aws.validated
2801+
def test_update_vpc_link_invalid_id(self, aws_client, snapshot):
2802+
patch_operations = [
2803+
{"op": "replace", "path": "/name", "value": "new-name"},
2804+
]
2805+
with pytest.raises(ClientError) as e:
2806+
aws_client.apigateway.update_vpc_link(
2807+
vpcLinkId="invalid-id", patchOperations=patch_operations
2808+
)
2809+
snapshot.match("update_vpc_link_invalid_id", e.value.response)
2810+
2811+
26552812
class TestApigatewayTestInvoke:
26562813
@markers.aws.validated
26572814
def test_invoke_test_method(

0 commit comments

Comments
 (0)