From 1cf5073f0c3b3e3e541d76489bfec2e78af429d1 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Sun, 1 Oct 2023 09:30:11 -0400 Subject: [PATCH 01/27] updated vpc lattice v2 event --- .../utilities/data_classes/__init__.py | 3 +- .../utilities/data_classes/vpc_lattice.py | 128 ++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/aws_lambda_powertools/utilities/data_classes/__init__.py b/aws_lambda_powertools/utilities/data_classes/__init__.py index 99754266928..d245bc35f0d 100644 --- a/aws_lambda_powertools/utilities/data_classes/__init__.py +++ b/aws_lambda_powertools/utilities/data_classes/__init__.py @@ -27,7 +27,7 @@ from .ses_event import SESEvent from .sns_event import SNSEvent from .sqs_event import SQSEvent -from .vpc_lattice import VPCLatticeEvent +from .vpc_lattice import VPCLatticeEvent, VPCLatticeEventV2 __all__ = [ "APIGatewayProxyEvent", @@ -56,4 +56,5 @@ "event_source", "AWSConfigRuleEvent", "VPCLatticeEvent", + "VPCLatticeEventV2", ] diff --git a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py index 35194f1f3f0..baa3332a677 100644 --- a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py +++ b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py @@ -140,3 +140,131 @@ def get_header_value( def header_serializer(self) -> BaseHeadersSerializer: # When using the VPC Lattice integration, we have multiple HTTP Headers. return HttpApiHeadersSerializer() + + +class VPCLatticeEventV2(BaseProxyEvent): + @property + def version(self) -> str: + """The VPC Lattice v2 Event version""" + return self["version"] + + @property + def path(self) -> str: + """The VPC Lattice path""" + return self["path"] + + @property + def method(self) -> str: + """The VPC Lattice method used. Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT.""" + return self["method"] + + @property + def headers(self) -> Dict[str, str]: + """The VPC Lattice event headers.""" + return self["headers"] + + @property + def query_string_parameters(self) -> Dict[str, str]: + """The request query string parameters.""" + return self["query_string_parameters"] + + @property + def body(self) -> str: + """The VPC Lattice body.""" + return self["body"] + + @property + def is_base64_encoded(self) -> bool: + """A boolean flag to indicate if the applicable request payload is Base64-encode""" + return self["is_base64_encoded"] + + @property + def request_context(self) -> dict: + """he VPC Lattice event request context.""" + return self["requestContext"] + + @property + def json_body(self) -> Any: + """Parses the submitted body as json""" + if self._json_data is None: + self._json_data = self._json_deserializer(self.decoded_body) + return self._json_data + + @property + def decoded_body(self) -> str: + """Dynamically base64 decode body as a str""" + body: str = self["body"] + if self.is_base64_encoded: + return base64_decode(body) + return body + + + def get_query_string_value(self, name: str, default_value: Optional[str] = None) -> Optional[str]: + """Get query string value by name + + Parameters + ---------- + name: str + Query string parameter name + default_value: str, optional + Default value if no value was found by name + Returns + ------- + str, optional + Query string parameter value + """ + return get_query_string_value( + query_string_parameters=self.query_string_parameters, + name=name, + default_value=default_value, + ) + + @overload + def get_header_value( + self, + name: str, + default_value: str, + case_sensitive: Optional[bool] = False, + ) -> str: + ... + + @overload + def get_header_value( + self, + name: str, + default_value: Optional[str] = None, + case_sensitive: Optional[bool] = False, + ) -> Optional[str]: + ... + + def get_header_value( + self, + name: str, + default_value: Optional[str] = None, + case_sensitive: Optional[bool] = False, + ) -> Optional[str]: + """Get header value by name + + Parameters + ---------- + name: str + Header name + default_value: str, optional + Default value if no value was found by name + case_sensitive: bool + Whether to use a case-sensitive look up + Returns + ------- + str, optional + Header value + """ + return get_header_value( + headers=self.headers, + name=name, + default_value=default_value, + case_sensitive=case_sensitive, + ) + + def header_serializer(self) -> BaseHeadersSerializer: + # When using the VPC Lattice integration, we have multiple HTTP Headers. + return HttpApiHeadersSerializer() From 75307605cb10f3654dbb296589128ab77279c248 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Sun, 1 Oct 2023 09:34:22 -0400 Subject: [PATCH 02/27] updating base --- aws_lambda_powertools/event_handler/api_gateway.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 2163d7d762e..f3737d5e68a 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -22,6 +22,7 @@ APIGatewayProxyEventV2, LambdaFunctionUrlEvent, VPCLatticeEvent, + VPCLatticeEventV2 ) from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent from aws_lambda_powertools.utilities.typing import LambdaContext @@ -43,6 +44,7 @@ class ProxyEventType(Enum): APIGatewayProxyEventV2 = "APIGatewayProxyEventV2" ALBEvent = "ALBEvent" VPCLatticeEvent = "VPCLatticeEvent" + VPCLatticeEventV2 = "VPCLatticeEventV2" LambdaFunctionUrlEvent = "LambdaFunctionUrlEvent" @@ -999,6 +1001,9 @@ def _to_proxy_event(self, event: Dict) -> BaseProxyEvent: if self._proxy_type == ProxyEventType.VPCLatticeEvent: logger.debug("Converting event to VPC Lattice contract") return VPCLatticeEvent(event) + if self._proxy_type == ProxyEventType.VPCLatticeEventV2: + logger.debug("Converting event to VPC Lattice contract") + return VPCLatticeEventV2(event) logger.debug("Converting event to ALB contract") return ALBEvent(event) From 93cd96e65f6a83208499efa3b228e044ba9c9708 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Sun, 1 Oct 2023 09:35:13 -0400 Subject: [PATCH 03/27] remove line break --- aws_lambda_powertools/utilities/data_classes/vpc_lattice.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py index baa3332a677..27a4dacd63f 100644 --- a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py +++ b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py @@ -198,7 +198,6 @@ def decoded_body(self) -> str: return base64_decode(body) return body - def get_query_string_value(self, name: str, default_value: Optional[str] = None) -> Optional[str]: """Get query string value by name From b1f9e96094fa085a10a79e9d94722a78bfe11a1d Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Sun, 1 Oct 2023 09:38:28 -0400 Subject: [PATCH 04/27] more details return type for request context --- aws_lambda_powertools/utilities/data_classes/vpc_lattice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py index 27a4dacd63f..0fac2fcd067 100644 --- a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py +++ b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py @@ -179,7 +179,7 @@ def is_base64_encoded(self) -> bool: return self["is_base64_encoded"] @property - def request_context(self) -> dict: + def request_context(self) -> Dict[str, str]: """he VPC Lattice event request context.""" return self["requestContext"] From c1dbf1bca24874bf8dc26672b101a85ba60dafba Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Sun, 1 Oct 2023 11:04:01 -0400 Subject: [PATCH 05/27] couple doc string updates --- .../utilities/data_classes/vpc_lattice.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py index 0fac2fcd067..14e91e87ca6 100644 --- a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py +++ b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py @@ -150,17 +150,17 @@ def version(self) -> str: @property def path(self) -> str: - """The VPC Lattice path""" + """The VPC Lattice v2 Event path""" return self["path"] @property def method(self) -> str: - """The VPC Lattice method used. Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT.""" + """The VPC Lattice v2 Event method used. Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT.""" return self["method"] @property def headers(self) -> Dict[str, str]: - """The VPC Lattice event headers.""" + """The VPC Lattice v2 Event event headers.""" return self["headers"] @property @@ -170,7 +170,7 @@ def query_string_parameters(self) -> Dict[str, str]: @property def body(self) -> str: - """The VPC Lattice body.""" + """The VPC Lattice v2 Event body.""" return self["body"] @property @@ -180,7 +180,7 @@ def is_base64_encoded(self) -> bool: @property def request_context(self) -> Dict[str, str]: - """he VPC Lattice event request context.""" + """he VPC Lattice v2 Event request context.""" return self["requestContext"] @property From 680c9be46d496a7f8f8988f20ac665e8bdb3644d Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Sun, 1 Oct 2023 11:06:16 -0400 Subject: [PATCH 06/27] copy pasta --- aws_lambda_powertools/utilities/data_classes/vpc_lattice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py index 14e91e87ca6..a2e9e853ed8 100644 --- a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py +++ b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py @@ -160,7 +160,7 @@ def method(self) -> str: @property def headers(self) -> Dict[str, str]: - """The VPC Lattice v2 Event event headers.""" + """The VPC Lattice v2 Event headers.""" return self["headers"] @property From 3d996ac04f701b5751aac3e00b42fcb2e21e6ce0 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Mon, 2 Oct 2023 17:19:26 -0400 Subject: [PATCH 07/27] ruff work --- aws_lambda_powertools/utilities/data_classes/vpc_lattice.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py index a2e9e853ed8..d76ca91a92a 100644 --- a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py +++ b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py @@ -155,7 +155,10 @@ def path(self) -> str: @property def method(self) -> str: - """The VPC Lattice v2 Event method used. Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT.""" + """ + The VPC Lattice v2 Event method used. + Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT. + """ return self["method"] @property From 0c48df9a6a4df5bd2e19c847bf4875df8a57bc97 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Mon, 2 Oct 2023 17:24:42 -0400 Subject: [PATCH 08/27] small tweak --- aws_lambda_powertools/utilities/data_classes/vpc_lattice.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py index d76ca91a92a..cdea217a86e 100644 --- a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py +++ b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py @@ -197,9 +197,7 @@ def json_body(self) -> Any: def decoded_body(self) -> str: """Dynamically base64 decode body as a str""" body: str = self["body"] - if self.is_base64_encoded: - return base64_decode(body) - return body + return base64_decode(body) if self.is_base64_encoded else body def get_query_string_value(self, name: str, default_value: Optional[str] = None) -> Optional[str]: """Get query string value by name From 27e3761944be94da0d2a69d4c5f7ef3b17898f01 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Mon, 2 Oct 2023 20:56:16 -0400 Subject: [PATCH 09/27] adding model 2 tests --- .../utilities/parser/envelopes/__init__.py | 3 +- .../utilities/parser/envelopes/vpc_lattice.py | 25 ++++++++++++++++- .../utilities/parser/models/__init__.py | 3 +- .../utilities/parser/models/vpc_lattice.py | 13 ++++++++- tests/events/vpcLatticeV2Event .json | 28 +++++++++++++++++++ tests/unit/parser/test_vpc_lattice.py | 28 ++++++++++++++++++- 6 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 tests/events/vpcLatticeV2Event .json diff --git a/aws_lambda_powertools/utilities/parser/envelopes/__init__.py b/aws_lambda_powertools/utilities/parser/envelopes/__init__.py index cbca982adf7..399bead1346 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/__init__.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/__init__.py @@ -10,7 +10,7 @@ from .lambda_function_url import LambdaFunctionUrlEnvelope from .sns import SnsEnvelope, SnsSqsEnvelope from .sqs import SqsEnvelope -from .vpc_lattice import VpcLatticeEnvelope +from .vpc_lattice import VpcLatticeEnvelope, VpcLatticeV2Envelope __all__ = [ "ApiGatewayEnvelope", @@ -27,4 +27,5 @@ "KafkaEnvelope", "BaseEnvelope", "VpcLatticeEnvelope", + "VpcLatticeV2Envelope" ] diff --git a/aws_lambda_powertools/utilities/parser/envelopes/vpc_lattice.py b/aws_lambda_powertools/utilities/parser/envelopes/vpc_lattice.py index fb95ac05a8d..597968daf6b 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/vpc_lattice.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/vpc_lattice.py @@ -1,7 +1,7 @@ import logging from typing import Any, Dict, Optional, Type, Union -from ..models import VpcLatticeModel +from ..models import VpcLatticeModel,VpcLatticeV2Model from ..types import Model from .base import BaseEnvelope @@ -30,3 +30,26 @@ def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Type[Model]) parsed_envelope: VpcLatticeModel = VpcLatticeModel.parse_obj(data) logger.debug(f"Parsing event payload in `detail` with {model}") return self._parse(data=parsed_envelope.body, model=model) + +class VpcLatticeV2Envelope(BaseEnvelope): + """Amazon VPC Lattice envelope to extract data within body key""" + + def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Type[Model]) -> Optional[Model]: + """Parses data found with model provided + + Parameters + ---------- + data : Dict + Lambda event to be parsed + model : Type[Model] + Data model provided to parse after extracting data using envelope + + Returns + ------- + Optional[Model] + Parsed detail payload with model provided + """ + logger.debug(f"Parsing incoming data with VPC Lattice V2 model {VpcLatticeV2Model}") + parsed_envelope: VpcLatticeV2Model = VpcLatticeV2Model.parse_obj(data) + logger.debug(f"Parsing event payload in `detail` with {model}") + return self._parse(data=parsed_envelope.body, model=model) \ No newline at end of file diff --git a/aws_lambda_powertools/utilities/parser/models/__init__.py b/aws_lambda_powertools/utilities/parser/models/__init__.py index f1b2d30d9cf..39c42b2eca8 100644 --- a/aws_lambda_powertools/utilities/parser/models/__init__.py +++ b/aws_lambda_powertools/utilities/parser/models/__init__.py @@ -88,7 +88,7 @@ ) from .sns import SnsModel, SnsNotificationModel, SnsRecordModel from .sqs import SqsAttributesModel, SqsModel, SqsMsgAttributeModel, SqsRecordModel -from .vpc_lattice import VpcLatticeModel +from .vpc_lattice import VpcLatticeModel, VpcLatticeV2Model __all__ = [ "APIGatewayProxyEventV2Model", @@ -163,4 +163,5 @@ "CloudFormationCustomResourceCreateModel", "CloudFormationCustomResourceBaseModel", "VpcLatticeModel", + "VpcLatticeV2Model" ] diff --git a/aws_lambda_powertools/utilities/parser/models/vpc_lattice.py b/aws_lambda_powertools/utilities/parser/models/vpc_lattice.py index 8442fc92781..86e1c6c60b8 100644 --- a/aws_lambda_powertools/utilities/parser/models/vpc_lattice.py +++ b/aws_lambda_powertools/utilities/parser/models/vpc_lattice.py @@ -1,6 +1,6 @@ from typing import Dict, Type, Union -from pydantic import BaseModel +from pydantic import BaseModel, Field class VpcLatticeModel(BaseModel): @@ -10,3 +10,14 @@ class VpcLatticeModel(BaseModel): is_base64_encoded: bool headers: Dict[str, str] query_string_parameters: Dict[str, str] + + +class VpcLatticeV2Model(BaseModel): + version: str + path: str + method: str + headers: Dict[str, str] + query_string_parameters: Dict[str, str] + body: Union[str, Type[BaseModel]] + is_base64_encoded: bool + request_context: Dict[str, str] = Field(alias="requestContext") \ No newline at end of file diff --git a/tests/events/vpcLatticeV2Event .json b/tests/events/vpcLatticeV2Event .json new file mode 100644 index 00000000000..e3cc99fa43b --- /dev/null +++ b/tests/events/vpcLatticeV2Event .json @@ -0,0 +1,28 @@ +{ + "version": "2.0", + "path": "/newpath", + "method": "GET", + "headers": { + "user_agent": "curl/7.64.1", + "x-forwarded-for": "10.213.229.10", + "host": "test-lambda-service-3908sdf9u3u.dkfjd93.vpc-lattice-svcs.us-east-2.on.aws", + "accept": "*/*" + }, + "query_string_parameters": { + "order-id": "314159" + }, + "body": { + "message": "Hello from Lambda!" + }, + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:us-east-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:us-east-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:us-east-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:us-east-2:123456789012:vpc/vpc-0b8276c84697e7339" + }, + "region": "us-east-2", + "timeEpoch": "1690497599177430" + } +} \ No newline at end of file diff --git a/tests/unit/parser/test_vpc_lattice.py b/tests/unit/parser/test_vpc_lattice.py index e5dfedfb445..4d1cb659ca5 100644 --- a/tests/unit/parser/test_vpc_lattice.py +++ b/tests/unit/parser/test_vpc_lattice.py @@ -1,7 +1,7 @@ import pytest from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, parse -from aws_lambda_powertools.utilities.parser.models import VpcLatticeModel +from aws_lambda_powertools.utilities.parser.models import VpcLatticeModel, VpcLatticeV2Model from tests.functional.utils import load_event from tests.unit.parser.schemas import MyVpcLatticeBusiness @@ -47,3 +47,29 @@ def test_vpc_lattice_event_invalid(): with pytest.raises(ValidationError): VpcLatticeModel(**raw_event) + + +def test_vpc_lattice_v2_event_with_envelope(): + raw_event = load_event("vpcLatticeV2Event.json") + raw_event["body"] = '{"username": "Stephen", "name": "Bawks"}' + parsed_event: MyVpcLatticeBusiness = parse( + event=raw_event, + model=MyVpcLatticeBusiness, + envelope=envelopes.VpcLatticeV2Envelope, + ) + + assert parsed_event.username == "Stephen" + assert parsed_event.name == "Bawks" + + +def test_vpc_lattice_v2_event(): + raw_event = load_event("vpcLatticeV2Event.json") + model = VpcLatticeV2Model(**raw_event) + + assert model.body == raw_event["body"] + assert model.method == raw_event["method"] + assert model.path == raw_event["path"] + assert model.is_base64_encoded == raw_event["is_base64_encoded"] + assert model.headers == raw_event["headers"] + assert model.query_string_parameters == raw_event["query_string_parameters"] + assert model.request_context == raw_event["requestContext"] From 86a7ff5ff8f7609bc4cc59898b62acb00b40cd7d Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Mon, 2 Oct 2023 20:57:01 -0400 Subject: [PATCH 10/27] starting docs --- docs/utilities/parser.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/utilities/parser.md b/docs/utilities/parser.md index 846460e43d2..2fdba30586c 100644 --- a/docs/utilities/parser.md +++ b/docs/utilities/parser.md @@ -192,6 +192,7 @@ Parser comes with the following built-in models: | **SnsModel** | Lambda Event Source payload for Amazon Simple Notification Service | | **SqsModel** | Lambda Event Source payload for Amazon SQS | | **VpcLatticeModel** | Lambda Event Source payload for Amazon VPC Lattice | +| **VpcLatticeV2Model** | Lambda Event Source payload for Amazon VPC Lattice v2 payload | #### Extending built-in models From 687f45e146daaee7383d13161b3162fa1d56e559 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Tue, 3 Oct 2023 08:35:36 -0400 Subject: [PATCH 11/27] couple more tests --- tests/unit/parser/test_vpc_lattice.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/unit/parser/test_vpc_lattice.py b/tests/unit/parser/test_vpc_lattice.py index 4d1cb659ca5..81bbb9de5f4 100644 --- a/tests/unit/parser/test_vpc_lattice.py +++ b/tests/unit/parser/test_vpc_lattice.py @@ -72,4 +72,20 @@ def test_vpc_lattice_v2_event(): assert model.is_base64_encoded == raw_event["is_base64_encoded"] assert model.headers == raw_event["headers"] assert model.query_string_parameters == raw_event["query_string_parameters"] - assert model.request_context == raw_event["requestContext"] + assert model.request_context == raw_event["request_context"] + +def test_vpc_lattice_v2_event_custom_model(): + class MyCustomResource(VpcLatticeV2Model): + body: str + + raw_event = load_event("vpcLatticeV2Event.json") + model = MyCustomResource(**raw_event) + + assert model.body == raw_event["body"] + +def test_vpc_lattice_v2_event_invalid(): + raw_event = load_event("vpcLatticeV2Event.json") + raw_event["body"] = ["some_more_data"] + + with pytest.raises(ValidationError): + VpcLatticeV2Model(**raw_event) \ No newline at end of file From 5e47060d0f53acfcdd4572cbdd9a043051a35378 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Tue, 3 Oct 2023 08:39:22 -0400 Subject: [PATCH 12/27] words matter --- tests/events/vpcLatticeV2Event .json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/events/vpcLatticeV2Event .json b/tests/events/vpcLatticeV2Event .json index e3cc99fa43b..f030973c66b 100644 --- a/tests/events/vpcLatticeV2Event .json +++ b/tests/events/vpcLatticeV2Event .json @@ -12,7 +12,7 @@ "order-id": "314159" }, "body": { - "message": "Hello from Lambda!" + "message": "Hello from Powertools!" }, "isBase64Encoded": false, "requestContext": { From 32463a1555391d2e1c89c059cdf20fb298e47ad2 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Tue, 3 Oct 2023 08:41:08 -0400 Subject: [PATCH 13/27] missing line break --- tests/events/vpcLatticeV2Event .json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/events/vpcLatticeV2Event .json b/tests/events/vpcLatticeV2Event .json index f030973c66b..2e754b295bb 100644 --- a/tests/events/vpcLatticeV2Event .json +++ b/tests/events/vpcLatticeV2Event .json @@ -25,4 +25,4 @@ "region": "us-east-2", "timeEpoch": "1690497599177430" } -} \ No newline at end of file +} From 77ee1531ba13b77466b1d22b6cfabc9062be11a8 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Tue, 3 Oct 2023 08:44:16 -0400 Subject: [PATCH 14/27] ruff do work --- aws_lambda_powertools/event_handler/api_gateway.py | 2 +- aws_lambda_powertools/utilities/parser/envelopes/__init__.py | 2 +- .../utilities/parser/envelopes/vpc_lattice.py | 4 ++-- aws_lambda_powertools/utilities/parser/models/__init__.py | 2 +- aws_lambda_powertools/utilities/parser/models/vpc_lattice.py | 2 +- tests/unit/parser/test_vpc_lattice.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index f3737d5e68a..15f8ddb82a9 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -22,7 +22,7 @@ APIGatewayProxyEventV2, LambdaFunctionUrlEvent, VPCLatticeEvent, - VPCLatticeEventV2 + VPCLatticeEventV2, ) from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent from aws_lambda_powertools.utilities.typing import LambdaContext diff --git a/aws_lambda_powertools/utilities/parser/envelopes/__init__.py b/aws_lambda_powertools/utilities/parser/envelopes/__init__.py index 399bead1346..2e1a7af2f03 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/__init__.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/__init__.py @@ -27,5 +27,5 @@ "KafkaEnvelope", "BaseEnvelope", "VpcLatticeEnvelope", - "VpcLatticeV2Envelope" + "VpcLatticeV2Envelope", ] diff --git a/aws_lambda_powertools/utilities/parser/envelopes/vpc_lattice.py b/aws_lambda_powertools/utilities/parser/envelopes/vpc_lattice.py index 597968daf6b..ea944d27bea 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/vpc_lattice.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/vpc_lattice.py @@ -1,7 +1,7 @@ import logging from typing import Any, Dict, Optional, Type, Union -from ..models import VpcLatticeModel,VpcLatticeV2Model +from ..models import VpcLatticeModel, VpcLatticeV2Model from ..types import Model from .base import BaseEnvelope @@ -52,4 +52,4 @@ def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Type[Model]) logger.debug(f"Parsing incoming data with VPC Lattice V2 model {VpcLatticeV2Model}") parsed_envelope: VpcLatticeV2Model = VpcLatticeV2Model.parse_obj(data) logger.debug(f"Parsing event payload in `detail` with {model}") - return self._parse(data=parsed_envelope.body, model=model) \ No newline at end of file + return self._parse(data=parsed_envelope.body, model=model) diff --git a/aws_lambda_powertools/utilities/parser/models/__init__.py b/aws_lambda_powertools/utilities/parser/models/__init__.py index 39c42b2eca8..dc316153d88 100644 --- a/aws_lambda_powertools/utilities/parser/models/__init__.py +++ b/aws_lambda_powertools/utilities/parser/models/__init__.py @@ -163,5 +163,5 @@ "CloudFormationCustomResourceCreateModel", "CloudFormationCustomResourceBaseModel", "VpcLatticeModel", - "VpcLatticeV2Model" + "VpcLatticeV2Model", ] diff --git a/aws_lambda_powertools/utilities/parser/models/vpc_lattice.py b/aws_lambda_powertools/utilities/parser/models/vpc_lattice.py index 86e1c6c60b8..9cf193c08ec 100644 --- a/aws_lambda_powertools/utilities/parser/models/vpc_lattice.py +++ b/aws_lambda_powertools/utilities/parser/models/vpc_lattice.py @@ -20,4 +20,4 @@ class VpcLatticeV2Model(BaseModel): query_string_parameters: Dict[str, str] body: Union[str, Type[BaseModel]] is_base64_encoded: bool - request_context: Dict[str, str] = Field(alias="requestContext") \ No newline at end of file + request_context: Dict[str, str] = Field(alias="requestContext") diff --git a/tests/unit/parser/test_vpc_lattice.py b/tests/unit/parser/test_vpc_lattice.py index 81bbb9de5f4..dde6082e722 100644 --- a/tests/unit/parser/test_vpc_lattice.py +++ b/tests/unit/parser/test_vpc_lattice.py @@ -88,4 +88,4 @@ def test_vpc_lattice_v2_event_invalid(): raw_event["body"] = ["some_more_data"] with pytest.raises(ValidationError): - VpcLatticeV2Model(**raw_event) \ No newline at end of file + VpcLatticeV2Model(**raw_event) From 2dc9b0d92a067166f1e100794f706499ba25daf2 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Tue, 3 Oct 2023 14:20:33 +0100 Subject: [PATCH 15/27] Adding more fields in parser --- .../utilities/data_classes/vpc_lattice.py | 182 ++++++------------ .../utilities/parser/envelopes/__init__.py | 3 +- .../utilities/parser/envelopes/vpc_lattice.py | 25 +-- .../parser/envelopes/vpc_latticev2.py | 32 +++ .../utilities/parser/models/__init__.py | 3 +- .../utilities/parser/models/vpc_lattice.py | 13 +- .../utilities/parser/models/vpc_latticev2.py | 28 +++ ...ceV2Event .json => vpcLatticeV2Event.json} | 6 +- .../data_classes/test_vpc_lattice_eventv2.py | 24 +++ tests/unit/parser/test_vpc_lattice.py | 44 +---- tests/unit/parser/test_vpc_latticev2.py | 50 +++++ 11 files changed, 197 insertions(+), 213 deletions(-) create mode 100644 aws_lambda_powertools/utilities/parser/envelopes/vpc_latticev2.py create mode 100644 aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py rename tests/events/{vpcLatticeV2Event .json => vpcLatticeV2Event.json} (90%) create mode 100644 tests/unit/data_classes/test_vpc_lattice_eventv2.py create mode 100644 tests/unit/parser/test_vpc_latticev2.py diff --git a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py index cdea217a86e..993458b2445 100644 --- a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py +++ b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py @@ -4,7 +4,7 @@ BaseHeadersSerializer, HttpApiHeadersSerializer, ) -from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent +from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent, DictWrapper from aws_lambda_powertools.utilities.data_classes.shared_functions import ( base64_decode, get_header_value, @@ -12,7 +12,7 @@ ) -class VPCLatticeEvent(BaseProxyEvent): +class VPCLatticeEventBase(BaseProxyEvent): @property def body(self) -> str: """The VPC Lattice body.""" @@ -30,11 +30,6 @@ def headers(self) -> Dict[str, str]: """The VPC Lattice event headers.""" return self["headers"] - @property - def is_base64_encoded(self) -> bool: - """A boolean flag to indicate if the applicable request payload is Base64-encode""" - return self["is_base64_encoded"] - @property def decoded_body(self) -> str: """Dynamically base64 decode body as a str""" @@ -48,29 +43,16 @@ def method(self) -> str: """The VPC Lattice method used. Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT.""" return self["method"] - @property - def query_string_parameters(self) -> Dict[str, str]: - """The request query string parameters.""" - return self["query_string_parameters"] - - @property - def raw_path(self) -> str: - """The raw VPC Lattice request path.""" - return self["raw_path"] - - # VPCLattice event has no path field - # Added here for consistency with the BaseProxyEvent class - @property - def path(self) -> str: - return self["raw_path"] - - # VPCLattice event has no http_method field - # Added here for consistency with the BaseProxyEvent class @property def http_method(self) -> str: """The HTTP method used. Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT.""" return self["method"] + @property + def query_string_parameters(self) -> Dict[str, str]: + """The request query string parameters.""" + return self["query_string_parameters"] + def get_query_string_value(self, name: str, default_value: Optional[str] = None) -> Optional[str]: """Get query string value by name @@ -142,129 +124,73 @@ def header_serializer(self) -> BaseHeadersSerializer: return HttpApiHeadersSerializer() -class VPCLatticeEventV2(BaseProxyEvent): +class VPCLatticeEvent(VPCLatticeEventBase): @property - def version(self) -> str: - """The VPC Lattice v2 Event version""" - return self["version"] + def raw_path(self) -> str: + """The raw VPC Lattice request path.""" + return self["raw_path"] @property - def path(self) -> str: - """The VPC Lattice v2 Event path""" - return self["path"] + def is_base64_encoded(self) -> bool: + """A boolean flag to indicate if the applicable request payload is Base64-encode""" + return self["is_base64_encoded"] + # VPCLattice event has no path field + # Added here for consistency with the BaseProxyEvent class @property - def method(self) -> str: - """ - The VPC Lattice v2 Event method used. - Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT. - """ - return self["method"] + def path(self) -> str: + return self["raw_path"] - @property - def headers(self) -> Dict[str, str]: - """The VPC Lattice v2 Event headers.""" - return self["headers"] +class vpcLatticeEventV2RequestContext(DictWrapper): @property - def query_string_parameters(self) -> Dict[str, str]: - """The request query string parameters.""" - return self["query_string_parameters"] + def service_network_arn(self) -> str: + """The VPC Lattice v2 Event requestContext serviceNetworkArn""" + return self["serviceNetworkArn"] @property - def body(self) -> str: - """The VPC Lattice v2 Event body.""" - return self["body"] + def service_arn(self) -> str: + """The VPC Lattice v2 Event requestContext serviceArn""" + return self["serviceArn"] @property - def is_base64_encoded(self) -> bool: - """A boolean flag to indicate if the applicable request payload is Base64-encode""" - return self["is_base64_encoded"] + def target_group_arn(self) -> str: + """The VPC Lattice v2 Event requestContext targetGroupArn""" + return self["targetGroupArn"] @property - def request_context(self) -> Dict[str, str]: - """he VPC Lattice v2 Event request context.""" - return self["requestContext"] + def source_vpc_arn(self) -> str: + """The VPC Lattice v2 Event requestContext sourceVpcArn""" + return self["identity"].get("sourceVpcArn") @property - def json_body(self) -> Any: - """Parses the submitted body as json""" - if self._json_data is None: - self._json_data = self._json_deserializer(self.decoded_body) - return self._json_data + def region(self) -> str: + """The VPC Lattice v2 Event requestContext serviceNetworkArn""" + return self["region"] @property - def decoded_body(self) -> str: - """Dynamically base64 decode body as a str""" - body: str = self["body"] - return base64_decode(body) if self.is_base64_encoded else body + def time_epoch(self) -> str: + """The VPC Lattice v2 Event requestContext timeEpoch""" + return self["timeEpoch"] - def get_query_string_value(self, name: str, default_value: Optional[str] = None) -> Optional[str]: - """Get query string value by name - Parameters - ---------- - name: str - Query string parameter name - default_value: str, optional - Default value if no value was found by name - Returns - ------- - str, optional - Query string parameter value - """ - return get_query_string_value( - query_string_parameters=self.query_string_parameters, - name=name, - default_value=default_value, - ) - - @overload - def get_header_value( - self, - name: str, - default_value: str, - case_sensitive: Optional[bool] = False, - ) -> str: - ... - - @overload - def get_header_value( - self, - name: str, - default_value: Optional[str] = None, - case_sensitive: Optional[bool] = False, - ) -> Optional[str]: - ... +class VPCLatticeEventV2(VPCLatticeEventBase): + @property + def version(self) -> str: + """The VPC Lattice v2 Event version""" + return self["version"] - def get_header_value( - self, - name: str, - default_value: Optional[str] = None, - case_sensitive: Optional[bool] = False, - ) -> Optional[str]: - """Get header value by name + @property + def is_base64_encoded(self) -> bool: + """A boolean flag to indicate if the applicable request payload is Base64-encode""" + return self["isBase64Encoded"] - Parameters - ---------- - name: str - Header name - default_value: str, optional - Default value if no value was found by name - case_sensitive: bool - Whether to use a case-sensitive look up - Returns - ------- - str, optional - Header value - """ - return get_header_value( - headers=self.headers, - name=name, - default_value=default_value, - case_sensitive=case_sensitive, - ) + @property + def path(self) -> str: + """The VPC Lattice v2 Event path""" + return self["path"] - def header_serializer(self) -> BaseHeadersSerializer: - # When using the VPC Lattice integration, we have multiple HTTP Headers. - return HttpApiHeadersSerializer() + @property + def request_context(self) -> vpcLatticeEventV2RequestContext: + """he VPC Lattice v2 Event request context.""" + return vpcLatticeEventV2RequestContext(self["requestContext"]) diff --git a/aws_lambda_powertools/utilities/parser/envelopes/__init__.py b/aws_lambda_powertools/utilities/parser/envelopes/__init__.py index 2e1a7af2f03..affffd98174 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/__init__.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/__init__.py @@ -10,7 +10,8 @@ from .lambda_function_url import LambdaFunctionUrlEnvelope from .sns import SnsEnvelope, SnsSqsEnvelope from .sqs import SqsEnvelope -from .vpc_lattice import VpcLatticeEnvelope, VpcLatticeV2Envelope +from .vpc_lattice import VpcLatticeEnvelope +from .vpc_latticev2 import VpcLatticeV2Envelope __all__ = [ "ApiGatewayEnvelope", diff --git a/aws_lambda_powertools/utilities/parser/envelopes/vpc_lattice.py b/aws_lambda_powertools/utilities/parser/envelopes/vpc_lattice.py index ea944d27bea..fb95ac05a8d 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/vpc_lattice.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/vpc_lattice.py @@ -1,7 +1,7 @@ import logging from typing import Any, Dict, Optional, Type, Union -from ..models import VpcLatticeModel, VpcLatticeV2Model +from ..models import VpcLatticeModel from ..types import Model from .base import BaseEnvelope @@ -30,26 +30,3 @@ def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Type[Model]) parsed_envelope: VpcLatticeModel = VpcLatticeModel.parse_obj(data) logger.debug(f"Parsing event payload in `detail` with {model}") return self._parse(data=parsed_envelope.body, model=model) - -class VpcLatticeV2Envelope(BaseEnvelope): - """Amazon VPC Lattice envelope to extract data within body key""" - - def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Type[Model]) -> Optional[Model]: - """Parses data found with model provided - - Parameters - ---------- - data : Dict - Lambda event to be parsed - model : Type[Model] - Data model provided to parse after extracting data using envelope - - Returns - ------- - Optional[Model] - Parsed detail payload with model provided - """ - logger.debug(f"Parsing incoming data with VPC Lattice V2 model {VpcLatticeV2Model}") - parsed_envelope: VpcLatticeV2Model = VpcLatticeV2Model.parse_obj(data) - logger.debug(f"Parsing event payload in `detail` with {model}") - return self._parse(data=parsed_envelope.body, model=model) diff --git a/aws_lambda_powertools/utilities/parser/envelopes/vpc_latticev2.py b/aws_lambda_powertools/utilities/parser/envelopes/vpc_latticev2.py new file mode 100644 index 00000000000..77dbf2a4a24 --- /dev/null +++ b/aws_lambda_powertools/utilities/parser/envelopes/vpc_latticev2.py @@ -0,0 +1,32 @@ +import logging +from typing import Any, Dict, Optional, Type, Union + +from ..models import VpcLatticeV2Model +from ..types import Model +from .base import BaseEnvelope + +logger = logging.getLogger(__name__) + + +class VpcLatticeV2Envelope(BaseEnvelope): + """Amazon VPC Lattice envelope to extract data within body key""" + + def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Type[Model]) -> Optional[Model]: + """Parses data found with model provided + + Parameters + ---------- + data : Dict + Lambda event to be parsed + model : Type[Model] + Data model provided to parse after extracting data using envelope + + Returns + ------- + Optional[Model] + Parsed detail payload with model provided + """ + logger.debug(f"Parsing incoming data with VPC Lattice V2 model {VpcLatticeV2Model}") + parsed_envelope: VpcLatticeV2Model = VpcLatticeV2Model.parse_obj(data) + logger.debug(f"Parsing event payload in `detail` with {model}") + return self._parse(data=parsed_envelope.body, model=model) diff --git a/aws_lambda_powertools/utilities/parser/models/__init__.py b/aws_lambda_powertools/utilities/parser/models/__init__.py index dc316153d88..3c707fda61e 100644 --- a/aws_lambda_powertools/utilities/parser/models/__init__.py +++ b/aws_lambda_powertools/utilities/parser/models/__init__.py @@ -88,7 +88,8 @@ ) from .sns import SnsModel, SnsNotificationModel, SnsRecordModel from .sqs import SqsAttributesModel, SqsModel, SqsMsgAttributeModel, SqsRecordModel -from .vpc_lattice import VpcLatticeModel, VpcLatticeV2Model +from .vpc_lattice import VpcLatticeModel +from .vpc_latticev2 import VpcLatticeV2Model __all__ = [ "APIGatewayProxyEventV2Model", diff --git a/aws_lambda_powertools/utilities/parser/models/vpc_lattice.py b/aws_lambda_powertools/utilities/parser/models/vpc_lattice.py index 9cf193c08ec..8442fc92781 100644 --- a/aws_lambda_powertools/utilities/parser/models/vpc_lattice.py +++ b/aws_lambda_powertools/utilities/parser/models/vpc_lattice.py @@ -1,6 +1,6 @@ from typing import Dict, Type, Union -from pydantic import BaseModel, Field +from pydantic import BaseModel class VpcLatticeModel(BaseModel): @@ -10,14 +10,3 @@ class VpcLatticeModel(BaseModel): is_base64_encoded: bool headers: Dict[str, str] query_string_parameters: Dict[str, str] - - -class VpcLatticeV2Model(BaseModel): - version: str - path: str - method: str - headers: Dict[str, str] - query_string_parameters: Dict[str, str] - body: Union[str, Type[BaseModel]] - is_base64_encoded: bool - request_context: Dict[str, str] = Field(alias="requestContext") diff --git a/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py b/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py new file mode 100644 index 00000000000..a9846db256a --- /dev/null +++ b/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py @@ -0,0 +1,28 @@ +from datetime import datetime +from typing import Dict, Optional, Type, Union + +from pydantic import BaseModel, Field + + +class VpcLatticeV2RequestContextIdentity(BaseModel): + source_vpc_arn: str = Field(alias="sourceVpcArn") + + +class VpcLatticeV2RequestContext(BaseModel): + service_network_arn: str = Field(alias="serviceNetworkArn") + service_arn: str = Field(alias="serviceArn") + target_group_arn: str = Field(alias="targetGroupArn") + identity: VpcLatticeV2RequestContextIdentity + region: str + time_epoch: datetime = Field(alias="timeEpoch") + + +class VpcLatticeV2Model(BaseModel): + version: str + path: str + method: str + headers: Dict[str, str] + query_string_parameters: Optional[Dict[str, str]] = None + body: Optional[Union[str, Type[BaseModel]]] = None + is_base64_encoded: Optional[bool] = Field(None, alias="isBase64Encoded") + request_context: VpcLatticeV2RequestContext = Field(None, alias="requestContext") diff --git a/tests/events/vpcLatticeV2Event .json b/tests/events/vpcLatticeV2Event.json similarity index 90% rename from tests/events/vpcLatticeV2Event .json rename to tests/events/vpcLatticeV2Event.json index 2e754b295bb..696c8ed57c8 100644 --- a/tests/events/vpcLatticeV2Event .json +++ b/tests/events/vpcLatticeV2Event.json @@ -9,11 +9,9 @@ "accept": "*/*" }, "query_string_parameters": { - "order-id": "314159" - }, - "body": { - "message": "Hello from Powertools!" + "order-id": "1" }, + "body": "{\"message\": \"Hello from Lambda!\"}", "isBase64Encoded": false, "requestContext": { "serviceNetworkArn": "arn:aws:vpc-lattice:us-east-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", diff --git a/tests/unit/data_classes/test_vpc_lattice_eventv2.py b/tests/unit/data_classes/test_vpc_lattice_eventv2.py new file mode 100644 index 00000000000..20bf244a1b2 --- /dev/null +++ b/tests/unit/data_classes/test_vpc_lattice_eventv2.py @@ -0,0 +1,24 @@ +from aws_lambda_powertools.utilities.data_classes.vpc_lattice import VPCLatticeEventV2 +from tests.functional.utils import load_event + + +def test_vpc_lattice_event(): + raw_event = load_event("vpcLatticeV2Event.json") + parsed_event = VPCLatticeEventV2(raw_event) + + assert parsed_event.path == raw_event["path"] + assert parsed_event.get_query_string_value("order-id") == "1" + assert parsed_event.get_header_value("user_agent") == "curl/7.64.1" + assert parsed_event.decoded_body == '{"message": "Hello from Lambda!"}' + assert parsed_event.json_body == {"message": "Hello from Lambda!"} + assert parsed_event.method == raw_event["method"] + assert parsed_event.headers == raw_event["headers"] + assert parsed_event.query_string_parameters == raw_event["query_string_parameters"] + assert parsed_event.body == raw_event["body"] + assert parsed_event.is_base64_encoded == raw_event["isBase64Encoded"] + assert parsed_event.request_context.region == raw_event["requestContext"]["region"] + assert parsed_event.request_context.service_network_arn == raw_event["requestContext"]["serviceNetworkArn"] + assert parsed_event.request_context.service_arn == raw_event["requestContext"]["serviceArn"] + assert parsed_event.request_context.target_group_arn == raw_event["requestContext"]["targetGroupArn"] + assert parsed_event.request_context.source_vpc_arn == raw_event["requestContext"]["identity"]["sourceVpcArn"] + assert parsed_event.request_context.time_epoch == raw_event["requestContext"]["timeEpoch"] diff --git a/tests/unit/parser/test_vpc_lattice.py b/tests/unit/parser/test_vpc_lattice.py index dde6082e722..e5dfedfb445 100644 --- a/tests/unit/parser/test_vpc_lattice.py +++ b/tests/unit/parser/test_vpc_lattice.py @@ -1,7 +1,7 @@ import pytest from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, parse -from aws_lambda_powertools.utilities.parser.models import VpcLatticeModel, VpcLatticeV2Model +from aws_lambda_powertools.utilities.parser.models import VpcLatticeModel from tests.functional.utils import load_event from tests.unit.parser.schemas import MyVpcLatticeBusiness @@ -47,45 +47,3 @@ def test_vpc_lattice_event_invalid(): with pytest.raises(ValidationError): VpcLatticeModel(**raw_event) - - -def test_vpc_lattice_v2_event_with_envelope(): - raw_event = load_event("vpcLatticeV2Event.json") - raw_event["body"] = '{"username": "Stephen", "name": "Bawks"}' - parsed_event: MyVpcLatticeBusiness = parse( - event=raw_event, - model=MyVpcLatticeBusiness, - envelope=envelopes.VpcLatticeV2Envelope, - ) - - assert parsed_event.username == "Stephen" - assert parsed_event.name == "Bawks" - - -def test_vpc_lattice_v2_event(): - raw_event = load_event("vpcLatticeV2Event.json") - model = VpcLatticeV2Model(**raw_event) - - assert model.body == raw_event["body"] - assert model.method == raw_event["method"] - assert model.path == raw_event["path"] - assert model.is_base64_encoded == raw_event["is_base64_encoded"] - assert model.headers == raw_event["headers"] - assert model.query_string_parameters == raw_event["query_string_parameters"] - assert model.request_context == raw_event["request_context"] - -def test_vpc_lattice_v2_event_custom_model(): - class MyCustomResource(VpcLatticeV2Model): - body: str - - raw_event = load_event("vpcLatticeV2Event.json") - model = MyCustomResource(**raw_event) - - assert model.body == raw_event["body"] - -def test_vpc_lattice_v2_event_invalid(): - raw_event = load_event("vpcLatticeV2Event.json") - raw_event["body"] = ["some_more_data"] - - with pytest.raises(ValidationError): - VpcLatticeV2Model(**raw_event) diff --git a/tests/unit/parser/test_vpc_latticev2.py b/tests/unit/parser/test_vpc_latticev2.py new file mode 100644 index 00000000000..656b61153ad --- /dev/null +++ b/tests/unit/parser/test_vpc_latticev2.py @@ -0,0 +1,50 @@ +import pytest + +from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, parse +from aws_lambda_powertools.utilities.parser.models import VpcLatticeV2Model +from tests.functional.utils import load_event +from tests.unit.parser.schemas import MyVpcLatticeBusiness + + +def test_vpc_lattice_v2_event_with_envelope(): + raw_event = load_event("vpcLatticeV2Event.json") + raw_event["body"] = '{"username": "Stephen", "name": "Bawks"}' + parsed_event: MyVpcLatticeBusiness = parse( + event=raw_event, + model=MyVpcLatticeBusiness, + envelope=envelopes.VpcLatticeV2Envelope, + ) + + assert parsed_event.username == "Stephen" + assert parsed_event.name == "Bawks" + + +def test_vpc_lattice_v2_event(): + raw_event = load_event("vpcLatticeV2Event.json") + model = VpcLatticeV2Model(**raw_event) + + assert model.body == raw_event["body"] + assert model.method == raw_event["method"] + assert model.path == raw_event["path"] + assert model.is_base64_encoded == raw_event["isBase64Encoded"] + assert model.headers == raw_event["headers"] + assert model.query_string_parameters == raw_event["query_string_parameters"] + assert model.request_context == raw_event["requestContext"] + + +def test_vpc_lattice_v2_event_custom_model(): + class MyCustomResource(VpcLatticeV2Model): + body: str + + raw_event = load_event("vpcLatticeV2Event.json") + model = MyCustomResource(**raw_event) + + assert model.body == raw_event["body"] + + +def test_vpc_lattice_v2_event_invalid(): + raw_event = load_event("vpcLatticeV2Event.json") + raw_event["body"] = ["some_more_data"] + + with pytest.raises(ValidationError): + VpcLatticeV2Model(**raw_event) From fb07a968734d408528114a03943dd33e44e84c30 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Tue, 3 Oct 2023 09:33:50 -0400 Subject: [PATCH 16/27] pull and add examples --- docs/utilities/data_classes.md | 19 ++++++++++++++++++ examples/event_sources/src/vpc_lattice_v2.py | 20 +++++++++++++++++++ .../src/vpc_lattice_v2_payload.json | 6 ++++-- 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 examples/event_sources/src/vpc_lattice_v2.py rename tests/events/vpcLatticeV2Event.json => examples/event_sources/src/vpc_lattice_v2_payload.json (90%) diff --git a/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index fd4a176f631..30ef27528fe 100644 --- a/docs/utilities/data_classes.md +++ b/docs/utilities/data_classes.md @@ -104,6 +104,7 @@ Log Data Event for Troubleshooting | [SNS](#sns) | `SNSEvent` | | [SQS](#sqs) | `SQSEvent` | | [VPC Lattice](#vpc-lattice) | `VPCLatticeEvent` | +| [VPC Lattice V2](#vpc-lattice-v2) | `VPCLatticeV2Event` | ???+ info The examples provided below are far from exhaustive - the data classes themselves are designed to provide a form of @@ -1198,6 +1199,24 @@ You can register your Lambda functions as targets within an Amazon VPC Lattice s --8<-- "examples/event_sources/src/vpc_lattice_payload.json" ``` +### VPC Lattice V2 + +You can register your Lambda functions as targets within an Amazon VPC Lattice service network. By doing this, your Lambda function becomes a service within the network, and clients that have access to the VPC Lattice service network can call your service. + +[Click here](https://docs.aws.amazon.com/lambda/latest/dg/services-vpc-lattice.html){target="_blank"} for more information about using AWS Lambda with Amazon VPC Lattice. + +=== "app.py" + + ```python hl_lines="2 8" + --8<-- "examples/event_sources/src/vpc_lattice_v2.py" + ``` + +=== "Lattice Example Event" + + ```json + --8<-- "examples/event_sources/src/vpc_lattice_payload_v2.json" + ``` + ## Advanced ### Debugging diff --git a/examples/event_sources/src/vpc_lattice_v2.py b/examples/event_sources/src/vpc_lattice_v2.py new file mode 100644 index 00000000000..0d11328bd76 --- /dev/null +++ b/examples/event_sources/src/vpc_lattice_v2.py @@ -0,0 +1,20 @@ +from aws_lambda_powertools import Logger +from aws_lambda_powertools.utilities.data_classes import VPCLatticeEventV2, event_source +from aws_lambda_powertools.utilities.typing import LambdaContext + +logger = Logger() + + +@event_source(data_class=VPCLatticeEventV2) +def lambda_handler(event: VPCLatticeEventV2, context: LambdaContext): + logger.info(event.body) + + response = { + "isBase64Encoded": False, + "statusCode": 200, + "statusDescription": "200 OK", + "headers": {"Content-Type": "application/text"}, + "body": "VPC Lattice V2 Event ✨🎉✨", + } + + return response diff --git a/tests/events/vpcLatticeV2Event.json b/examples/event_sources/src/vpc_lattice_v2_payload.json similarity index 90% rename from tests/events/vpcLatticeV2Event.json rename to examples/event_sources/src/vpc_lattice_v2_payload.json index 696c8ed57c8..2e754b295bb 100644 --- a/tests/events/vpcLatticeV2Event.json +++ b/examples/event_sources/src/vpc_lattice_v2_payload.json @@ -9,9 +9,11 @@ "accept": "*/*" }, "query_string_parameters": { - "order-id": "1" + "order-id": "314159" + }, + "body": { + "message": "Hello from Powertools!" }, - "body": "{\"message\": \"Hello from Lambda!\"}", "isBase64Encoded": false, "requestContext": { "serviceNetworkArn": "arn:aws:vpc-lattice:us-east-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", From 47c12d4ef10b66690c9971e46d46050a9f8f1dc2 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Tue, 3 Oct 2023 09:41:22 -0400 Subject: [PATCH 17/27] where did you go --- tests/events/vpcLatticeV2Event.json | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/events/vpcLatticeV2Event.json diff --git a/tests/events/vpcLatticeV2Event.json b/tests/events/vpcLatticeV2Event.json new file mode 100644 index 00000000000..b42a1e0625c --- /dev/null +++ b/tests/events/vpcLatticeV2Event.json @@ -0,0 +1,26 @@ +{ + "version": "2.0", + "path": "/newpath", + "method": "GET", + "headers": { + "user_agent": "curl/7.64.1", + "x-forwarded-for": "10.213.229.10", + "host": "test-lambda-service-3908sdf9u3u.dkfjd93.vpc-lattice-svcs.us-east-2.on.aws", + "accept": "*/*" + }, + "query_string_parameters": { + "order-id": "1" + }, + "body": "{\"message\": \"Hello from Lambda!\"}", + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:us-east-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:us-east-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:us-east-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:us-east-2:123456789012:vpc/vpc-0b8276c84697e7339" + }, + "region": "us-east-2", + "timeEpoch": "1690497599177430" + } +} \ No newline at end of file From 54f9363d449c0688aeebf497b88997867c99b23c Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Tue, 3 Oct 2023 22:37:23 +0100 Subject: [PATCH 18/27] Adding more fields in parser and event source --- .../utilities/data_classes/vpc_lattice.py | 62 +++++++++++++++++-- .../utilities/parser/models/vpc_latticev2.py | 11 +++- tests/events/vpcLatticeV2Event.json | 10 ++- .../data_classes/test_vpc_lattice_eventv2.py | 13 +++- tests/unit/parser/test_vpc_latticev2.py | 17 ++++- 5 files changed, 102 insertions(+), 11 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py index 993458b2445..793a2be6359 100644 --- a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py +++ b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py @@ -142,6 +142,58 @@ def path(self) -> str: return self["raw_path"] +class vpcLatticeEventV2Identity(DictWrapper): + @property + def source_vpc_arn(self) -> Optional[str]: + """The VPC Lattice v2 Event requestContext Identity sourceVpcArn""" + return self.get("sourceVpcArn") + + @property + def get_type(self) -> Optional[str]: + """The VPC Lattice v2 Event requestContext Identity type""" + return self.get("type") + + @property + def principal(self) -> Optional[str]: + """The VPC Lattice v2 Event requestContext principal""" + return self.get("principal") + + @property + def principal_org_id(self) -> Optional[str]: + """The VPC Lattice v2 Event requestContext principalOrgID""" + return self.get("principalOrgID") + + @property + def session_name(self) -> Optional[str]: + """The VPC Lattice v2 Event requestContext sessionName""" + return self.get("sessionName") + + @property + def x509_subject_cn(self) -> Optional[str]: + """The VPC Lattice v2 Event requestContext X509SubjectCn""" + return self.get("X509SubjectCn") + + @property + def x509_issuer_ou(self) -> Optional[str]: + """The VPC Lattice v2 Event requestContext X509IssuerOu""" + return self.get("X509IssuerOu") + + @property + def x509_san_dns(self) -> Optional[str]: + """The VPC Lattice v2 Event requestContext X509SanDns""" + return self.get("x509SanDns") + + @property + def x509_san_uri(self) -> Optional[str]: + """The VPC Lattice v2 Event requestContext X509SanUri""" + return self.get("X509SanUri") + + @property + def x509_san_name_cn(self) -> Optional[str]: + """The VPC Lattice v2 Event requestContext X509SanNameCn""" + return self.get("X509SanNameCn") + + class vpcLatticeEventV2RequestContext(DictWrapper): @property def service_network_arn(self) -> str: @@ -159,9 +211,9 @@ def target_group_arn(self) -> str: return self["targetGroupArn"] @property - def source_vpc_arn(self) -> str: - """The VPC Lattice v2 Event requestContext sourceVpcArn""" - return self["identity"].get("sourceVpcArn") + def identity(self) -> vpcLatticeEventV2Identity: + """The VPC Lattice v2 Event requestContext identity""" + return vpcLatticeEventV2Identity(self["identity"]) @property def region(self) -> str: @@ -181,9 +233,9 @@ def version(self) -> str: return self["version"] @property - def is_base64_encoded(self) -> bool: + def is_base64_encoded(self) -> Optional[bool]: """A boolean flag to indicate if the applicable request payload is Base64-encode""" - return self["isBase64Encoded"] + return self.get("isBase64Encoded") @property def path(self) -> str: diff --git a/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py b/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py index a9846db256a..1fa175192df 100644 --- a/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py +++ b/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py @@ -5,7 +5,16 @@ class VpcLatticeV2RequestContextIdentity(BaseModel): - source_vpc_arn: str = Field(alias="sourceVpcArn") + source_vpc_arn: Optional[str] = Field(None, alias="sourceVpcArn") + get_type: Optional[str] = Field(None, alias="type") + principal: Optional[str] = Field(None, alias="principal") + principal_org_id: Optional[str] = Field(None, alias="principalOrgID") + session_name: Optional[str] = Field(None, alias="sessionName") + x509_subject_cn: Optional[str] = Field(None, alias="X509SubjectCn") + x509_issuer_ou: Optional[str] = Field(None, alias="X509IssuerOu") + x509_san_dns: Optional[str] = Field(None, alias="x509SanDns") + x509_san_uri: Optional[str] = Field(None, alias="X509SanUri") + x509_san_name_cn: Optional[str] = Field(None, alias="X509SanNameCn") class VpcLatticeV2RequestContext(BaseModel): diff --git a/tests/events/vpcLatticeV2Event.json b/tests/events/vpcLatticeV2Event.json index b42a1e0625c..9f751e2443c 100644 --- a/tests/events/vpcLatticeV2Event.json +++ b/tests/events/vpcLatticeV2Event.json @@ -18,9 +18,13 @@ "serviceArn": "arn:aws:vpc-lattice:us-east-2:123456789012:service/svc-0a40eebed65f8d69c", "targetGroupArn": "arn:aws:vpc-lattice:us-east-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", "identity": { - "sourceVpcArn": "arn:aws:ec2:us-east-2:123456789012:vpc/vpc-0b8276c84697e7339" + "sourceVpcArn": "arn:aws:ec2:region:123456789012:vpc/vpc-0b8276c84697e7339", + "type" : "AWS_IAM", + "principal": "arn:aws:sts::123456789012:assumed-role/example-role/057d00f8b51257ba3c853a0f248943cf", + "sessionName": "057d00f8b51257ba3c853a0f248943cf", + "x509SanDns": "example.com" }, "region": "us-east-2", - "timeEpoch": "1690497599177430" + "timeEpoch": "1696331543569073" } -} \ No newline at end of file +} diff --git a/tests/unit/data_classes/test_vpc_lattice_eventv2.py b/tests/unit/data_classes/test_vpc_lattice_eventv2.py index 20bf244a1b2..6e3b14442b9 100644 --- a/tests/unit/data_classes/test_vpc_lattice_eventv2.py +++ b/tests/unit/data_classes/test_vpc_lattice_eventv2.py @@ -20,5 +20,16 @@ def test_vpc_lattice_event(): assert parsed_event.request_context.service_network_arn == raw_event["requestContext"]["serviceNetworkArn"] assert parsed_event.request_context.service_arn == raw_event["requestContext"]["serviceArn"] assert parsed_event.request_context.target_group_arn == raw_event["requestContext"]["targetGroupArn"] - assert parsed_event.request_context.source_vpc_arn == raw_event["requestContext"]["identity"]["sourceVpcArn"] assert parsed_event.request_context.time_epoch == raw_event["requestContext"]["timeEpoch"] + assert ( + parsed_event.request_context.identity.source_vpc_arn == raw_event["requestContext"]["identity"]["sourceVpcArn"] + ) + assert parsed_event.request_context.identity.get_type == raw_event["requestContext"]["identity"]["type"] + assert parsed_event.request_context.identity.principal == raw_event["requestContext"]["identity"]["principal"] + assert parsed_event.request_context.identity.session_name == raw_event["requestContext"]["identity"]["sessionName"] + assert parsed_event.request_context.identity.x509_san_dns == raw_event["requestContext"]["identity"]["x509SanDns"] + assert parsed_event.request_context.identity.x509_issuer_ou is None + assert parsed_event.request_context.identity.x509_san_name_cn is None + assert parsed_event.request_context.identity.x509_san_uri is None + assert parsed_event.request_context.identity.x509_subject_cn is None + assert parsed_event.request_context.identity.principal_org_id is None diff --git a/tests/unit/parser/test_vpc_latticev2.py b/tests/unit/parser/test_vpc_latticev2.py index 656b61153ad..342db2dbea0 100644 --- a/tests/unit/parser/test_vpc_latticev2.py +++ b/tests/unit/parser/test_vpc_latticev2.py @@ -29,7 +29,22 @@ def test_vpc_lattice_v2_event(): assert model.is_base64_encoded == raw_event["isBase64Encoded"] assert model.headers == raw_event["headers"] assert model.query_string_parameters == raw_event["query_string_parameters"] - assert model.request_context == raw_event["requestContext"] + assert model.request_context.region == raw_event["requestContext"]["region"] + assert model.request_context.service_network_arn == raw_event["requestContext"]["serviceNetworkArn"] + assert model.request_context.service_arn == raw_event["requestContext"]["serviceArn"] + assert model.request_context.target_group_arn == raw_event["requestContext"]["targetGroupArn"] + convert_time = int((model.request_context.time_epoch.timestamp() * 1000000)) + assert convert_time == int(raw_event["requestContext"]["timeEpoch"]) + assert model.request_context.identity.source_vpc_arn == raw_event["requestContext"]["identity"]["sourceVpcArn"] + assert model.request_context.identity.get_type == raw_event["requestContext"]["identity"]["type"] + assert model.request_context.identity.principal == raw_event["requestContext"]["identity"]["principal"] + assert model.request_context.identity.session_name == raw_event["requestContext"]["identity"]["sessionName"] + assert model.request_context.identity.x509_san_dns == raw_event["requestContext"]["identity"]["x509SanDns"] + assert model.request_context.identity.x509_issuer_ou is None + assert model.request_context.identity.x509_san_name_cn is None + assert model.request_context.identity.x509_san_uri is None + assert model.request_context.identity.x509_subject_cn is None + assert model.request_context.identity.principal_org_id is None def test_vpc_lattice_v2_event_custom_model(): From d9c702bf81f6174b9cdbbbee47fe626195ea9bba Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Tue, 3 Oct 2023 22:53:28 +0100 Subject: [PATCH 19/27] Convert microsecond to milisecond - need to review --- .../utilities/parser/models/vpc_latticev2.py | 6 +++++- tests/unit/parser/test_vpc_latticev2.py | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py b/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py index 1fa175192df..14bb0231f1d 100644 --- a/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py +++ b/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import Dict, Optional, Type, Union -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, validator class VpcLatticeV2RequestContextIdentity(BaseModel): @@ -25,6 +25,10 @@ class VpcLatticeV2RequestContext(BaseModel): region: str time_epoch: datetime = Field(alias="timeEpoch") + @validator("time_epoch", pre=True, allow_reuse=True) + def time_epoch_convert_to_miliseconds(cls, value: int): + return round(int(value) / 1000) + class VpcLatticeV2Model(BaseModel): version: str diff --git a/tests/unit/parser/test_vpc_latticev2.py b/tests/unit/parser/test_vpc_latticev2.py index 342db2dbea0..e9384261f21 100644 --- a/tests/unit/parser/test_vpc_latticev2.py +++ b/tests/unit/parser/test_vpc_latticev2.py @@ -33,8 +33,9 @@ def test_vpc_lattice_v2_event(): assert model.request_context.service_network_arn == raw_event["requestContext"]["serviceNetworkArn"] assert model.request_context.service_arn == raw_event["requestContext"]["serviceArn"] assert model.request_context.target_group_arn == raw_event["requestContext"]["targetGroupArn"] - convert_time = int((model.request_context.time_epoch.timestamp() * 1000000)) - assert convert_time == int(raw_event["requestContext"]["timeEpoch"]) + convert_time = int((model.request_context.time_epoch.timestamp() * 1000)) + event_converted_time = round(int(raw_event["requestContext"]["timeEpoch"]) / 1000) + assert convert_time == event_converted_time assert model.request_context.identity.source_vpc_arn == raw_event["requestContext"]["identity"]["sourceVpcArn"] assert model.request_context.identity.get_type == raw_event["requestContext"]["identity"]["type"] assert model.request_context.identity.principal == raw_event["requestContext"]["identity"]["principal"] From 54d6a2f975b06d11927f6ed532a296ae05eac562 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Wed, 4 Oct 2023 07:53:51 -0400 Subject: [PATCH 20/27] additional tests --- .../vpcLatticeEventV2PathTrailingSlash.json | 30 ++++++++ .../event_handler/test_vpc_latticev2.py | 77 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 tests/events/vpcLatticeEventV2PathTrailingSlash.json create mode 100644 tests/functional/event_handler/test_vpc_latticev2.py diff --git a/tests/events/vpcLatticeEventV2PathTrailingSlash.json b/tests/events/vpcLatticeEventV2PathTrailingSlash.json new file mode 100644 index 00000000000..5f5fa7edd72 --- /dev/null +++ b/tests/events/vpcLatticeEventV2PathTrailingSlash.json @@ -0,0 +1,30 @@ +{ + "version": "2.0", + "path": "/newpath/", + "method": "GET", + "headers": { + "user_agent": "curl/7.64.1", + "x-forwarded-for": "10.213.229.10", + "host": "test-lambda-service-3908sdf9u3u.dkfjd93.vpc-lattice-svcs.us-east-2.on.aws", + "accept": "*/*" + }, + "query_string_parameters": { + "order-id": "1" + }, + "body": "{\"message\": \"Hello from Lambda!\"}", + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:us-east-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:us-east-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:us-east-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:region:123456789012:vpc/vpc-0b8276c84697e7339", + "type" : "AWS_IAM", + "principal": "arn:aws:sts::123456789012:assumed-role/example-role/057d00f8b51257ba3c853a0f248943cf", + "sessionName": "057d00f8b51257ba3c853a0f248943cf", + "x509SanDns": "example.com" + }, + "region": "us-east-2", + "timeEpoch": "1696331543569073" + } +} diff --git a/tests/functional/event_handler/test_vpc_latticev2.py b/tests/functional/event_handler/test_vpc_latticev2.py new file mode 100644 index 00000000000..959c752f189 --- /dev/null +++ b/tests/functional/event_handler/test_vpc_latticev2.py @@ -0,0 +1,77 @@ +from aws_lambda_powertools.event_handler import ( + Response, + VPCLatticeResolver, + content_types, +) +from aws_lambda_powertools.event_handler.api_gateway import CORSConfig +from aws_lambda_powertools.utilities.data_classes import VPCLatticeEventV2 +from tests.functional.utils import load_event + + +def test_vpclattice_event(): + # GIVEN a VPC Lattice event + app = VPCLatticeResolver() + + @app.get("/newpath") + def foo(): + assert isinstance(app.current_event, VPCLatticeEventV2) + assert app.lambda_context == {} + return Response(200, content_types.TEXT_HTML, "foo") + + # WHEN calling the event handler + result = app(load_event("vpcLatticeV2Event.json"), {}) + + # THEN process event correctly + # AND set the current_event type as VPCLatticeEvent + assert result["statusCode"] == 200 + assert result["headers"]["Content-Type"] == content_types.TEXT_HTML + assert result["body"] == "foo" + + +def test_vpclattice_event_path_trailing_slash(json_dump): + # GIVEN a VPC Lattice event + app = VPCLatticeResolver() + + @app.get("/newpath") + def foo(): + assert isinstance(app.current_event, VPCLatticeEventV2) + assert app.lambda_context == {} + return Response(200, content_types.TEXT_HTML, "foo") + + # WHEN calling the event handler using path with trailing "/" + result = app(load_event("vpcLatticeEventPathTrailingSlash.json"), {}) + + # THEN + assert result["statusCode"] == 404 + assert result["headers"]["Content-Type"] == content_types.APPLICATION_JSON + expected = {"statusCode": 404, "message": "Not found"} + assert result["body"] == json_dump(expected) + + +def test_cors_preflight_body_is_empty_not_null(): + # GIVEN CORS is configured + app = VPCLatticeResolver(cors=CORSConfig()) + + event = {"raw_path": "/my/request", "method": "OPTIONS", "headers": {}} + + # WHEN calling the event handler + result = app(event, {}) + + # THEN there body should be empty strings + assert result["body"] == "" + + +def test_vpclattice_url_no_matches(): + # GIVEN a VPC Lattice event + app = VPCLatticeResolver() + + @app.post("/no_match") + def foo(): + raise RuntimeError() + + # WHEN calling the event handler + result = app(load_event("vpcLatticeV2Event.json"), {}) + + # THEN process event correctly + # AND return 404 because the event doesn't match any known route + assert result["statusCode"] == 404 From 0c5cf990ddfdf2b2c9ca32ae5ed70486293e80f9 Mon Sep 17 00:00:00 2001 From: stephenbawks Date: Wed, 4 Oct 2023 08:41:34 -0400 Subject: [PATCH 21/27] fix path --- tests/functional/event_handler/test_vpc_latticev2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/event_handler/test_vpc_latticev2.py b/tests/functional/event_handler/test_vpc_latticev2.py index 959c752f189..9b8bc930f9d 100644 --- a/tests/functional/event_handler/test_vpc_latticev2.py +++ b/tests/functional/event_handler/test_vpc_latticev2.py @@ -52,7 +52,7 @@ def test_cors_preflight_body_is_empty_not_null(): # GIVEN CORS is configured app = VPCLatticeResolver(cors=CORSConfig()) - event = {"raw_path": "/my/request", "method": "OPTIONS", "headers": {}} + event = {"path": "/my/request", "method": "OPTIONS", "headers": {}} # WHEN calling the event handler result = app(event, {}) From 2b50d1387f78c7d11a63b010a1cdbbe096c0e850 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Wed, 4 Oct 2023 12:04:16 +0100 Subject: [PATCH 22/27] Wording --- aws_lambda_powertools/event_handler/api_gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 15f8ddb82a9..46cb5587135 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -1002,7 +1002,7 @@ def _to_proxy_event(self, event: Dict) -> BaseProxyEvent: logger.debug("Converting event to VPC Lattice contract") return VPCLatticeEvent(event) if self._proxy_type == ProxyEventType.VPCLatticeEventV2: - logger.debug("Converting event to VPC Lattice contract") + logger.debug("Converting event to VPC LatticeV2 contract") return VPCLatticeEventV2(event) logger.debug("Converting event to ALB contract") return ALBEvent(event) From 46c00093765fc2fd4dd3ec72cfe5c8e94dd5c05e Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Wed, 4 Oct 2023 15:43:42 +0100 Subject: [PATCH 23/27] Adding VPC Lattice v2 resolver --- .../event_handler/__init__.py | 3 +- .../event_handler/vpc_lattice.py | 47 ++++++++++++++++++- .../utilities/data_classes/vpc_lattice.py | 15 ++++-- .../utilities/parser/models/vpc_latticev2.py | 2 +- tests/events/vpcLatticeV2Event.json | 2 +- .../event_handler/test_vpc_latticev2.py | 16 +++---- .../data_classes/test_vpc_lattice_eventv2.py | 4 +- 7 files changed, 70 insertions(+), 19 deletions(-) diff --git a/aws_lambda_powertools/event_handler/__init__.py b/aws_lambda_powertools/event_handler/__init__.py index 85298cfc15c..7bdd9a97f72 100644 --- a/aws_lambda_powertools/event_handler/__init__.py +++ b/aws_lambda_powertools/event_handler/__init__.py @@ -14,7 +14,7 @@ from aws_lambda_powertools.event_handler.lambda_function_url import ( LambdaFunctionUrlResolver, ) -from aws_lambda_powertools.event_handler.vpc_lattice import VPCLatticeResolver +from aws_lambda_powertools.event_handler.vpc_lattice import VPCLatticeResolver, VPCLatticeV2Resolver __all__ = [ "AppSyncResolver", @@ -26,4 +26,5 @@ "LambdaFunctionUrlResolver", "Response", "VPCLatticeResolver", + "VPCLatticeV2Resolver", ] diff --git a/aws_lambda_powertools/event_handler/vpc_lattice.py b/aws_lambda_powertools/event_handler/vpc_lattice.py index b3cb042b40b..bcee046e382 100644 --- a/aws_lambda_powertools/event_handler/vpc_lattice.py +++ b/aws_lambda_powertools/event_handler/vpc_lattice.py @@ -5,7 +5,7 @@ ApiGatewayResolver, ProxyEventType, ) -from aws_lambda_powertools.utilities.data_classes import VPCLatticeEvent +from aws_lambda_powertools.utilities.data_classes import VPCLatticeEvent, VPCLatticeEventV2 class VPCLatticeResolver(ApiGatewayResolver): @@ -51,3 +51,48 @@ def __init__( ): """Amazon VPC Lattice resolver""" super().__init__(ProxyEventType.VPCLatticeEvent, cors, debug, serializer, strip_prefixes) + + +class VPCLatticeV2Resolver(ApiGatewayResolver): + """VPC Lattice resolver + + Documentation: + - https://docs.aws.amazon.com/lambda/latest/dg/services-vpc-lattice.html + - https://docs.aws.amazon.com/lambda/latest/dg/services-vpc-lattice.html#vpc-lattice-receiving-events + + Examples + -------- + Simple example integrating with Tracer + + ```python + from aws_lambda_powertools import Tracer + from aws_lambda_powertools.event_handler import VPCLatticeV2Resolver + + tracer = Tracer() + app = VPCLatticeV2Resolver() + + @app.get("/get-call") + def simple_get(): + return {"message": "Foo"} + + @app.post("/post-call") + def simple_post(): + post_data: dict = app.current_event.json_body + return {"message": post_data} + + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + """ + + current_event: VPCLatticeEventV2 + + def __init__( + self, + cors: Optional[CORSConfig] = None, + debug: Optional[bool] = None, + serializer: Optional[Callable[[Dict], str]] = None, + strip_prefixes: Optional[List[Union[str, Pattern]]] = None, + ): + """Amazon VPC Lattice resolver""" + super().__init__(ProxyEventType.VPCLatticeEventV2, cors, debug, serializer, strip_prefixes) diff --git a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py index 793a2be6359..693b3bce35a 100644 --- a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py +++ b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py @@ -48,11 +48,6 @@ def http_method(self) -> str: """The HTTP method used. Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT.""" return self["method"] - @property - def query_string_parameters(self) -> Dict[str, str]: - """The request query string parameters.""" - return self["query_string_parameters"] - def get_query_string_value(self, name: str, default_value: Optional[str] = None) -> Optional[str]: """Get query string value by name @@ -141,6 +136,11 @@ def is_base64_encoded(self) -> bool: def path(self) -> str: return self["raw_path"] + @property + def query_string_parameters(self) -> Dict[str, str]: + """The request query string parameters.""" + return self["query_string_parameters"] + class vpcLatticeEventV2Identity(DictWrapper): @property @@ -246,3 +246,8 @@ def path(self) -> str: def request_context(self) -> vpcLatticeEventV2RequestContext: """he VPC Lattice v2 Event request context.""" return vpcLatticeEventV2RequestContext(self["requestContext"]) + + @property + def query_string_parameters(self) -> Dict[str, str]: + """The request query string parameters.""" + return self["queryStringParameters"] diff --git a/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py b/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py index 14bb0231f1d..0f1d72818d7 100644 --- a/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py +++ b/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py @@ -35,7 +35,7 @@ class VpcLatticeV2Model(BaseModel): path: str method: str headers: Dict[str, str] - query_string_parameters: Optional[Dict[str, str]] = None + query_string_parameters: Optional[Dict[str, str]] = Field(None, alias="queryStringParameters") body: Optional[Union[str, Type[BaseModel]]] = None is_base64_encoded: Optional[bool] = Field(None, alias="isBase64Encoded") request_context: VpcLatticeV2RequestContext = Field(None, alias="requestContext") diff --git a/tests/events/vpcLatticeV2Event.json b/tests/events/vpcLatticeV2Event.json index 9f751e2443c..fe10d83a3af 100644 --- a/tests/events/vpcLatticeV2Event.json +++ b/tests/events/vpcLatticeV2Event.json @@ -8,7 +8,7 @@ "host": "test-lambda-service-3908sdf9u3u.dkfjd93.vpc-lattice-svcs.us-east-2.on.aws", "accept": "*/*" }, - "query_string_parameters": { + "queryStringParameters": { "order-id": "1" }, "body": "{\"message\": \"Hello from Lambda!\"}", diff --git a/tests/functional/event_handler/test_vpc_latticev2.py b/tests/functional/event_handler/test_vpc_latticev2.py index 9b8bc930f9d..32c3276577b 100644 --- a/tests/functional/event_handler/test_vpc_latticev2.py +++ b/tests/functional/event_handler/test_vpc_latticev2.py @@ -1,6 +1,6 @@ from aws_lambda_powertools.event_handler import ( Response, - VPCLatticeResolver, + VPCLatticeV2Resolver, content_types, ) from aws_lambda_powertools.event_handler.api_gateway import CORSConfig @@ -8,9 +8,9 @@ from tests.functional.utils import load_event -def test_vpclattice_event(): +def test_vpclatticev2_event(): # GIVEN a VPC Lattice event - app = VPCLatticeResolver() + app = VPCLatticeV2Resolver() @app.get("/newpath") def foo(): @@ -28,9 +28,9 @@ def foo(): assert result["body"] == "foo" -def test_vpclattice_event_path_trailing_slash(json_dump): +def test_vpclatticev2_event_path_trailing_slash(json_dump): # GIVEN a VPC Lattice event - app = VPCLatticeResolver() + app = VPCLatticeV2Resolver() @app.get("/newpath") def foo(): @@ -50,7 +50,7 @@ def foo(): def test_cors_preflight_body_is_empty_not_null(): # GIVEN CORS is configured - app = VPCLatticeResolver(cors=CORSConfig()) + app = VPCLatticeV2Resolver(cors=CORSConfig()) event = {"path": "/my/request", "method": "OPTIONS", "headers": {}} @@ -61,9 +61,9 @@ def test_cors_preflight_body_is_empty_not_null(): assert result["body"] == "" -def test_vpclattice_url_no_matches(): +def test_vpclatticev2_url_no_matches(): # GIVEN a VPC Lattice event - app = VPCLatticeResolver() + app = VPCLatticeV2Resolver() @app.post("/no_match") def foo(): diff --git a/tests/unit/data_classes/test_vpc_lattice_eventv2.py b/tests/unit/data_classes/test_vpc_lattice_eventv2.py index 6e3b14442b9..3726831445f 100644 --- a/tests/unit/data_classes/test_vpc_lattice_eventv2.py +++ b/tests/unit/data_classes/test_vpc_lattice_eventv2.py @@ -2,7 +2,7 @@ from tests.functional.utils import load_event -def test_vpc_lattice_event(): +def test_vpc_lattice_v2_event(): raw_event = load_event("vpcLatticeV2Event.json") parsed_event = VPCLatticeEventV2(raw_event) @@ -13,7 +13,7 @@ def test_vpc_lattice_event(): assert parsed_event.json_body == {"message": "Hello from Lambda!"} assert parsed_event.method == raw_event["method"] assert parsed_event.headers == raw_event["headers"] - assert parsed_event.query_string_parameters == raw_event["query_string_parameters"] + assert parsed_event.query_string_parameters == raw_event["queryStringParameters"] assert parsed_event.body == raw_event["body"] assert parsed_event.is_base64_encoded == raw_event["isBase64Encoded"] assert parsed_event.request_context.region == raw_event["requestContext"]["region"] From 157556f7b378838b4d3d2455bf31f044b8a51af6 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Wed, 4 Oct 2023 15:53:24 +0100 Subject: [PATCH 24/27] Fixing resolver --- aws_lambda_powertools/utilities/data_classes/vpc_lattice.py | 2 +- tests/functional/event_handler/test_vpc_latticev2.py | 2 +- tests/unit/parser/test_vpc_latticev2.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py index 693b3bce35a..864b1aca1ec 100644 --- a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py +++ b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py @@ -250,4 +250,4 @@ def request_context(self) -> vpcLatticeEventV2RequestContext: @property def query_string_parameters(self) -> Dict[str, str]: """The request query string parameters.""" - return self["queryStringParameters"] + return self.get("queryStringParameters") diff --git a/tests/functional/event_handler/test_vpc_latticev2.py b/tests/functional/event_handler/test_vpc_latticev2.py index 32c3276577b..e249b7d2ba1 100644 --- a/tests/functional/event_handler/test_vpc_latticev2.py +++ b/tests/functional/event_handler/test_vpc_latticev2.py @@ -39,7 +39,7 @@ def foo(): return Response(200, content_types.TEXT_HTML, "foo") # WHEN calling the event handler using path with trailing "/" - result = app(load_event("vpcLatticeEventPathTrailingSlash.json"), {}) + result = app(load_event("vpcLatticeEventV2PathTrailingSlash.json"), {}) # THEN assert result["statusCode"] == 404 diff --git a/tests/unit/parser/test_vpc_latticev2.py b/tests/unit/parser/test_vpc_latticev2.py index e9384261f21..9aa862e36ec 100644 --- a/tests/unit/parser/test_vpc_latticev2.py +++ b/tests/unit/parser/test_vpc_latticev2.py @@ -28,7 +28,7 @@ def test_vpc_lattice_v2_event(): assert model.path == raw_event["path"] assert model.is_base64_encoded == raw_event["isBase64Encoded"] assert model.headers == raw_event["headers"] - assert model.query_string_parameters == raw_event["query_string_parameters"] + assert model.query_string_parameters == raw_event["queryStringParameters"] assert model.request_context.region == raw_event["requestContext"]["region"] assert model.request_context.service_network_arn == raw_event["requestContext"]["serviceNetworkArn"] assert model.request_context.service_arn == raw_event["requestContext"]["serviceArn"] From 5c56a435702f299f60177a7bfcbe39a3d3667e27 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Wed, 4 Oct 2023 16:27:47 +0100 Subject: [PATCH 25/27] Fixing mypy --- aws_lambda_powertools/utilities/data_classes/vpc_lattice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py index 864b1aca1ec..710a12085e2 100644 --- a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py +++ b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py @@ -248,6 +248,6 @@ def request_context(self) -> vpcLatticeEventV2RequestContext: return vpcLatticeEventV2RequestContext(self["requestContext"]) @property - def query_string_parameters(self) -> Dict[str, str]: + def query_string_parameters(self) -> Optional[Dict[str, str]]: """The request query string parameters.""" return self.get("queryStringParameters") From 144df4b044259b2286cf0d3a3b9076d5c46b9c34 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Wed, 4 Oct 2023 17:38:37 +0100 Subject: [PATCH 26/27] Event Handler documentation --- docs/core/event_handler/api_gateway.md | 18 +++++-- docs/utilities/data_classes.md | 17 +++--- ...getting_started_vpclatticev2_resolver.json | 29 ++++++++++ .../getting_started_vpclatticev2_resolver.py | 28 ++++++++++ .../src/vpc_lattice_v2_payload.json | 53 ++++++++++--------- 5 files changed, 106 insertions(+), 39 deletions(-) create mode 100644 examples/event_handler_rest/src/getting_started_vpclatticev2_resolver.json create mode 100644 examples/event_handler_rest/src/getting_started_vpclatticev2_resolver.py diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 4e4e935f699..88d5096267a 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -122,15 +122,27 @@ When using [AWS Lambda Function URL](https://docs.aws.amazon.com/lambda/latest/d #### VPC Lattice -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`. +When using [VPC Lattice with AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/services-vpc-lattice.html){target="_blank"}, you can use `VPCLatticeV2Resolver`. -=== "getting_started_vpclattice_resolver.py" +=== "Payload v2 (Recommended)" + + ```python hl_lines="5 11" title="Using VPC Lattice resolver" + --8<-- "examples/event_handler_rest/src/getting_started_vpclatticev2_resolver.py" + ``` + +=== "Payload v2 (Recommended) - Sample Event" + + ```json hl_lines="2 3" title="Example payload delivered to the handler" + --8<-- "examples/event_handler_rest/src/getting_started_vpclatticev2_resolver.json" + ``` + +=== "Payload v1" ```python hl_lines="5 11" title="Using VPC Lattice resolver" --8<-- "examples/event_handler_rest/src/getting_started_vpclattice_resolver.py" ``` -=== "getting_started_vpclattice_resolver.json" +=== "Payload v1 - Sample Event" ```json hl_lines="2 3" title="Example payload delivered to the handler" --8<-- "examples/event_handler_rest/src/getting_started_vpclattice_resolver.json" diff --git a/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index 30ef27528fe..cb922975dac 100644 --- a/docs/utilities/data_classes.md +++ b/docs/utilities/data_classes.md @@ -103,9 +103,6 @@ Log Data Event for Troubleshooting | [SES](#ses) | `SESEvent` | | [SNS](#sns) | `SNSEvent` | | [SQS](#sqs) | `SQSEvent` | -| [VPC Lattice](#vpc-lattice) | `VPCLatticeEvent` | -| [VPC Lattice V2](#vpc-lattice-v2) | `VPCLatticeV2Event` | - ???+ info The examples provided below are far from exhaustive - the data classes themselves are designed to provide a form of documentation inherently (via autocompletion, types and docstrings). @@ -1181,25 +1178,25 @@ AWS Secrets Manager rotation uses an AWS Lambda function to update the secret. [ do_something_with(record.body) ``` -### VPC Lattice +### VPC Lattice Payload V2 -You can register your Lambda functions as targets within an Amazon VPC Lattice service network. By doing this, your Lambda function becomes a service within the network, and clients that have access to the VPC Lattice service network can call your service. +You can register your Lambda functions as targets within an Amazon VPC Lattice service network. By doing this, your Lambda function becomes a service within the network, and clients that have access to the VPC Lattice service network can call your service using [Payload V2](https://docs.aws.amazon.com/lambda/latest/dg/services-vpc-lattice.html#vpc-lattice-receiving-events){target="_blank"}. [Click here](https://docs.aws.amazon.com/lambda/latest/dg/services-vpc-lattice.html){target="_blank"} for more information about using AWS Lambda with Amazon VPC Lattice. === "app.py" ```python hl_lines="2 8" - --8<-- "examples/event_sources/src/vpc_lattice.py" + --8<-- "examples/event_sources/src/vpc_lattice_v2.py" ``` === "Lattice Example Event" ```json - --8<-- "examples/event_sources/src/vpc_lattice_payload.json" + --8<-- "examples/event_sources/src/vpc_lattice_v2_payload.json" ``` -### VPC Lattice V2 +### VPC Lattice Payload V1 You can register your Lambda functions as targets within an Amazon VPC Lattice service network. By doing this, your Lambda function becomes a service within the network, and clients that have access to the VPC Lattice service network can call your service. @@ -1208,13 +1205,13 @@ You can register your Lambda functions as targets within an Amazon VPC Lattice s === "app.py" ```python hl_lines="2 8" - --8<-- "examples/event_sources/src/vpc_lattice_v2.py" + --8<-- "examples/event_sources/src/vpc_lattice.py" ``` === "Lattice Example Event" ```json - --8<-- "examples/event_sources/src/vpc_lattice_payload_v2.json" + --8<-- "examples/event_sources/src/vpc_lattice_payload.json" ``` ## Advanced diff --git a/examples/event_handler_rest/src/getting_started_vpclatticev2_resolver.json b/examples/event_handler_rest/src/getting_started_vpclatticev2_resolver.json new file mode 100644 index 00000000000..38c94683432 --- /dev/null +++ b/examples/event_handler_rest/src/getting_started_vpclatticev2_resolver.json @@ -0,0 +1,29 @@ +{ + "version": "2.0", + "path": "/todos", + "method": "GET", + "headers": { + "user_agent": "curl/7.64.1", + "x-forwarded-for": "10.213.229.10", + "host": "test-lambda-service-3908sdf9u3u.dkfjd93.vpc-lattice-svcs.us-east-2.on.aws", + "accept": "*/*" + }, + "queryStringParameters": { + "order-id": "1" + }, + "body": "{\"message\": \"Hello from Lambda!\"}", + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:us-east-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:us-east-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:us-east-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:region:123456789012:vpc/vpc-0b8276c84697e7339", + "type" : "AWS_IAM", + "principal": "arn:aws:sts::123456789012:assumed-role/example-role/057d00f8b51257ba3c853a0f248943cf", + "sessionName": "057d00f8b51257ba3c853a0f248943cf", + "x509SanDns": "example.com" + }, + "region": "us-east-2", + "timeEpoch": "1696331543569073" + } +} diff --git a/examples/event_handler_rest/src/getting_started_vpclatticev2_resolver.py b/examples/event_handler_rest/src/getting_started_vpclatticev2_resolver.py new file mode 100644 index 00000000000..4cf61caecaf --- /dev/null +++ b/examples/event_handler_rest/src/getting_started_vpclatticev2_resolver.py @@ -0,0 +1,28 @@ +import requests +from requests import Response + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import VPCLatticeV2Resolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = VPCLatticeV2Resolver() + + +@app.get("/todos") +@tracer.capture_method +def get_todos(): + todos: Response = requests.get("https://jsonplaceholder.typicode.com/todos") + todos.raise_for_status() + + # for brevity, we'll limit to the first 10 only + return {"todos": todos.json()[:10]} + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPLICATION_LOAD_BALANCER) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/event_sources/src/vpc_lattice_v2_payload.json b/examples/event_sources/src/vpc_lattice_v2_payload.json index 2e754b295bb..38c94683432 100644 --- a/examples/event_sources/src/vpc_lattice_v2_payload.json +++ b/examples/event_sources/src/vpc_lattice_v2_payload.json @@ -1,28 +1,29 @@ { - "version": "2.0", - "path": "/newpath", - "method": "GET", - "headers": { - "user_agent": "curl/7.64.1", - "x-forwarded-for": "10.213.229.10", - "host": "test-lambda-service-3908sdf9u3u.dkfjd93.vpc-lattice-svcs.us-east-2.on.aws", - "accept": "*/*" - }, - "query_string_parameters": { - "order-id": "314159" - }, - "body": { - "message": "Hello from Powertools!" - }, - "isBase64Encoded": false, - "requestContext": { - "serviceNetworkArn": "arn:aws:vpc-lattice:us-east-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", - "serviceArn": "arn:aws:vpc-lattice:us-east-2:123456789012:service/svc-0a40eebed65f8d69c", - "targetGroupArn": "arn:aws:vpc-lattice:us-east-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", - "identity": { - "sourceVpcArn": "arn:aws:ec2:us-east-2:123456789012:vpc/vpc-0b8276c84697e7339" - }, - "region": "us-east-2", - "timeEpoch": "1690497599177430" - } + "version": "2.0", + "path": "/todos", + "method": "GET", + "headers": { + "user_agent": "curl/7.64.1", + "x-forwarded-for": "10.213.229.10", + "host": "test-lambda-service-3908sdf9u3u.dkfjd93.vpc-lattice-svcs.us-east-2.on.aws", + "accept": "*/*" + }, + "queryStringParameters": { + "order-id": "1" + }, + "body": "{\"message\": \"Hello from Lambda!\"}", + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:us-east-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:us-east-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:us-east-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:region:123456789012:vpc/vpc-0b8276c84697e7339", + "type" : "AWS_IAM", + "principal": "arn:aws:sts::123456789012:assumed-role/example-role/057d00f8b51257ba3c853a0f248943cf", + "sessionName": "057d00f8b51257ba3c853a0f248943cf", + "x509SanDns": "example.com" + }, + "region": "us-east-2", + "timeEpoch": "1696331543569073" + } } From ce1f3071e943992855fb5737e1f6b09afa863579 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Wed, 4 Oct 2023 18:03:57 +0100 Subject: [PATCH 27/27] Preserving timeEpoch field + adding a new one --- .../utilities/data_classes/vpc_lattice.py | 2 +- .../utilities/parser/models/vpc_latticev2.py | 5 +++-- docs/utilities/data_classes.md | 7 +++++-- tests/unit/parser/test_vpc_latticev2.py | 3 ++- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py index 710a12085e2..00ba5136eec 100644 --- a/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py +++ b/aws_lambda_powertools/utilities/data_classes/vpc_lattice.py @@ -221,7 +221,7 @@ def region(self) -> str: return self["region"] @property - def time_epoch(self) -> str: + def time_epoch(self) -> float: """The VPC Lattice v2 Event requestContext timeEpoch""" return self["timeEpoch"] diff --git a/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py b/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py index 0f1d72818d7..dc764684484 100644 --- a/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py +++ b/aws_lambda_powertools/utilities/parser/models/vpc_latticev2.py @@ -23,9 +23,10 @@ class VpcLatticeV2RequestContext(BaseModel): target_group_arn: str = Field(alias="targetGroupArn") identity: VpcLatticeV2RequestContextIdentity region: str - time_epoch: datetime = Field(alias="timeEpoch") + time_epoch: float = Field(alias="timeEpoch") + time_epoch_as_datetime: datetime = Field(alias="timeEpoch") - @validator("time_epoch", pre=True, allow_reuse=True) + @validator("time_epoch_as_datetime", pre=True, allow_reuse=True) def time_epoch_convert_to_miliseconds(cls, value: int): return round(int(value) / 1000) diff --git a/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index cb922975dac..7cc966313fb 100644 --- a/docs/utilities/data_classes.md +++ b/docs/utilities/data_classes.md @@ -103,6 +103,9 @@ Log Data Event for Troubleshooting | [SES](#ses) | `SESEvent` | | [SNS](#sns) | `SNSEvent` | | [SQS](#sqs) | `SQSEvent` | +| [VPC Lattice V2](#vpc-lattice-v2) | `VPCLatticeV2Event` | +| [VPC Lattice V1](#vpc-lattice-v1) | `VPCLatticeEvent` | + ???+ info The examples provided below are far from exhaustive - the data classes themselves are designed to provide a form of documentation inherently (via autocompletion, types and docstrings). @@ -1178,7 +1181,7 @@ AWS Secrets Manager rotation uses an AWS Lambda function to update the secret. [ do_something_with(record.body) ``` -### VPC Lattice Payload V2 +### VPC Lattice V2 You can register your Lambda functions as targets within an Amazon VPC Lattice service network. By doing this, your Lambda function becomes a service within the network, and clients that have access to the VPC Lattice service network can call your service using [Payload V2](https://docs.aws.amazon.com/lambda/latest/dg/services-vpc-lattice.html#vpc-lattice-receiving-events){target="_blank"}. @@ -1196,7 +1199,7 @@ You can register your Lambda functions as targets within an Amazon VPC Lattice s --8<-- "examples/event_sources/src/vpc_lattice_v2_payload.json" ``` -### VPC Lattice Payload V1 +### VPC Lattice V1 You can register your Lambda functions as targets within an Amazon VPC Lattice service network. By doing this, your Lambda function becomes a service within the network, and clients that have access to the VPC Lattice service network can call your service. diff --git a/tests/unit/parser/test_vpc_latticev2.py b/tests/unit/parser/test_vpc_latticev2.py index 9aa862e36ec..78d93fde041 100644 --- a/tests/unit/parser/test_vpc_latticev2.py +++ b/tests/unit/parser/test_vpc_latticev2.py @@ -33,7 +33,8 @@ def test_vpc_lattice_v2_event(): assert model.request_context.service_network_arn == raw_event["requestContext"]["serviceNetworkArn"] assert model.request_context.service_arn == raw_event["requestContext"]["serviceArn"] assert model.request_context.target_group_arn == raw_event["requestContext"]["targetGroupArn"] - convert_time = int((model.request_context.time_epoch.timestamp() * 1000)) + assert model.request_context.time_epoch == float(raw_event["requestContext"]["timeEpoch"]) + convert_time = int((model.request_context.time_epoch_as_datetime.timestamp() * 1000)) event_converted_time = round(int(raw_event["requestContext"]["timeEpoch"]) / 1000) assert convert_time == event_converted_time assert model.request_context.identity.source_vpc_arn == raw_event["requestContext"]["identity"]["sourceVpcArn"]