Skip to content

Commit 314d0ec

Browse files
ran-isenbergleandrodamascenaRan Isenbergheitorlessa
authored andcommitted
feat(parser): support for CloudFormation Custom Resources (aws-powertools#2335)
Co-authored-by: Leandro Damascena <[email protected]> Co-authored-by: Ran Isenberg <[email protected]> Co-authored-by: heitorlessa <[email protected]>
1 parent 831e875 commit 314d0ec

File tree

7 files changed

+195
-21
lines changed

7 files changed

+195
-21
lines changed

aws_lambda_powertools/utilities/parser/models/__init__.py

+10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
RequestContextV2AuthorizerJwt,
1515
RequestContextV2Http,
1616
)
17+
from .cloudformation_custom_resource import (
18+
CloudFormationCustomResourceBaseModel,
19+
CloudFormationCustomResourceCreateModel,
20+
CloudFormationCustomResourceDeleteModel,
21+
CloudFormationCustomResourceUpdateModel,
22+
)
1723
from .cloudwatch import (
1824
CloudWatchLogsData,
1925
CloudWatchLogsDecode,
@@ -147,4 +153,8 @@
147153
"KafkaBaseEventModel",
148154
"KinesisFirehoseSqsModel",
149155
"KinesisFirehoseSqsRecord",
156+
"CloudFormationCustomResourceUpdateModel",
157+
"CloudFormationCustomResourceDeleteModel",
158+
"CloudFormationCustomResourceCreateModel",
159+
"CloudFormationCustomResourceBaseModel",
150160
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from typing import Any, Dict, Union
2+
3+
from pydantic import BaseModel, Field, HttpUrl
4+
5+
from aws_lambda_powertools.utilities.parser.types import Literal
6+
7+
8+
class CloudFormationCustomResourceBaseModel(BaseModel):
9+
request_type: str = Field(..., alias="RequestType")
10+
service_token: str = Field(..., alias="ServiceToken")
11+
response_url: HttpUrl = Field(..., alias="ResponseURL")
12+
stack_id: str = Field(..., alias="StackId")
13+
request_id: str = Field(..., alias="RequestId")
14+
logical_resource_id: str = Field(..., alias="LogicalResourceId")
15+
resource_type: str = Field(..., alias="ResourceType")
16+
resource_properties: Union[Dict[str, Any], BaseModel, None] = Field(None, alias="ResourceProperties")
17+
18+
19+
class CloudFormationCustomResourceCreateModel(CloudFormationCustomResourceBaseModel):
20+
request_type: Literal["Create"] = Field(..., alias="RequestType")
21+
22+
23+
class CloudFormationCustomResourceDeleteModel(CloudFormationCustomResourceBaseModel):
24+
request_type: Literal["Delete"] = Field(..., alias="RequestType")
25+
26+
27+
class CloudFormationCustomResourceUpdateModel(CloudFormationCustomResourceBaseModel):
28+
request_type: Literal["Update"] = Field(..., alias="RequestType")
29+
old_resource_properties: Union[Dict[str, Any], BaseModel, None] = Field(None, alias="OldResourceProperties")

docs/utilities/parser.md

+24-21
Original file line numberDiff line numberDiff line change
@@ -156,27 +156,30 @@ def my_function():
156156

157157
Parser comes with the following built-in models:
158158

159-
| Model name | Description |
160-
| --------------------------------------- | ------------------------------------------------------------------------------------- |
161-
| **AlbModel** | Lambda Event Source payload for Amazon Application Load Balancer |
162-
| **APIGatewayProxyEventModel** | Lambda Event Source payload for Amazon API Gateway |
163-
| **APIGatewayProxyEventV2Model** | Lambda Event Source payload for Amazon API Gateway v2 payload |
164-
| **CloudwatchLogsModel** | Lambda Event Source payload for Amazon CloudWatch Logs |
165-
| **DynamoDBStreamModel** | Lambda Event Source payload for Amazon DynamoDB Streams |
166-
| **EventBridgeModel** | Lambda Event Source payload for Amazon EventBridge |
167-
| **KafkaMskEventModel** | Lambda Event Source payload for AWS MSK payload |
168-
| **KafkaSelfManagedEventModel** | Lambda Event Source payload for self managed Kafka payload |
169-
| **KinesisDataStreamModel** | Lambda Event Source payload for Amazon Kinesis Data Streams |
170-
| **KinesisFirehoseModel** | Lambda Event Source payload for Amazon Kinesis Firehose |
171-
| **KinesisFirehoseSqsModel** | Lambda Event Source payload for SQS messages wrapped in Kinesis Firehose records |
172-
| **LambdaFunctionUrlModel** | Lambda Event Source payload for Lambda Function URL payload |
173-
| **S3EventNotificationEventBridgeModel** | Lambda Event Source payload for Amazon S3 Event Notification to EventBridge. |
174-
| **S3Model** | Lambda Event Source payload for Amazon S3 |
175-
| **S3ObjectLambdaEvent** | Lambda Event Source payload for Amazon S3 Object Lambda |
176-
| **S3SqsEventNotificationModel** | Lambda Event Source payload for S3 event notifications wrapped in SQS event (S3->SQS) |
177-
| **SesModel** | Lambda Event Source payload for Amazon Simple Email Service |
178-
| **SnsModel** | Lambda Event Source payload for Amazon Simple Notification Service |
179-
| **SqsModel** | Lambda Event Source payload for Amazon SQS |
159+
| Model name | Description |
160+
| ------------------------------------------- | ------------------------------------------------------------------------------------- |
161+
| **AlbModel** | Lambda Event Source payload for Amazon Application Load Balancer |
162+
| **APIGatewayProxyEventModel** | Lambda Event Source payload for Amazon API Gateway |
163+
| **APIGatewayProxyEventV2Model** | Lambda Event Source payload for Amazon API Gateway v2 payload |
164+
| **CloudFormationCustomResourceCreateModel** | Lambda Event Source payload for AWS CloudFormation `CREATE` operation |
165+
| **CloudFormationCustomResourceUpdateModel** | Lambda Event Source payload for AWS CloudFormation `UPDATE` operation |
166+
| **CloudFormationCustomResourceDeleteModel** | Lambda Event Source payload for AWS CloudFormation `DELETE` operation |
167+
| **CloudwatchLogsModel** | Lambda Event Source payload for Amazon CloudWatch Logs |
168+
| **DynamoDBStreamModel** | Lambda Event Source payload for Amazon DynamoDB Streams |
169+
| **EventBridgeModel** | Lambda Event Source payload for Amazon EventBridge |
170+
| **KafkaMskEventModel** | Lambda Event Source payload for AWS MSK payload |
171+
| **KafkaSelfManagedEventModel** | Lambda Event Source payload for self managed Kafka payload |
172+
| **KinesisDataStreamModel** | Lambda Event Source payload for Amazon Kinesis Data Streams |
173+
| **KinesisFirehoseModel** | Lambda Event Source payload for Amazon Kinesis Firehose |
174+
| **KinesisFirehoseSqsModel** | Lambda Event Source payload for SQS messages wrapped in Kinesis Firehose records |
175+
| **LambdaFunctionUrlModel** | Lambda Event Source payload for Lambda Function URL payload |
176+
| **S3EventNotificationEventBridgeModel** | Lambda Event Source payload for Amazon S3 Event Notification to EventBridge. |
177+
| **S3Model** | Lambda Event Source payload for Amazon S3 |
178+
| **S3ObjectLambdaEvent** | Lambda Event Source payload for Amazon S3 Object Lambda |
179+
| **S3SqsEventNotificationModel** | Lambda Event Source payload for S3 event notifications wrapped in SQS event (S3->SQS) |
180+
| **SesModel** | Lambda Event Source payload for Amazon Simple Email Service |
181+
| **SnsModel** | Lambda Event Source payload for Amazon Simple Notification Service |
182+
| **SqsModel** | Lambda Event Source payload for Amazon SQS |
180183

181184
#### Extending built-in models
182185

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"RequestType": "Create",
3+
"ServiceToken": "arn:aws:lambda:us-east-1:xxx:function:xxxx-CrbuiltinfunctionidProvi-2vKAalSppmKe",
4+
"ResponseURL": "https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/7F%7Cb1f50fdfc25f3b",
5+
"StackId": "arn:aws:cloudformation:us-east-1:xxxx:stack/xxxx/271845b0-f2e8-11ed-90ac-0eeb25b8ae21",
6+
"RequestId": "xxxxx-d2a0-4dfb-ab1f-xxxxxx",
7+
"LogicalResourceId": "xxxxxxxxx",
8+
"ResourceType": "Custom::MyType",
9+
"ResourceProperties": {
10+
"ServiceToken": "arn:aws:lambda:us-east-1:xxxxx:function:xxxxx",
11+
"MyProps": "ss"
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"RequestType": "Delete",
3+
"ServiceToken": "arn:aws:lambda:us-east-1:xxx:function:xxxx-CrbuiltinfunctionidProvi-2vKAalSppmKe",
4+
"ResponseURL": "https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/7F%7Cb1f50fdfc25f3b",
5+
"StackId": "arn:aws:cloudformation:us-east-1:xxxx:stack/xxxx/271845b0-f2e8-11ed-90ac-0eeb25b8ae21",
6+
"RequestId": "xxxxx-d2a0-4dfb-ab1f-xxxxxx",
7+
"LogicalResourceId": "xxxxxxxxx",
8+
"ResourceType": "Custom::MyType",
9+
"ResourceProperties": {
10+
"ServiceToken": "arn:aws:lambda:us-east-1:xxxxx:function:xxxxx",
11+
"MyProps": "ss"
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"RequestType": "Update",
3+
"ServiceToken": "arn:aws:lambda:us-east-1:xxx:function:xxxx-CrbuiltinfunctionidProvi-2vKAalSppmKe",
4+
"ResponseURL": "https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/7F%7Cb1f50fdfc25f3b",
5+
"StackId": "arn:aws:cloudformation:us-east-1:xxxx:stack/xxxx/271845b0-f2e8-11ed-90ac-0eeb25b8ae21",
6+
"RequestId": "xxxxx-d2a0-4dfb-ab1f-xxxxxx",
7+
"LogicalResourceId": "xxxxxxxxx",
8+
"ResourceType": "Custom::MyType",
9+
"ResourceProperties": {
10+
"ServiceToken": "arn:aws:lambda:us-east-1:xxxxx:function:xxxxx",
11+
"MyProps": "new"
12+
},
13+
"OldResourceProperties": {
14+
"ServiceToken": "arn:aws:lambda:us-east-1:xxxxx:function:xxxxx-xxxx-xxx",
15+
"MyProps": "old"
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import pytest
2+
from pydantic import BaseModel, Field
3+
4+
from aws_lambda_powertools.utilities.parser import ValidationError
5+
from aws_lambda_powertools.utilities.parser.models import (
6+
CloudFormationCustomResourceCreateModel,
7+
CloudFormationCustomResourceDeleteModel,
8+
CloudFormationCustomResourceUpdateModel,
9+
)
10+
from tests.functional.utils import load_event
11+
12+
13+
def test_cloudformation_custom_resource_create_event():
14+
raw_event = load_event("cloudformationCustomResourceCreate.json")
15+
model = CloudFormationCustomResourceCreateModel(**raw_event)
16+
17+
assert model.request_type == raw_event["RequestType"]
18+
assert model.request_id == raw_event["RequestId"]
19+
assert model.service_token == raw_event["ServiceToken"]
20+
assert str(model.response_url) == raw_event["ResponseURL"]
21+
assert model.stack_id == raw_event["StackId"]
22+
assert model.logical_resource_id == raw_event["LogicalResourceId"]
23+
assert model.resource_type == raw_event["ResourceType"]
24+
assert model.resource_properties == raw_event["ResourceProperties"]
25+
26+
27+
def test_cloudformation_custom_resource_create_event_custom_model():
28+
class MyModel(BaseModel):
29+
MyProps: str
30+
31+
class MyCustomResource(CloudFormationCustomResourceCreateModel):
32+
resource_properties: MyModel = Field(..., alias="ResourceProperties")
33+
34+
raw_event = load_event("cloudformationCustomResourceCreate.json")
35+
model = MyCustomResource(**raw_event)
36+
37+
assert model.resource_properties.MyProps == raw_event["ResourceProperties"].get("MyProps")
38+
39+
40+
def test_cloudformation_custom_resource_create_event_invalid():
41+
raw_event = load_event("cloudformationCustomResourceCreate.json")
42+
raw_event["ResourceProperties"] = ["some_data"]
43+
44+
with pytest.raises(ValidationError):
45+
CloudFormationCustomResourceCreateModel(**raw_event)
46+
47+
48+
def test_cloudformation_custom_resource_update_event():
49+
raw_event = load_event("cloudformationCustomResourceUpdate.json")
50+
model = CloudFormationCustomResourceUpdateModel(**raw_event)
51+
52+
assert model.request_type == raw_event["RequestType"]
53+
assert model.request_id == raw_event["RequestId"]
54+
assert model.service_token == raw_event["ServiceToken"]
55+
assert str(model.response_url) == raw_event["ResponseURL"]
56+
assert model.stack_id == raw_event["StackId"]
57+
assert model.logical_resource_id == raw_event["LogicalResourceId"]
58+
assert model.resource_type == raw_event["ResourceType"]
59+
assert model.resource_properties == raw_event["ResourceProperties"]
60+
assert model.old_resource_properties == raw_event["OldResourceProperties"]
61+
62+
63+
def test_cloudformation_custom_resource_update_event_invalid():
64+
raw_event = load_event("cloudformationCustomResourceUpdate.json")
65+
raw_event["OldResourceProperties"] = ["some_data"]
66+
67+
with pytest.raises(ValidationError):
68+
CloudFormationCustomResourceUpdateModel(**raw_event)
69+
70+
71+
def test_cloudformation_custom_resource_delete_event():
72+
raw_event = load_event("cloudformationCustomResourceDelete.json")
73+
model = CloudFormationCustomResourceDeleteModel(**raw_event)
74+
75+
assert model.request_type == raw_event["RequestType"]
76+
assert model.request_id == raw_event["RequestId"]
77+
assert model.service_token == raw_event["ServiceToken"]
78+
assert str(model.response_url) == raw_event["ResponseURL"]
79+
assert model.stack_id == raw_event["StackId"]
80+
assert model.logical_resource_id == raw_event["LogicalResourceId"]
81+
assert model.resource_type == raw_event["ResourceType"]
82+
assert model.resource_properties == raw_event["ResourceProperties"]
83+
84+
85+
def test_cloudformation_custom_resource_delete_event_invalid():
86+
raw_event = load_event("cloudformationCustomResourceDelete.json")
87+
raw_event["ResourceProperties"] = ["some_data"]
88+
with pytest.raises(ValidationError):
89+
CloudFormationCustomResourceDeleteModel(**raw_event)

0 commit comments

Comments
 (0)