Skip to content

Commit d90f57b

Browse files
committed
feat: add CloudFormationCustomResourceEvent event source data class.
1 parent 51261b2 commit d90f57b

12 files changed

+165
-12
lines changed
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# logger.powertools_handler is set with Powertools Logger handler; useful when there are many handlers
22
LOGGER_ATTRIBUTE_POWERTOOLS_HANDLER = "powertools_handler"
3-
# logger.init attribute is set when Logger has been configured
3+
# logger.init attribute is set when Logger has been configured
44
LOGGER_ATTRIBUTE_PRECONFIGURED = "init"
55
LOGGER_ATTRIBUTE_HANDLER = "logger_handler"

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,52 @@
1+
from enum import Enum
2+
from typing import Any, Dict, Optional
3+
4+
from aws_lambda_powertools.utilities.data_classes.common import DictWrapper
5+
6+
7+
class CloudFormationRequestType(Enum):
8+
CREATE = "Create"
9+
UPDATE = "Update"
10+
DELETE = "Delete"
11+
12+
13+
class CloudFormationCustomResourceEvent(DictWrapper):
14+
@property
15+
def request_type(self) -> CloudFormationRequestType:
16+
return CloudFormationRequestType(self["RequestType"])
17+
18+
@property
19+
def service_token(self) -> str:
20+
return self["ServiceToken"]
21+
22+
@property
23+
def response_url(self) -> str:
24+
return self["ResponseUrl"]
25+
26+
@property
27+
def stack_id(self) -> str:
28+
return self["StackId"]
29+
30+
@property
31+
def request_id(self) -> str:
32+
return self["RequestId"]
33+
34+
@property
35+
def logical_resource_id(self) -> str:
36+
return self["LogicalResourceId"]
37+
38+
@property
39+
def physical_resource_id(self) -> Optional[str]:
40+
return self.get("PhysicalResourceId")
41+
42+
@property
43+
def resource_type(self) -> str:
44+
return self["ResourceType"]
45+
46+
@property
47+
def resource_properties(self) -> Optional[Dict[str, Any]]:
48+
return self.get("ResourceProperties")
49+
50+
@property
51+
def old_resource_properties(self) -> Optional[Dict[str, Any]]:
52+
return self.get("OldResourceProperties")

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

+49
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,54 @@ 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
504+
from aws_lambda_powertools.utilities.data_classes import event_source, CloudFormationCustomResourceEvent
505+
from aws_lambda_powertools.utilities.data_classes.cloudformation_custom_resource_event import CloudFormationRequestType
506+
from aws_lambda_powertools.utilities.typing import LambdaContext
507+
from aws_lambda_powertools import Logger
508+
509+
logger = Logger()
510+
511+
512+
@event_source(data_class=CloudFormationCustomResourceEvent)
513+
def lambda_handler(event: CloudFormationCustomResourceEvent, context: LambdaContext):
514+
request_type = event.request_type
515+
516+
if request_type == CloudFormationRequestType.CREATE:
517+
return on_create(event)
518+
if request_type == CloudFormationRequestType.UPDATE:
519+
return on_update(event)
520+
if request_type == CloudFormationRequestType.DELETE:
521+
return on_delete(event)
522+
523+
524+
def on_create(event: CloudFormationCustomResourceEvent):
525+
props = event.resource_properties
526+
logger.info(f"Create new resource with props {props}.")
527+
528+
# Add your create code here ...
529+
physical_id = ...
530+
531+
return {"PhysicalResourceId": physical_id}
532+
533+
534+
def on_update(event: CloudFormationCustomResourceEvent):
535+
physical_id = event.physical_resource_id
536+
props = event.resource_properties
537+
logger.info(f"Update resource {physical_id} with props {props}.")
538+
# ...
539+
540+
541+
def on_delete(event: CloudFormationCustomResourceEvent):
542+
physical_id = event.physical_resource_id
543+
logger.info(f"Delete resource {physical_id}.")
544+
# ...
545+
```
546+
498547
### CloudWatch Dashboard Custom Widget
499548

500549
=== "app.py"

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,32 @@
1+
import pytest
2+
3+
from aws_lambda_powertools.utilities.data_classes import (
4+
CloudFormationCustomResourceEvent,
5+
)
6+
from aws_lambda_powertools.utilities.data_classes.cloudformation_custom_resource_event import (
7+
CloudFormationRequestType,
8+
)
9+
from tests.functional.utils import load_event
10+
11+
12+
@pytest.mark.parametrize(
13+
"event_file",
14+
[
15+
"cloudformationCustomResourceCreate.json",
16+
"cloudformationCustomResourceUpdate.json",
17+
"cloudformationCustomResourceDelete.json",
18+
],
19+
)
20+
def test_cloudformation_custom_resource_event(event_file):
21+
raw_event = load_event(event_file)
22+
parsed_event = CloudFormationCustomResourceEvent(raw_event)
23+
24+
assert parsed_event.request_type == CloudFormationRequestType(raw_event["RequestType"])
25+
assert isinstance(parsed_event.request_type, CloudFormationRequestType)
26+
assert parsed_event.service_token == raw_event["ServiceToken"]
27+
assert parsed_event.stack_id == raw_event["StackId"]
28+
assert parsed_event.request_id == raw_event["RequestId"]
29+
assert parsed_event.logical_resource_id == raw_event["LogicalResourceId"]
30+
assert parsed_event.resource_type == raw_event["ResourceType"]
31+
assert parsed_event.resource_properties == raw_event.get("ResourceProperties")
32+
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("cloudformationCustomResourceUpdate.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)