Skip to content

Commit 05d5cdc

Browse files
leandrodamascenarafaelgsr
authored andcommitted
feat(event_handler): add VPCLatticeResolver (aws-powertools#2601)
1 parent 3e92a87 commit 05d5cdc

File tree

12 files changed

+254
-13
lines changed

12 files changed

+254
-13
lines changed

aws_lambda_powertools/event_handler/__init__.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22
Event handler decorators for common Lambda events
33
"""
44

5-
from .api_gateway import (
5+
from aws_lambda_powertools.event_handler.api_gateway import (
66
ALBResolver,
77
APIGatewayHttpResolver,
88
ApiGatewayResolver,
99
APIGatewayRestResolver,
1010
CORSConfig,
1111
Response,
1212
)
13-
from .appsync import AppSyncResolver
14-
from .lambda_function_url import LambdaFunctionUrlResolver
13+
from aws_lambda_powertools.event_handler.appsync import AppSyncResolver
14+
from aws_lambda_powertools.event_handler.lambda_function_url import (
15+
LambdaFunctionUrlResolver,
16+
)
17+
from aws_lambda_powertools.event_handler.vpc_lattice import VPCLatticeResolver
1518

1619
__all__ = [
1720
"AppSyncResolver",
@@ -22,4 +25,5 @@
2225
"CORSConfig",
2326
"LambdaFunctionUrlResolver",
2427
"Response",
28+
"VPCLatticeResolver",
2529
]

aws_lambda_powertools/event_handler/api_gateway.py

+6
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
APIGatewayProxyEvent,
3434
APIGatewayProxyEventV2,
3535
LambdaFunctionUrlEvent,
36+
VPCLatticeEvent,
3637
)
3738
from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent
3839
from aws_lambda_powertools.utilities.typing import LambdaContext
@@ -53,6 +54,7 @@ class ProxyEventType(Enum):
5354
APIGatewayProxyEvent = "APIGatewayProxyEvent"
5455
APIGatewayProxyEventV2 = "APIGatewayProxyEventV2"
5556
ALBEvent = "ALBEvent"
57+
VPCLatticeEvent = "VPCLatticeEvent"
5658
LambdaFunctionUrlEvent = "LambdaFunctionUrlEvent"
5759

5860

@@ -683,13 +685,17 @@ def _to_proxy_event(self, event: Dict) -> BaseProxyEvent:
683685
if self._proxy_type == ProxyEventType.LambdaFunctionUrlEvent:
684686
logger.debug("Converting event to Lambda Function URL contract")
685687
return LambdaFunctionUrlEvent(event)
688+
if self._proxy_type == ProxyEventType.VPCLatticeEvent:
689+
logger.debug("Converting event to VPC Lattice contract")
690+
return VPCLatticeEvent(event)
686691
logger.debug("Converting event to ALB contract")
687692
return ALBEvent(event)
688693

689694
def _resolve(self) -> ResponseBuilder:
690695
"""Resolves the response or return the not found response"""
691696
method = self.current_event.http_method.upper()
692697
path = self._remove_prefix(self.current_event.path)
698+
693699
for route in self._static_routes + self._dynamic_routes:
694700
if method != route.method:
695701
continue
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from typing import Callable, Dict, List, Optional
2+
3+
from aws_lambda_powertools.event_handler import CORSConfig
4+
from aws_lambda_powertools.event_handler.api_gateway import (
5+
ApiGatewayResolver,
6+
ProxyEventType,
7+
)
8+
from aws_lambda_powertools.utilities.data_classes import VPCLatticeEvent
9+
10+
11+
class VPCLatticeResolver(ApiGatewayResolver):
12+
"""VPC Lattice resolver
13+
14+
Documentation:
15+
- https://docs.aws.amazon.com/lambda/latest/dg/services-vpc-lattice.html
16+
- https://docs.aws.amazon.com/lambda/latest/dg/services-vpc-lattice.html#vpc-lattice-receiving-events
17+
18+
Examples
19+
--------
20+
Simple example integrating with Tracer
21+
22+
```python
23+
from aws_lambda_powertools import Tracer
24+
from aws_lambda_powertools.event_handler import VPCLatticeResolver
25+
26+
tracer = Tracer()
27+
app = VPCLatticeResolver()
28+
29+
@app.get("/get-call")
30+
def simple_get():
31+
return {"message": "Foo"}
32+
33+
@app.post("/post-call")
34+
def simple_post():
35+
post_data: dict = app.current_event.json_body
36+
return {"message": post_data}
37+
38+
@tracer.capture_lambda_handler
39+
def lambda_handler(event, context):
40+
return app.resolve(event, context)
41+
"""
42+
43+
current_event: VPCLatticeEvent
44+
45+
def __init__(
46+
self,
47+
cors: Optional[CORSConfig] = None,
48+
debug: Optional[bool] = None,
49+
serializer: Optional[Callable[[Dict], str]] = None,
50+
strip_prefixes: Optional[List[str]] = None,
51+
):
52+
"""Amazon VPC Lattice resolver"""
53+
super().__init__(ProxyEventType.VPCLatticeEvent, cors, debug, serializer, strip_prefixes)

aws_lambda_powertools/logging/correlation_paths.py

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
EVENT_BRIDGE = "id"
99
LAMBDA_FUNCTION_URL = API_GATEWAY_REST
1010
S3_OBJECT_LAMBDA = "xAmzRequestId"
11+
VPC_LATTICE = 'headers."x-amzn-trace-id"'

aws_lambda_powertools/utilities/data_classes/vpc_lattice.py

+23-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
from typing import Any, Dict, Optional
22

3-
from aws_lambda_powertools.utilities.data_classes.common import DictWrapper
3+
from aws_lambda_powertools.shared.headers_serializer import (
4+
BaseHeadersSerializer,
5+
HttpApiHeadersSerializer,
6+
)
7+
from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent
48
from aws_lambda_powertools.utilities.data_classes.shared_functions import (
59
base64_decode,
610
get_header_value,
711
get_query_string_value,
812
)
913

1014

11-
class VPCLatticeEvent(DictWrapper):
15+
class VPCLatticeEvent(BaseProxyEvent):
1216
@property
1317
def body(self) -> str:
1418
"""The VPC Lattice body."""
@@ -54,6 +58,19 @@ def raw_path(self) -> str:
5458
"""The raw VPC Lattice request path."""
5559
return self["raw_path"]
5660

61+
# VPCLattice event has no path field
62+
# Added here for consistency with the BaseProxyEvent class
63+
@property
64+
def path(self) -> str:
65+
return self["raw_path"]
66+
67+
# VPCLattice event has no http_method field
68+
# Added here for consistency with the BaseProxyEvent class
69+
@property
70+
def http_method(self) -> str:
71+
"""The HTTP method used. Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT."""
72+
return self["method"]
73+
5774
def get_query_string_value(self, name: str, default_value: Optional[str] = None) -> Optional[str]:
5875
"""Get query string value by name
5976
@@ -101,3 +118,7 @@ def get_header_value(
101118
default_value=default_value,
102119
case_sensitive=case_sensitive,
103120
)
121+
122+
def header_serializer(self) -> BaseHeadersSerializer:
123+
# When using the VPC Lattice integration, we have multiple HTTP Headers.
124+
return HttpApiHeadersSerializer()

docs/core/event_handler/api_gateway.md

+20-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: REST API
33
description: Core utility
44
---
55

6-
Event handler for Amazon API Gateway REST and HTTP APIs, Application Loader Balancer (ALB), and Lambda Function URLs.
6+
Event handler for Amazon API Gateway REST and HTTP APIs, Application Loader Balancer (ALB), Lambda Function URLs, and VPC Lattice.
77

88
## Key Features
99

@@ -20,6 +20,8 @@ Event handler for Amazon API Gateway REST and HTTP APIs, Application Loader Bala
2020

2121
If you're using any API Gateway integration, you must have an existing [API Gateway Proxy integration](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html){target="_blank"} or [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html){target="_blank"} configured to invoke your Lambda function.
2222

23+
In case of using [VPC Lattice](https://docs.aws.amazon.com/lambda/latest/dg/services-vpc-lattice.html){target="_blank"}, you must have a service network configured to invoke your Lambda function.
24+
2325
This is the sample infrastructure for API Gateway and Lambda Function URLs we are using for the examples in this documentation.
2426

2527
???+ info "There is no additional permissions or dependencies required to use this utility."
@@ -42,7 +44,7 @@ Before you decorate your functions to handle a given path and HTTP method(s), yo
4244

4345
A resolver will handle request resolution, including [one or more routers](#split-routes-with-router), and give you access to the current event via typed properties.
4446

45-
For resolvers, we provide: `APIGatewayRestResolver`, `APIGatewayHttpResolver`, `ALBResolver`, and `LambdaFunctionUrlResolver`. From here on, we will default to `APIGatewayRestResolver` across examples.
47+
For resolvers, we provide: `APIGatewayRestResolver`, `APIGatewayHttpResolver`, `ALBResolver`, `LambdaFunctionUrlResolver`, and `VPCLatticeResolver`. From here on, we will default to `APIGatewayRestResolver` across examples.
4648

4749
???+ info "Auto-serialization"
4850
We serialize `Dict` responses as JSON, trim whitespace for compact responses, set content-type to `application/json`, and
@@ -116,6 +118,22 @@ When using [AWS Lambda Function URL](https://docs.aws.amazon.com/lambda/latest/d
116118
--8<-- "examples/event_handler_lambda_function_url/src/getting_started_lambda_function_url_resolver.json"
117119
```
118120

121+
#### VPC Lattice
122+
123+
When using [VPC Lattice with AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/services-vpc-lattice.html){target="_blank"}, you can use `VPCLatticeResolver`.
124+
125+
=== "getting_started_vpclattice_resolver.py"
126+
127+
```python hl_lines="5 11" title="Using VPC Lattice resolver"
128+
--8<-- "examples/event_handler_rest/src/getting_started_vpclattice_resolver.py"
129+
```
130+
131+
=== "getting_started_vpclattice_resolver.json"
132+
133+
```json hl_lines="2 3" title="Example payload delivered to the handler"
134+
--8<-- "examples/event_handler_rest/src/getting_started_vpclattice_resolver.json"
135+
```
136+
119137
### Dynamic routes
120138

121139
You can use `/todos/<todo_id>` to configure dynamic URL paths, where `<todo_id>` will be resolved at runtime.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"raw_path": "/testpath",
3+
"method": "GET",
4+
"headers": {
5+
"user_agent": "curl/7.64.1",
6+
"x-forwarded-for": "10.213.229.10",
7+
"host": "test-lambda-service-3908sdf9u3u.dkfjd93.vpc-lattice-svcs.us-east-2.on.aws",
8+
"accept": "*/*"
9+
},
10+
"query_string_parameters": {
11+
"order-id": "1"
12+
},
13+
"body": "eyJ0ZXN0IjogImV2ZW50In0=",
14+
"is_base64_encoded": true
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import requests
2+
from requests import Response
3+
4+
from aws_lambda_powertools import Logger, Tracer
5+
from aws_lambda_powertools.event_handler import VPCLatticeResolver
6+
from aws_lambda_powertools.logging import correlation_paths
7+
from aws_lambda_powertools.utilities.typing import LambdaContext
8+
9+
tracer = Tracer()
10+
logger = Logger()
11+
app = VPCLatticeResolver()
12+
13+
14+
@app.get("/todos")
15+
@tracer.capture_method
16+
def get_todos():
17+
todos: Response = requests.get("https://jsonplaceholder.typicode.com/todos")
18+
todos.raise_for_status()
19+
20+
# for brevity, we'll limit to the first 10 only
21+
return {"todos": todos.json()[:10]}
22+
23+
24+
# You can continue to use other utilities just as before
25+
@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPLICATION_LOAD_BALANCER)
26+
@tracer.capture_lambda_handler
27+
def lambda_handler(event: dict, context: LambdaContext) -> dict:
28+
return app.resolve(event, context)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"raw_path": "/testpath/",
3+
"method": "GET",
4+
"headers": {
5+
"user_agent": "curl/7.64.1",
6+
"x-forwarded-for": "10.213.229.10",
7+
"host": "test-lambda-service-3908sdf9u3u.dkfjd93.vpc-lattice-svcs.us-east-2.on.aws",
8+
"accept": "*/*"
9+
},
10+
"query_string_parameters": {
11+
"order-id": "1"
12+
},
13+
"body": "eyJ0ZXN0IjogImV2ZW50In0=",
14+
"is_base64_encoded": true
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import json
2+
3+
import pytest
4+
5+
6+
@pytest.fixture
7+
def json_dump():
8+
# our serializers reduce length to save on costs; fixture to replicate separators
9+
return lambda obj: json.dumps(obj, separators=(",", ":"))

tests/functional/event_handler/test_api_gateway.py

-6
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,6 @@
4141
from tests.functional.utils import load_event
4242

4343

44-
@pytest.fixture
45-
def json_dump():
46-
# our serializers reduce length to save on costs; fixture to replicate separators
47-
return lambda obj: json.dumps(obj, separators=(",", ":"))
48-
49-
5044
def read_media(file_name: str) -> bytes:
5145
path = Path(str(Path(__file__).parent.parent.parent.parent) + "/docs/media/" + file_name)
5246
return path.read_bytes()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from aws_lambda_powertools.event_handler import (
2+
Response,
3+
VPCLatticeResolver,
4+
content_types,
5+
)
6+
from aws_lambda_powertools.event_handler.api_gateway import CORSConfig
7+
from aws_lambda_powertools.utilities.data_classes import VPCLatticeEvent
8+
from tests.functional.utils import load_event
9+
10+
11+
def test_vpclattice_event():
12+
# GIVEN a VPC Lattice event
13+
app = VPCLatticeResolver()
14+
15+
@app.get("/testpath")
16+
def foo():
17+
assert isinstance(app.current_event, VPCLatticeEvent)
18+
assert app.lambda_context == {}
19+
return Response(200, content_types.TEXT_HTML, "foo")
20+
21+
# WHEN calling the event handler
22+
result = app(load_event("vpcLatticeEvent.json"), {})
23+
24+
# THEN process event correctly
25+
# AND set the current_event type as VPCLatticeEvent
26+
assert result["statusCode"] == 200
27+
assert result["headers"]["Content-Type"] == content_types.TEXT_HTML
28+
assert result["body"] == "foo"
29+
30+
31+
def test_vpclattice_event_path_trailing_slash(json_dump):
32+
# GIVEN a VPC Lattice event
33+
app = VPCLatticeResolver()
34+
35+
@app.get("/testpath")
36+
def foo():
37+
assert isinstance(app.current_event, VPCLatticeEvent)
38+
assert app.lambda_context == {}
39+
return Response(200, content_types.TEXT_HTML, "foo")
40+
41+
# WHEN calling the event handler using path with trailing "/"
42+
result = app(load_event("vpcLatticeEventPathTrailingSlash.json"), {})
43+
44+
# THEN
45+
assert result["statusCode"] == 404
46+
assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON
47+
expected = {"statusCode": 404, "message": "Not found"}
48+
assert result["body"] == json_dump(expected)
49+
50+
51+
def test_cors_preflight_body_is_empty_not_null():
52+
# GIVEN CORS is configured
53+
app = VPCLatticeResolver(cors=CORSConfig())
54+
55+
event = {"raw_path": "/my/request", "method": "OPTIONS", "headers": {}}
56+
57+
# WHEN calling the event handler
58+
result = app(event, {})
59+
60+
# THEN there body should be empty strings
61+
assert result["body"] == ""
62+
63+
64+
def test_vpclattice_url_no_matches():
65+
# GIVEN a VPC Lattice event
66+
app = VPCLatticeResolver()
67+
68+
@app.post("/no_match")
69+
def foo():
70+
raise RuntimeError()
71+
72+
# WHEN calling the event handler
73+
result = app(load_event("vpcLatticeEvent.json"), {})
74+
75+
# THEN process event correctly
76+
# AND return 404 because the event doesn't match any known route
77+
assert result["statusCode"] == 404

0 commit comments

Comments
 (0)