diff --git a/aws_lambda_powertools/utilities/parser/models/__init__.py b/aws_lambda_powertools/utilities/parser/models/__init__.py index c2385b7bf14..952280a519c 100644 --- a/aws_lambda_powertools/utilities/parser/models/__init__.py +++ b/aws_lambda_powertools/utilities/parser/models/__init__.py @@ -14,6 +14,12 @@ RequestContextV2AuthorizerJwt, RequestContextV2Http, ) +from .cloudformation_custom_resource import ( + CloudFormationCustomResourceBaseModel, + CloudFormationCustomResourceCreateModel, + CloudFormationCustomResourceDeleteModel, + CloudFormationCustomResourceUpdateModel, +) from .cloudwatch import ( CloudWatchLogsData, CloudWatchLogsDecode, @@ -147,4 +153,8 @@ "KafkaBaseEventModel", "KinesisFirehoseSqsModel", "KinesisFirehoseSqsRecord", + "CloudFormationCustomResourceUpdateModel", + "CloudFormationCustomResourceDeleteModel", + "CloudFormationCustomResourceCreateModel", + "CloudFormationCustomResourceBaseModel", ] diff --git a/aws_lambda_powertools/utilities/parser/models/cloudformation_custom_resource.py b/aws_lambda_powertools/utilities/parser/models/cloudformation_custom_resource.py new file mode 100644 index 00000000000..479ff53fb45 --- /dev/null +++ b/aws_lambda_powertools/utilities/parser/models/cloudformation_custom_resource.py @@ -0,0 +1,29 @@ +from typing import Any, Dict, Union + +from pydantic import BaseModel, Field, HttpUrl + +from aws_lambda_powertools.utilities.parser.types import Literal + + +class CloudFormationCustomResourceBaseModel(BaseModel): + request_type: str = Field(..., alias="RequestType") + service_token: str = Field(..., alias="ServiceToken") + response_url: HttpUrl = Field(..., alias="ResponseURL") + stack_id: str = Field(..., alias="StackId") + request_id: str = Field(..., alias="RequestId") + logical_resource_id: str = Field(..., alias="LogicalResourceId") + resource_type: str = Field(..., alias="ResourceType") + resource_properties: Union[Dict[str, Any], BaseModel, None] = Field(None, alias="ResourceProperties") + + +class CloudFormationCustomResourceCreateModel(CloudFormationCustomResourceBaseModel): + request_type: Literal["Create"] = Field(..., alias="RequestType") + + +class CloudFormationCustomResourceDeleteModel(CloudFormationCustomResourceBaseModel): + request_type: Literal["Delete"] = Field(..., alias="RequestType") + + +class CloudFormationCustomResourceUpdateModel(CloudFormationCustomResourceBaseModel): + request_type: Literal["Update"] = Field(..., alias="RequestType") + old_resource_properties: Union[Dict[str, Any], BaseModel, None] = Field(None, alias="OldResourceProperties") diff --git a/docs/utilities/parser.md b/docs/utilities/parser.md index b0852a0129f..0e25f9441a4 100644 --- a/docs/utilities/parser.md +++ b/docs/utilities/parser.md @@ -156,27 +156,30 @@ def my_function(): Parser comes with the following built-in models: -| Model name | Description | -| --------------------------------------- | ------------------------------------------------------------------------------------- | -| **AlbModel** | Lambda Event Source payload for Amazon Application Load Balancer | -| **APIGatewayProxyEventModel** | Lambda Event Source payload for Amazon API Gateway | -| **APIGatewayProxyEventV2Model** | Lambda Event Source payload for Amazon API Gateway v2 payload | -| **CloudwatchLogsModel** | Lambda Event Source payload for Amazon CloudWatch Logs | -| **DynamoDBStreamModel** | Lambda Event Source payload for Amazon DynamoDB Streams | -| **EventBridgeModel** | Lambda Event Source payload for Amazon EventBridge | -| **KafkaMskEventModel** | Lambda Event Source payload for AWS MSK payload | -| **KafkaSelfManagedEventModel** | Lambda Event Source payload for self managed Kafka payload | -| **KinesisDataStreamModel** | Lambda Event Source payload for Amazon Kinesis Data Streams | -| **KinesisFirehoseModel** | Lambda Event Source payload for Amazon Kinesis Firehose | -| **KinesisFirehoseSqsModel** | Lambda Event Source payload for SQS messages wrapped in Kinesis Firehose records | -| **LambdaFunctionUrlModel** | Lambda Event Source payload for Lambda Function URL payload | -| **S3EventNotificationEventBridgeModel** | Lambda Event Source payload for Amazon S3 Event Notification to EventBridge. | -| **S3Model** | Lambda Event Source payload for Amazon S3 | -| **S3ObjectLambdaEvent** | Lambda Event Source payload for Amazon S3 Object Lambda | -| **S3SqsEventNotificationModel** | Lambda Event Source payload for S3 event notifications wrapped in SQS event (S3->SQS) | -| **SesModel** | Lambda Event Source payload for Amazon Simple Email Service | -| **SnsModel** | Lambda Event Source payload for Amazon Simple Notification Service | -| **SqsModel** | Lambda Event Source payload for Amazon SQS | +| Model name | Description | +| ------------------------------------------- | ------------------------------------------------------------------------------------- | +| **AlbModel** | Lambda Event Source payload for Amazon Application Load Balancer | +| **APIGatewayProxyEventModel** | Lambda Event Source payload for Amazon API Gateway | +| **APIGatewayProxyEventV2Model** | Lambda Event Source payload for Amazon API Gateway v2 payload | +| **CloudFormationCustomResourceCreateModel** | Lambda Event Source payload for AWS CloudFormation `CREATE` operation | +| **CloudFormationCustomResourceUpdateModel** | Lambda Event Source payload for AWS CloudFormation `UPDATE` operation | +| **CloudFormationCustomResourceDeleteModel** | Lambda Event Source payload for AWS CloudFormation `DELETE` operation | +| **CloudwatchLogsModel** | Lambda Event Source payload for Amazon CloudWatch Logs | +| **DynamoDBStreamModel** | Lambda Event Source payload for Amazon DynamoDB Streams | +| **EventBridgeModel** | Lambda Event Source payload for Amazon EventBridge | +| **KafkaMskEventModel** | Lambda Event Source payload for AWS MSK payload | +| **KafkaSelfManagedEventModel** | Lambda Event Source payload for self managed Kafka payload | +| **KinesisDataStreamModel** | Lambda Event Source payload for Amazon Kinesis Data Streams | +| **KinesisFirehoseModel** | Lambda Event Source payload for Amazon Kinesis Firehose | +| **KinesisFirehoseSqsModel** | Lambda Event Source payload for SQS messages wrapped in Kinesis Firehose records | +| **LambdaFunctionUrlModel** | Lambda Event Source payload for Lambda Function URL payload | +| **S3EventNotificationEventBridgeModel** | Lambda Event Source payload for Amazon S3 Event Notification to EventBridge. | +| **S3Model** | Lambda Event Source payload for Amazon S3 | +| **S3ObjectLambdaEvent** | Lambda Event Source payload for Amazon S3 Object Lambda | +| **S3SqsEventNotificationModel** | Lambda Event Source payload for S3 event notifications wrapped in SQS event (S3->SQS) | +| **SesModel** | Lambda Event Source payload for Amazon Simple Email Service | +| **SnsModel** | Lambda Event Source payload for Amazon Simple Notification Service | +| **SqsModel** | Lambda Event Source payload for Amazon SQS | #### Extending built-in models diff --git a/tests/events/cloudformationCustomResourceCreate.json b/tests/events/cloudformationCustomResourceCreate.json new file mode 100644 index 00000000000..5c32d8c7aa1 --- /dev/null +++ b/tests/events/cloudformationCustomResourceCreate.json @@ -0,0 +1,13 @@ +{ + "RequestType": "Create", + "ServiceToken": "arn:aws:lambda:us-east-1:xxx:function:xxxx-CrbuiltinfunctionidProvi-2vKAalSppmKe", + "ResponseURL": "https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/7F%7Cb1f50fdfc25f3b", + "StackId": "arn:aws:cloudformation:us-east-1:xxxx:stack/xxxx/271845b0-f2e8-11ed-90ac-0eeb25b8ae21", + "RequestId": "xxxxx-d2a0-4dfb-ab1f-xxxxxx", + "LogicalResourceId": "xxxxxxxxx", + "ResourceType": "Custom::MyType", + "ResourceProperties": { + "ServiceToken": "arn:aws:lambda:us-east-1:xxxxx:function:xxxxx", + "MyProps": "ss" + } +} \ No newline at end of file diff --git a/tests/events/cloudformationCustomResourceDelete.json b/tests/events/cloudformationCustomResourceDelete.json new file mode 100644 index 00000000000..f26738133db --- /dev/null +++ b/tests/events/cloudformationCustomResourceDelete.json @@ -0,0 +1,13 @@ +{ + "RequestType": "Delete", + "ServiceToken": "arn:aws:lambda:us-east-1:xxx:function:xxxx-CrbuiltinfunctionidProvi-2vKAalSppmKe", + "ResponseURL": "https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/7F%7Cb1f50fdfc25f3b", + "StackId": "arn:aws:cloudformation:us-east-1:xxxx:stack/xxxx/271845b0-f2e8-11ed-90ac-0eeb25b8ae21", + "RequestId": "xxxxx-d2a0-4dfb-ab1f-xxxxxx", + "LogicalResourceId": "xxxxxxxxx", + "ResourceType": "Custom::MyType", + "ResourceProperties": { + "ServiceToken": "arn:aws:lambda:us-east-1:xxxxx:function:xxxxx", + "MyProps": "ss" + } +} \ No newline at end of file diff --git a/tests/events/cloudformationCustomResourceUpdate.json b/tests/events/cloudformationCustomResourceUpdate.json new file mode 100644 index 00000000000..52257463455 --- /dev/null +++ b/tests/events/cloudformationCustomResourceUpdate.json @@ -0,0 +1,17 @@ +{ + "RequestType": "Update", + "ServiceToken": "arn:aws:lambda:us-east-1:xxx:function:xxxx-CrbuiltinfunctionidProvi-2vKAalSppmKe", + "ResponseURL": "https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/7F%7Cb1f50fdfc25f3b", + "StackId": "arn:aws:cloudformation:us-east-1:xxxx:stack/xxxx/271845b0-f2e8-11ed-90ac-0eeb25b8ae21", + "RequestId": "xxxxx-d2a0-4dfb-ab1f-xxxxxx", + "LogicalResourceId": "xxxxxxxxx", + "ResourceType": "Custom::MyType", + "ResourceProperties": { + "ServiceToken": "arn:aws:lambda:us-east-1:xxxxx:function:xxxxx", + "MyProps": "new" + }, + "OldResourceProperties": { + "ServiceToken": "arn:aws:lambda:us-east-1:xxxxx:function:xxxxx-xxxx-xxx", + "MyProps": "old" + } +} \ No newline at end of file diff --git a/tests/unit/parser/test_cloudformation_custom_resource.py b/tests/unit/parser/test_cloudformation_custom_resource.py new file mode 100644 index 00000000000..b5646c3f36a --- /dev/null +++ b/tests/unit/parser/test_cloudformation_custom_resource.py @@ -0,0 +1,89 @@ +import pytest +from pydantic import BaseModel, Field + +from aws_lambda_powertools.utilities.parser import ValidationError +from aws_lambda_powertools.utilities.parser.models import ( + CloudFormationCustomResourceCreateModel, + CloudFormationCustomResourceDeleteModel, + CloudFormationCustomResourceUpdateModel, +) +from tests.functional.utils import load_event + + +def test_cloudformation_custom_resource_create_event(): + raw_event = load_event("cloudformationCustomResourceCreate.json") + model = CloudFormationCustomResourceCreateModel(**raw_event) + + assert model.request_type == raw_event["RequestType"] + assert model.request_id == raw_event["RequestId"] + assert model.service_token == raw_event["ServiceToken"] + assert str(model.response_url) == raw_event["ResponseURL"] + assert model.stack_id == raw_event["StackId"] + assert model.logical_resource_id == raw_event["LogicalResourceId"] + assert model.resource_type == raw_event["ResourceType"] + assert model.resource_properties == raw_event["ResourceProperties"] + + +def test_cloudformation_custom_resource_create_event_custom_model(): + class MyModel(BaseModel): + MyProps: str + + class MyCustomResource(CloudFormationCustomResourceCreateModel): + resource_properties: MyModel = Field(..., alias="ResourceProperties") + + raw_event = load_event("cloudformationCustomResourceCreate.json") + model = MyCustomResource(**raw_event) + + assert model.resource_properties.MyProps == raw_event["ResourceProperties"].get("MyProps") + + +def test_cloudformation_custom_resource_create_event_invalid(): + raw_event = load_event("cloudformationCustomResourceCreate.json") + raw_event["ResourceProperties"] = ["some_data"] + + with pytest.raises(ValidationError): + CloudFormationCustomResourceCreateModel(**raw_event) + + +def test_cloudformation_custom_resource_update_event(): + raw_event = load_event("cloudformationCustomResourceUpdate.json") + model = CloudFormationCustomResourceUpdateModel(**raw_event) + + assert model.request_type == raw_event["RequestType"] + assert model.request_id == raw_event["RequestId"] + assert model.service_token == raw_event["ServiceToken"] + assert str(model.response_url) == raw_event["ResponseURL"] + assert model.stack_id == raw_event["StackId"] + assert model.logical_resource_id == raw_event["LogicalResourceId"] + assert model.resource_type == raw_event["ResourceType"] + assert model.resource_properties == raw_event["ResourceProperties"] + assert model.old_resource_properties == raw_event["OldResourceProperties"] + + +def test_cloudformation_custom_resource_update_event_invalid(): + raw_event = load_event("cloudformationCustomResourceUpdate.json") + raw_event["OldResourceProperties"] = ["some_data"] + + with pytest.raises(ValidationError): + CloudFormationCustomResourceUpdateModel(**raw_event) + + +def test_cloudformation_custom_resource_delete_event(): + raw_event = load_event("cloudformationCustomResourceDelete.json") + model = CloudFormationCustomResourceDeleteModel(**raw_event) + + assert model.request_type == raw_event["RequestType"] + assert model.request_id == raw_event["RequestId"] + assert model.service_token == raw_event["ServiceToken"] + assert str(model.response_url) == raw_event["ResponseURL"] + assert model.stack_id == raw_event["StackId"] + assert model.logical_resource_id == raw_event["LogicalResourceId"] + assert model.resource_type == raw_event["ResourceType"] + assert model.resource_properties == raw_event["ResourceProperties"] + + +def test_cloudformation_custom_resource_delete_event_invalid(): + raw_event = load_event("cloudformationCustomResourceDelete.json") + raw_event["ResourceProperties"] = ["some_data"] + with pytest.raises(ValidationError): + CloudFormationCustomResourceDeleteModel(**raw_event)