Skip to content

Commit

Permalink
feat(parser): support for CloudFormation Custom Resources (#2335)
Browse files Browse the repository at this point in the history
Co-authored-by: Leandro Damascena <leandro.damascena@gmail.com>
Co-authored-by: Ran Isenberg <ran.isenberg@ranthebuilder.cloud>
Co-authored-by: heitorlessa <lessa@amazon.co.uk>
  • Loading branch information
4 people authored Jun 9, 2023
1 parent 9a45a01 commit 29d6b94
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 21 deletions.
10 changes: 10 additions & 0 deletions aws_lambda_powertools/utilities/parser/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
RequestContextV2AuthorizerJwt,
RequestContextV2Http,
)
from .cloudformation_custom_resource import (
CloudFormationCustomResourceBaseModel,
CloudFormationCustomResourceCreateModel,
CloudFormationCustomResourceDeleteModel,
CloudFormationCustomResourceUpdateModel,
)
from .cloudwatch import (
CloudWatchLogsData,
CloudWatchLogsDecode,
Expand Down Expand Up @@ -147,4 +153,8 @@
"KafkaBaseEventModel",
"KinesisFirehoseSqsModel",
"KinesisFirehoseSqsRecord",
"CloudFormationCustomResourceUpdateModel",
"CloudFormationCustomResourceDeleteModel",
"CloudFormationCustomResourceCreateModel",
"CloudFormationCustomResourceBaseModel",
]
Original file line number Diff line number Diff line change
@@ -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")
45 changes: 24 additions & 21 deletions docs/utilities/parser.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 13 additions & 0 deletions tests/events/cloudformationCustomResourceCreate.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
13 changes: 13 additions & 0 deletions tests/events/cloudformationCustomResourceDelete.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
17 changes: 17 additions & 0 deletions tests/events/cloudformationCustomResourceUpdate.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
89 changes: 89 additions & 0 deletions tests/unit/parser/test_cloudformation_custom_resource.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 29d6b94

Please sign in to comment.