Skip to content

Commit 54e6cdd

Browse files
feat(event_source): add CloudFormationCustomResourceEvent data class. (#4342)
* feat: add CloudFormationCustomResourceEvent event source data class. * fix: remove CloudFormationRequestType type and other minor changes. * fix: fix copy-paste error when loading events in test_cloudformation_custom_resource. --------- Co-authored-by: Leandro Damascena <[email protected]>
1 parent 2892263 commit 54e6cdd

12 files changed

+157
-11
lines changed

aws_lambda_powertools/utilities/data_classes/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
)
1818
from .cloud_watch_custom_widget_event import CloudWatchDashboardCustomWidgetEvent
1919
from .cloud_watch_logs_event import CloudWatchLogsEvent
20+
from .cloudformation_custom_resource_event import CloudFormationCustomResourceEvent
2021
from .code_pipeline_job_event import CodePipelineJobEvent
2122
from .connect_contact_flow_event import ConnectContactFlowEvent
2223
from .dynamo_db_stream_event import DynamoDBStreamEvent
@@ -81,4 +82,5 @@
8182
"AWSConfigRuleEvent",
8283
"VPCLatticeEvent",
8384
"VPCLatticeEventV2",
85+
"CloudFormationCustomResourceEvent",
8486
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from typing import Any, Dict, Literal
2+
3+
from aws_lambda_powertools.utilities.data_classes.common import DictWrapper
4+
5+
6+
class CloudFormationCustomResourceEvent(DictWrapper):
7+
@property
8+
def request_type(self) -> Literal["Create", "Update", "Delete"]:
9+
return self["RequestType"]
10+
11+
@property
12+
def service_token(self) -> str:
13+
return self["ServiceToken"]
14+
15+
@property
16+
def response_url(self) -> str:
17+
return self["ResponseURL"]
18+
19+
@property
20+
def stack_id(self) -> str:
21+
return self["StackId"]
22+
23+
@property
24+
def request_id(self) -> str:
25+
return self["RequestId"]
26+
27+
@property
28+
def logical_resource_id(self) -> str:
29+
return self["LogicalResourceId"]
30+
31+
@property
32+
def physical_resource_id(self) -> str:
33+
return self.get("PhysicalResourceId") or ""
34+
35+
@property
36+
def resource_type(self) -> str:
37+
return self["ResourceType"]
38+
39+
@property
40+
def resource_properties(self) -> Dict[str, Any]:
41+
return self.get("ResourceProperties") or {}
42+
43+
@property
44+
def old_resource_properties(self) -> Dict[str, Any]:
45+
return self.get("OldResourceProperties") or {}

aws_lambda_powertools/utilities/parser/models/__init__.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@
6868
S3Model,
6969
S3RecordModel,
7070
)
71-
from .s3_batch_operation import S3BatchOperationJobModel, S3BatchOperationModel, S3BatchOperationTaskModel
71+
from .s3_batch_operation import (
72+
S3BatchOperationJobModel,
73+
S3BatchOperationModel,
74+
S3BatchOperationTaskModel,
75+
)
7276
from .s3_event_notification import (
7377
S3SqsEventNotificationModel,
7478
S3SqsEventNotificationRecordModel,

aws_lambda_powertools/utilities/parser/models/cloudformation_custom_resource.py

+2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ class CloudFormationCustomResourceCreateModel(CloudFormationCustomResourceBaseMo
2222

2323
class CloudFormationCustomResourceDeleteModel(CloudFormationCustomResourceBaseModel):
2424
request_type: Literal["Delete"] = Field(..., alias="RequestType")
25+
physical_resource_id: str = Field(..., alias="PhysicalResourceId")
2526

2627

2728
class CloudFormationCustomResourceUpdateModel(CloudFormationCustomResourceBaseModel):
2829
request_type: Literal["Update"] = Field(..., alias="RequestType")
30+
physical_resource_id: str = Field(..., alias="PhysicalResourceId")
2931
old_resource_properties: Union[Dict[str, Any], BaseModel, None] = Field(None, alias="OldResourceProperties")

docs/utilities/data_classes.md

+9
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ Log Data Event for Troubleshooting
8686
| [AppSync Resolver](#appsync-resolver) | `AppSyncResolverEvent` |
8787
| [AWS Config Rule](#aws-config-rule) | `AWSConfigRuleEvent` |
8888
| [Bedrock Agent](#bedrock-agent) | `BedrockAgent` |
89+
| [CloudFormation Custom Resource](#cloudformation-custom-resource) | `CloudFormationCustomResourceEvent` |
8990
| [CloudWatch Alarm State Change Action](#cloudwatch-alarm-state-change-action) | `CloudWatchAlarmEvent` |
9091
| [CloudWatch Dashboard Custom Widget](#cloudwatch-dashboard-custom-widget) | `CloudWatchDashboardCustomWidgetEvent` |
9192
| [CloudWatch Logs](#cloudwatch-logs) | `CloudWatchLogsEvent` |
@@ -495,6 +496,14 @@ In this example, we also use the new Logger `correlation_id` and built-in `corre
495496
--8<-- "examples/event_sources/src/bedrock_agent_event.py"
496497
```
497498

499+
### CloudFormation Custom Resource
500+
501+
=== "app.py"
502+
503+
```python hl_lines="11 13 15 17 19"
504+
--8<-- "examples/event_sources/src/cloudformation_custom_resource_handler.py"
505+
```
506+
498507
### CloudWatch Dashboard Custom Widget
499508

500509
=== "app.py"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from aws_lambda_powertools import Logger
2+
from aws_lambda_powertools.utilities.data_classes import (
3+
CloudFormationCustomResourceEvent,
4+
event_source,
5+
)
6+
from aws_lambda_powertools.utilities.typing import LambdaContext
7+
8+
logger = Logger()
9+
10+
11+
@event_source(data_class=CloudFormationCustomResourceEvent)
12+
def lambda_handler(event: CloudFormationCustomResourceEvent, context: LambdaContext):
13+
request_type = event.request_type
14+
15+
if request_type == "Create":
16+
return on_create(event)
17+
if request_type == "Update":
18+
return on_update(event)
19+
if request_type == "Delete":
20+
return on_delete(event)
21+
22+
23+
def on_create(event: CloudFormationCustomResourceEvent):
24+
props = event.resource_properties
25+
logger.info(f"Create new resource with props {props}.")
26+
27+
# Add your create code here ...
28+
physical_id = ...
29+
30+
return {"PhysicalResourceId": physical_id}
31+
32+
33+
def on_update(event: CloudFormationCustomResourceEvent):
34+
physical_id = event.physical_resource_id
35+
props = event.resource_properties
36+
logger.info(f"Update resource {physical_id} with props {props}.")
37+
# ...
38+
39+
40+
def on_delete(event: CloudFormationCustomResourceEvent):
41+
physical_id = event.physical_resource_id
42+
logger.info(f"Delete resource {physical_id}.")
43+
# ...

tests/events/cloudformationCustomResourceDelete.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
"StackId": "arn:aws:cloudformation:us-east-1:xxxx:stack/xxxx/271845b0-f2e8-11ed-90ac-0eeb25b8ae21",
66
"RequestId": "xxxxx-d2a0-4dfb-ab1f-xxxxxx",
77
"LogicalResourceId": "xxxxxxxxx",
8+
"PhysicalResourceId": "xxxxxxxxx",
89
"ResourceType": "Custom::MyType",
910
"ResourceProperties": {
1011
"ServiceToken": "arn:aws:lambda:us-east-1:xxxxx:function:xxxxx",
1112
"MyProps": "ss"
1213
}
13-
}
14+
}

tests/events/cloudformationCustomResourceUpdate.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"StackId": "arn:aws:cloudformation:us-east-1:xxxx:stack/xxxx/271845b0-f2e8-11ed-90ac-0eeb25b8ae21",
66
"RequestId": "xxxxx-d2a0-4dfb-ab1f-xxxxxx",
77
"LogicalResourceId": "xxxxxxxxx",
8+
"PhysicalResourceId": "xxxxxxxxx",
89
"ResourceType": "Custom::MyType",
910
"ResourceProperties": {
1011
"ServiceToken": "arn:aws:lambda:us-east-1:xxxxx:function:xxxxx",
@@ -14,4 +15,4 @@
1415
"ServiceToken": "arn:aws:lambda:us-east-1:xxxxx:function:xxxxx-xxxx-xxx",
1516
"MyProps": "old"
1617
}
17-
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import pytest
2+
3+
from aws_lambda_powertools.utilities.data_classes import (
4+
CloudFormationCustomResourceEvent,
5+
)
6+
from tests.functional.utils import load_event
7+
8+
9+
@pytest.mark.parametrize(
10+
"event_file",
11+
[
12+
"cloudformationCustomResourceCreate.json",
13+
"cloudformationCustomResourceUpdate.json",
14+
"cloudformationCustomResourceDelete.json",
15+
],
16+
)
17+
def test_cloudformation_custom_resource_event(event_file):
18+
raw_event = load_event(event_file)
19+
parsed_event = CloudFormationCustomResourceEvent(raw_event)
20+
21+
assert parsed_event.request_type == raw_event["RequestType"]
22+
assert parsed_event.service_token == raw_event["ServiceToken"]
23+
assert parsed_event.stack_id == raw_event["StackId"]
24+
assert parsed_event.request_id == raw_event["RequestId"]
25+
assert parsed_event.response_url == raw_event["ResponseURL"]
26+
assert parsed_event.logical_resource_id == raw_event["LogicalResourceId"]
27+
assert parsed_event.resource_type == raw_event["ResourceType"]
28+
assert parsed_event.resource_properties == raw_event.get("ResourceProperties", {})
29+
assert parsed_event.old_resource_properties == raw_event.get("OldResourceProperties", {})

tests/unit/data_classes/test_sqs_event.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,7 @@ def test_sqs_dlq_trigger_event():
6565
assert attributes.sequence_number is None
6666
assert attributes.message_group_id is None
6767
assert attributes.message_deduplication_id is None
68-
assert (
69-
attributes.dead_letter_queue_source_arn
70-
== raw_attributes["DeadLetterQueueSourceArn"]
71-
)
68+
assert attributes.dead_letter_queue_source_arn == raw_attributes["DeadLetterQueueSourceArn"]
7269

7370

7471
def test_decode_nested_s3_event():

tests/unit/parser/test_cloudformation_custom_resource.py

+16
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ def test_cloudformation_custom_resource_update_event():
6060
assert model.old_resource_properties == raw_event["OldResourceProperties"]
6161

6262

63+
def test_cloudformation_custom_resource_update_event_physical_id_missing():
64+
raw_event = load_event("cloudformationCustomResourceUpdate.json")
65+
del raw_event["PhysicalResourceId"]
66+
67+
with pytest.raises(ValidationError):
68+
CloudFormationCustomResourceUpdateModel(**raw_event)
69+
70+
6371
def test_cloudformation_custom_resource_update_event_invalid():
6472
raw_event = load_event("cloudformationCustomResourceUpdate.json")
6573
raw_event["OldResourceProperties"] = ["some_data"]
@@ -82,6 +90,14 @@ def test_cloudformation_custom_resource_delete_event():
8290
assert model.resource_properties == raw_event["ResourceProperties"]
8391

8492

93+
def test_cloudformation_custom_resource_delete_event_physical_id_missing():
94+
raw_event = load_event("cloudformationCustomResourceDelete.json")
95+
del raw_event["PhysicalResourceId"]
96+
97+
with pytest.raises(ValidationError):
98+
CloudFormationCustomResourceUpdateModel(**raw_event)
99+
100+
85101
def test_cloudformation_custom_resource_delete_event_invalid():
86102
raw_event = load_event("cloudformationCustomResourceDelete.json")
87103
raw_event["ResourceProperties"] = ["some_data"]

tests/unit/parser/test_sqs.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,4 @@ def test_sqs_dlq_trigger_event():
117117
convert_time = int(round(attributes.SentTimestamp.timestamp() * 1000))
118118
assert convert_time == int(raw_record["attributes"]["SentTimestamp"])
119119

120-
assert (
121-
attributes.DeadLetterQueueSourceArn
122-
== raw_record["attributes"]["DeadLetterQueueSourceArn"]
123-
)
120+
assert attributes.DeadLetterQueueSourceArn == raw_record["attributes"]["DeadLetterQueueSourceArn"]

0 commit comments

Comments
 (0)