8000 add support for VPC endpoints for private API Gateway REST APIs (#7905) · codeperl/localstack@5f13258 · GitHub
[go: up one dir, main page]

Skip to content

Commit 5f13258

Browse files
authored
add support for VPC endpoints for private API Gateway REST APIs (localstack#7905)
1 parent aeb3000 commit 5f13258

File tree

9 files changed

+409
-14
lines changed

9 files changed

+409
-14
lines changed

localstack/services/apigateway/helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
r"^/restapis/([A-Za-z0-9_\\-]+)(?:/([A-Za-z0-9\_($|%%24)\\-]+))?/%s/(.*)$" % PATH_USER_REQUEST
4545
)
4646
# URL pattern for invocations
47-
HOST_REGEX_EXECUTE_API = r"(?:.*://)?([a-zA-Z0-9-]+)\.execute-api\.(localhost.localstack.cloud|([^\.]+)\.amazonaws\.com)(.*)"
47+
HOST_REGEX_EXECUTE_API = r"(?:.*://)?([a-zA-Z0-9]+)(?:(-vpce-[^.]+))?\.execute-api\.(localhost.localstack.cloud|([^\.]+)\.amazonaws\.com)(.*)"
4848

4949
# regex path patterns
5050
PATH_REGEX_MAIN = r"^/restapis/([A-Za-z0-9_\-]+)/[a-z]+(\?.*)?&qu 10000 ot;

localstack/services/apigateway/invocations.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@ def run_authorizer(invocation_context: ApiInvocationContext, authorizer: Dict):
137137

138138

139139
def authorize_invocation(invocation_context: ApiInvocationContext):
140-
client = aws_stack.connect_to_service("apigateway")
140+
region_name = invocation_context.region_name or aws_stack.get_region()
141+
client = aws_stack.connect_to_service("apigateway", region_name=region_name)
141142
authorizers = client.get_authorizers(restApiId=invocation_context.api_id, limit=100).get(
142143
"items", []
143144
)

localstack/services/apigateway/router_asf.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,27 +99,28 @@ def __init__(self, router: Router[Handler]):
9999
def register_routes(self) -> None:
100100
"""Registers parameterized routes for API Gateway user invocations."""
101101
if self.registered:
102-
LOG.debug("Skipped API gateway route registration (routes already registered).")
102+
LOG.debug("Skipped API Gateway route registration (routes already registered).")
103103
return
104104
self.registered = True
105-
LOG.debug("Registering parameterized API gateway routes.")
105+
LOG.debug("Registering parameterized API Gateway routes.")
106+
host_pattern = "<regex('[^-]+'):api_id><regex('(-vpce-[^.]+)?'):vpce_suffix>.execute-api.<regex('.*'):server>"
106107
self.router.add(
107108
"/",
108-
host="<api_id>.execute-api.<regex('.*'):server>",
109+
host=host_pattern,
109110
endpoint=self.invoke_rest_api,
110111
defaults={"path": "", "stage": None},
111112
strict_slashes=True,
112113
)
113114
self.router.add(
114115
"/<stage>/",
115-
host="<api_id>.execute-api.<regex('.*'):server>",
116+
host=host_pattern,
116117
endpoint=self.invoke_rest_api,
117118
defaults={"path": ""},
118119
strict_slashes=False,
119120
)
120121
self.router.add(
121122
"/<stage>/<path:path>",
122-
host="<api_id>.execute-api.<regex('.*'):server>",
123+
host=host_pattern,
123124
endpoint=self.invoke_rest_api,
124125
strict_slashes=True,
125126
)
@@ -136,10 +137,12 @@ def register_routes(self) -> None:
136137
strict_slashes=True,
137138
)
138139

139-
def invoke_rest_api(self, request: Request, **url_params: Dict[str, Any]) -> Response:
140-
if not get_api_account_id_and_region(url_params["api_id"])[1]:
140+
def invoke_rest_api(self, request: Request, **url_params: Dict[str, str]) -> Response:
141+
_, region_name = get_api_account_id_and_region(url_params["api_id"])
142+
if not region_name:
141143
return Response(status=404)
142144
invocation_context = to_invocation_context(request, url_params)
145+
invocation_context.region_name = region_name
143146
result = invoke_rest_api_from_request(invocation_context)
144147
if result is not None:
145148
return convert_response(result)

localstack/services/awslambda/lambda_utils.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,6 @@ def store_lambda_logs(
185185

186186

187187
def get_main_endpoint_from_container() -> str:
188-
global DOCKER_MAIN_CONTAINER_IP
189188
if config.HOSTNAME_FROM_LAMBDA:
190189
return config.HOSTNAME_FROM_LAMBDA
191190
return get_endpoint_for_network(network=get_container_network_for_lambda())

localstack/services/ec2/provider.py

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1+
import json
12
import re
23
from abc import ABC
34
from datetime import datetime, timezone
45

56
from botocore.parsers import ResponseParserError
67
from moto.core.utils import camelcase_to_underscores, underscores_to_camelcase
78
from moto.ec2.exceptions import InvalidVpcEndPointIdError
8-
from moto.ec2.models import SubnetBackend, TransitGatewayAttachmentBackend
9+
from moto.ec2.models import (
10+
EC2Backend,
11+
SubnetBackend,
12+
TransitGatewayAttachmentBackend,
13+
VPCBackend,
14+
ec2_backends,
15+
)
916
from moto.ec2.models.launch_templates import LaunchTemplate as MotoLaunchTemplate
1017
from moto.ec2.models.subnets import Subnet
18+
from moto.ec2.models.vpcs import VPCEndPoint
1119

1220
from localstack.aws.api import RequestContext, handler
1321
from localstack.aws.api.ec2 import (
@@ -30,7 +38,13 @@
3038
DescribeSubnetsResult,
3139
DescribeTransitGatewaysRequest,
3240
DescribeTransitGatewaysResult,
41+
DescribeVpcEndpointServicesRequest,
42+
DescribeVpcEndpointServicesResult,
43+
DescribeVpcEndpointsRequest,
44+
DescribeVpcEndpointsResult,
45+
DnsOptions,
3346
DnsOptionsSpecification,
47+
DnsRecordIpType,
3448
Ec2Api,
3549
InstanceType,
3650
IpAddressType,
@@ -68,7 +82,7 @@
6882
from localstack.services.moto import call_moto
6983
from localstack.utils.aws import aws_stack
7084
from localstack.utils.patch import patch
71-
from localstack.utils.strings import first_char_to_upper, long_uid
85+
from localstack.utils.strings import first_char_to_upper, long_uid, short_uid
7286

7387
# additional subnet attributes not yet supported upstream
7488
ADDITIONAL_SUBNET_ATTRS = ("private_dns_name_options_on_launch", "enable_dns64")
@@ -381,6 +395,66 @@ def modify_launch_template(
381395

382396
return result
383397

398+
@handler("DescribeVpcEndpointServices", expand=False)
399+
def describe_vpc_endpoint_services(
400+
self,
401+
context: RequestContext,
402+
request: DescribeVpcEndpointServicesRequest,
403+
) -> DescribeVpcEndpointServicesResult:
404+
ep_services = VPCBackend._collect_default_endpoint_services(
405+
account_id=context.account_id, region=context.region
406+
)
407+
408+
moto_backend = get_moto_backend(context)
409+
service_names = [s["ServiceName"] for s in ep_services]
410+
execute_api_name = f"com.amazonaws.{context.region}.execute-api"
411+
412+
if execute_api_name not in service_names:
413+
# ensure that the service entry for execute-api exists
414+
zones = moto_backend.describe_availability_zones()
415+
zones = [zone.name for zone in zones]
416+
private_dns_name = f"*.execute-api.{context.region}.amazonaws.com"
417+
service = {
418+
"ServiceName": execute_api_name,
419+
"ServiceId": f"vpce-svc-{short_uid()}",
420+
"ServiceType": [{"ServiceType": "Interface"}],
421+
"AvailabilityZones": zones,
422+
"Owner": "amazon",
423+
"BaseEndpointDnsNames": [f"execute-api.{context.region}.vpce.amazonaws.com"],
424+
"PrivateDnsName": private_dns_name,
425+
"PrivateDnsNames": [{"PrivateDnsName": private_dns_name}],
426+
"VpcEndpointPolicySupported": True,
427+
"AcceptanceRequired": False,
428+
"ManagesVpcEndpoints": False,
429+
"PrivateDnsNameVerificationState": "verified",
430+
"SupportedIpAddressTypes": ["ipv4"],
431+
}
432+
ep_services.append(service)
433+
434+
return call_moto(context)
435+
436+
@handler("DescribeVpcEndpoints", expand=False)
437+
def describe_vpc_endpoints(
438+
self,
439+
context: RequestContext,
440+
request: DescribeVpcEndpointsRequest,
441+
) -> DescribeVpcEndpointsResult:
442+
result: DescribeVpcEndpointsResult = call_moto(context)
443+
444+
for endpoint in result.get("VpcEndpoints"):
445+
endpoint.setdefault("DnsOptions", DnsOptions(DnsRecordIpType=DnsRecordIpType.ipv4))
446+
endpoint.setdefault("IpAddressType", IpAddressType.ipv4)
447+
endpoint.setdefault("RequesterManaged", False)
448+
endpoint.setdefault("RouteTableIds", [])
449+
# AWS parity: Version should not be contained in the policy response
450+
policy = endpoint.get("PolicyDocument")
451+
if policy and '"Version":' in policy:
452+
policy = json.loads(policy)
453+
policy.pop("Version", None)
454+
endpoint["PolicyDocument"] = json.dumps(policy)
455+
456+
return result
457+
384458

385459
@patch(SubnetBackend.modify_subnet_attribute)
386460
def modify_subnet_attribute(fn, self, subnet_id, attr_name, attr_value):
@@ -399,6 +473,11 @@ def modify_subnet_attribute(fn, self, subnet_id, attr_name, attr_value):
399473
return fn(self, subnet_id, attr_name, attr_value)
400474

401475

476+
def get_moto_backend(context: RequestContext) -> EC2Backend:
477+
"""Get the moto EC2 backend for the given request context"""
478+
return ec2_backends[context.account_id][context.region]
479+
480+
402481
@patch(Subnet.get_filter_value)
403482
def get_filter_value(fn, self, filter_name):
404483
if filter_name in (
@@ -414,3 +493,8 @@ def delete_transit_gateway_vpc_attachment(fn, self, transit_gateway_attachment_i
414493
transit_gateway_attachment = self.transit_gateway_attachments.get(transit_gateway_attachment_id)
415494
transit_gateway_attachment.state = "deleted"
416495
return transit_gateway_attachment
496+
497+
498+
# fix a bug in upstream moto where a space is encoded in the "Statement" key - TODO remove once fixed upstream
499+
if "Statement " in VPCEndPoint.DEFAULT_POLICY:
500+
VPCEndPoint.DEFAULT_POLICY["Statement"] = VPCEndPoint.DEFAULT_POLICY.pop("Statement ")

localstack/testing/pytest/fixtures.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1880,6 +1880,41 @@ def factory(email_address: str) -> None:
18801880
ses_client.delete_identity(Identity=identity)
18811881

18821882

1883+
@pytest.fixture
1884+
def ec2_create_security_group(ec2_client):
1885+
ec2_sgs = []
1886+
1887+
def factory(ports=None, **kwargs):
1888+
if "GroupName" not in kwargs:
1889+
kwargs["GroupName"] = f"test-sg-{short_uid()}"
1890+
security_group = ec2_client.create_security_group(**kwargs)
1891+
1892+
permissions = [
1893+
{
1894+
"FromPort": port,
1895+
"IpProtocol": "tcp",
1896+
"IpRanges": [{"CidrIp": "0.0.0.0/0"}],
1897+
"ToPort": port,
1898+
}
1899+
for port in ports or []
1900+
]
1901+
ec2_client.authorize_security_group_ingress(
1902+
GroupName=kwargs["GroupName"],
1903+
IpPermissions=permissions,
1904+
)
1905+
1906+
ec2_sgs.append(security_group["GroupId"])
1907+
return security_group
1908+
1909+
yield factory
1910+
1911+
for sg_group_id in ec2_sgs:
1912+
try:
1913+
ec2_client.delete_security_group(GroupId=sg_group_id)
1914+
except Exception as e:
1915+
LOG.debug("Error cleaning up EC2 security group: %s, %s", sg_group_id, e)
1916+
1917+
18831918
@pytest.fixture
18841919
def cleanups(ec2_client):
18851920
cleanup_fns = []

tests/integration/apigateway/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def _factory(
8585
resourceId=resource_id,
8686
httpMethod="POST",
8787
authorizationType="NONE",
88+
apiKeyRequired=False,
8889
)
8990

9091
# set AWS policy to give API GW access to backend resources

0 commit comments

Comments
 (0)
0