Skip to content

Commit 16fa852

Browse files
authored
Support DLQ, RetryPolicy properties for EventBridgeRule,Schedule event sources (#1842)
* Add DeadLetterConfig,RetryPolicy properties for EventBridgeRule,Schedule event sources * Minor fix,rename function argument * Update test class name * Combine dlq extraction/generation into the utility class * Remove unused import
1 parent a79e4d6 commit 16fa852

File tree

60 files changed

+3283
-12
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+3283
-12
lines changed

docs/cloudformation_compatibility.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ CloudWatchEvent (superseded by EventBridgeRule, see below)
169169
Pattern All
170170
Input All
171171
InputPath All
172+
DeadLetterConfig All
173+
RetryPolicy All
172174
======================== ================================== ========================
173175

174176
EventBridgeRule
@@ -179,6 +181,8 @@ EventBridgeRule
179181
Pattern All
180182
Input All
181183
InputPath All
184+
DeadLetterConfig All
185+
RetryPolicy All
182186
======================== ================================== ========================
183187

184188
IotRule

docs/internals/generated_resources.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,8 @@ Example:
455455
Type: Schedule
456456
Properties:
457457
Input: rate(5 minutes)
458+
DeadLetterConfig:
459+
Type: SQS
458460
...
459461
460462
Additional generated resources:
@@ -464,6 +466,8 @@ CloudFormation Resource Type Logical ID
464466
================================== ================================
465467
AWS::Lambda::Permission MyFunction\ **MyTimer**\ Permission
466468
AWS::Events::Rule MyFunction\ **MyTimer**
469+
AWS::SQS::Queue MyFunction\ **MyTimer**\ Queue
470+
AWS::SQS::QueuePolicy MyFunction\ **MyTimer**\ QueuePolicy
467471
================================== ================================
468472

469473
CloudWatchEvent (superseded by EventBridgeRule, see below)
@@ -523,6 +527,11 @@ Example:
523527
detail:
524528
state:
525529
- terminated
530+
DeadLetterConfig:
531+
Type: SQS
532+
RetryPolicy:
533+
MaximumEventAgeInSeconds: 600
534+
MaximumRetryAttempts:3
526535
...
527536
528537
Additional generated resources:
@@ -532,6 +541,8 @@ CloudFormation Resource Type Logical ID
532541
================================== ================================
533542
AWS::Lambda::Permission MyFunction\ **OnTerminate**\ Permission
534543
AWS::Events::Rule MyFunction\ **OnTerminate**
544+
AWS::SQS::Queue MyFunction\ **OnTerminate**\ Queue
545+
AWS::SQS::QueuePolicy MyFunction\ **OnTerminate**\ QueuePolicy
535546
================================== ================================
536547

537548
AWS::Serverless::Api
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from samtranslator.model.sqs import SQSQueue, SQSQueuePolicy, SQSQueuePolicies
2+
from samtranslator.model.exceptions import InvalidEventException
3+
4+
5+
class EventBridgeRuleUtils:
6+
@staticmethod
7+
def create_dead_letter_queue_with_policy(rule_logical_id, rule_arn, queue_logical_id=None):
8+
resources = []
9+
10+
queue = SQSQueue(queue_logical_id or rule_logical_id + "Queue")
11+
dlq_queue_arn = queue.get_runtime_attr("arn")
12+
dlq_queue_url = queue.get_runtime_attr("queue_url")
13+
14+
# grant necessary permission to Eventbridge Rule resource for sending messages to dead-letter queue
15+
policy = SQSQueuePolicy(rule_logical_id + "QueuePolicy")
16+
policy.PolicyDocument = SQSQueuePolicies.eventbridge_dlq_send_message_resource_based_policy(
17+
rule_arn, dlq_queue_arn
18+
)
19+
policy.Queues = [dlq_queue_url]
20+
21+
resources.append(queue)
22+
resources.append(policy)
23+
24+
return resources
25+
26+
@staticmethod
27+
def validate_dlq_config(source_logical_id, dead_letter_config):
28+
supported_types = ["SQS"]
29+
is_arn_defined = "Arn" in dead_letter_config
30+
is_type_defined = "Type" in dead_letter_config
31+
if is_arn_defined and is_type_defined:
32+
raise InvalidEventException(
33+
source_logical_id, "You can either define 'Arn' or 'Type' property of DeadLetterConfig"
34+
)
35+
if is_type_defined and dead_letter_config.get("Type") not in supported_types:
36+
raise InvalidEventException(
37+
source_logical_id,
38+
"The only valid value for 'Type' property of DeadLetterConfig is 'SQS'",
39+
)
40+
if not is_arn_defined and not is_type_defined:
41+
raise InvalidEventException(source_logical_id, "No 'Arn' or 'Type' property provided for DeadLetterConfig")
42+
43+
@staticmethod
44+
def get_dlq_queue_arn_and_resources(cw_event_source, source_arn):
45+
"""returns dlq queue arn and dlq_resources, assuming cw_event_source.DeadLetterConfig has been validated"""
46+
dlq_queue_arn = cw_event_source.DeadLetterConfig.get("Arn")
47+
if dlq_queue_arn is not None:
48+
return dlq_queue_arn, []
49+
queue_logical_id = cw_event_source.DeadLetterConfig.get("QueueLogicalId")
50+
dlq_resources = EventBridgeRuleUtils.create_dead_letter_queue_with_policy(
51+
cw_event_source.logical_id, source_arn, queue_logical_id
52+
)
53+
dlq_queue_arn = dlq_resources[0].get_runtime_attr("arn")
54+
return dlq_queue_arn, dlq_resources

samtranslator/model/eventsources/push.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from samtranslator.model.events import EventsRule
1313
from samtranslator.model.eventsources.pull import SQS
1414
from samtranslator.model.sqs import SQSQueue, SQSQueuePolicy, SQSQueuePolicies
15+
from samtranslator.model.eventbridge_utils import EventBridgeRuleUtils
1516
from samtranslator.model.iot import IotTopicRule
1617
from samtranslator.model.cognito import CognitoUserPool
1718
from samtranslator.translator import logical_id_generator
@@ -94,6 +95,8 @@ class Schedule(PushEventSource):
9495
"Enabled": PropertyType(False, is_type(bool)),
9596
"Name": PropertyType(False, is_str()),
9697
"Description": PropertyType(False, is_str()),
98+
"DeadLetterConfig": PropertyType(False, is_type(dict)),
99+
"RetryPolicy": PropertyType(False, is_type(dict)),
97100
}
98101

99102
def to_cloudformation(self, **kwargs):
@@ -118,16 +121,23 @@ def to_cloudformation(self, **kwargs):
118121
events_rule.State = "ENABLED" if self.Enabled else "DISABLED"
119122
events_rule.Name = self.Name
120123
events_rule.Description = self.Description
121-
events_rule.Targets = [self._construct_target(function)]
122124

123125
source_arn = events_rule.get_runtime_attr("arn")
126+
dlq_queue_arn = None
127+
if self.DeadLetterConfig is not None:
128+
EventBridgeRuleUtils.validate_dlq_config(self.logical_id, self.DeadLetterConfig)
129+
dlq_queue_arn, dlq_resources = EventBridgeRuleUtils.get_dlq_queue_arn_and_resources(self, source_arn)
130+
resources.extend(dlq_resources)
131+
132+
events_rule.Targets = [self._construct_target(function, dlq_queue_arn)]
133+
124134
if CONDITION in function.resource_attributes:
125135
events_rule.set_resource_attribute(CONDITION, function.resource_attributes[CONDITION])
126136
resources.append(self._construct_permission(function, source_arn=source_arn))
127137

128138
return resources
129139

130-
def _construct_target(self, function):
140+
def _construct_target(self, function, dead_letter_queue_arn=None):
131141
"""Constructs the Target property for the EventBridge Rule.
132142
133143
:returns: the Target property
@@ -137,6 +147,12 @@ def _construct_target(self, function):
137147
if self.Input is not None:
138148
target["Input"] = self.Input
139149

150+
if self.DeadLetterConfig is not None:
151+
target["DeadLetterConfig"] = {"Arn": dead_letter_queue_arn}
152+
153+
if self.RetryPolicy is not None:
154+
target["RetryPolicy"] = self.RetryPolicy
155+
140156
return target
141157

142158

@@ -148,6 +164,8 @@ class CloudWatchEvent(PushEventSource):
148164
property_types = {
149165
"EventBusName": PropertyType(False, is_str()),
150166
"Pattern": PropertyType(False, is_type(dict)),
167+
"DeadLetterConfig": PropertyType(False, is_type(dict)),
168+
"RetryPolicy": PropertyType(False, is_type(dict)),
151169
"Input": PropertyType(False, is_str()),
152170
"InputPath": PropertyType(False, is_str()),
153171
"Target": PropertyType(False, is_type(dict)),
@@ -171,18 +189,24 @@ def to_cloudformation(self, **kwargs):
171189
events_rule = EventsRule(self.logical_id)
172190
events_rule.EventBusName = self.EventBusName
173191
events_rule.EventPattern = self.Pattern
174-
events_rule.Targets = [self._construct_target(function)]
192+
source_arn = events_rule.get_runtime_attr("arn")
193+
194+
dlq_queue_arn = None
195+
if self.DeadLetterConfig is not None:
196+
EventBridgeRuleUtils.validate_dlq_config(self.logical_id, self.DeadLetterConfig)
197+
dlq_queue_arn, dlq_resources = EventBridgeRuleUtils.get_dlq_queue_arn_and_resources(self, source_arn)
198+
resources.extend(dlq_resources)
199+
200+
events_rule.Targets = [self._construct_target(function, dlq_queue_arn)]
175201
if CONDITION in function.resource_attributes:
176202
events_rule.set_resource_attribute(CONDITION, function.resource_attributes[CONDITION])
177203

178204
resources.append(events_rule)
179-
180-
source_arn = events_rule.get_runtime_attr("arn")
181205
resources.append(self._construct_permission(function, source_arn=source_arn))
182206

183207
return resources
184208

185-
def _construct_target(self, function):
209+
def _construct_target(self, function, dead_letter_queue_arn=None):
186210
"""Constructs the Target property for the CloudWatch Events/EventBridge Rule.
187211
188212
:returns: the Target property
@@ -195,6 +219,13 @@ def _construct_target(self, function):
195219

196220
if self.InputPath is not None:
197221
target["InputPath"] = self.InputPath
222+
223+
if self.DeadLetterConfig is not None:
224+
target["DeadLetterConfig"] = {"Arn": dead_letter_queue_arn}
225+
226+
if self.RetryPolicy is not None:
227+
target["RetryPolicy"] = self.RetryPolicy
228+
198229
return target
199230

200231

samtranslator/model/sqs.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ class SQSQueuePolicy(Resource):
1919

2020

2121
class SQSQueuePolicies:
22-
@classmethod
23-
def sns_topic_send_message_role_policy(cls, topic_arn, queue_arn):
22+
@staticmethod
23+
def sns_topic_send_message_role_policy(topic_arn, queue_arn):
2424
document = {
2525
"Version": "2012-10-17",
2626
"Statement": [
@@ -34,3 +34,19 @@ def sns_topic_send_message_role_policy(cls, topic_arn, queue_arn):
3434
],
3535
}
3636
return document
37+
38+
@staticmethod
39+
def eventbridge_dlq_send_message_resource_based_policy(rule_arn, queue_arn):
40+
document = {
41+
"Version": "2012-10-17",
42+
"Statement": [
43+
{
44+
"Action": "sqs:SendMessage",
45+
"Effect": "Allow",
46+
"Principal": {"Service": "events.amazonaws.com"},
47+
"Resource": queue_arn,
48+
"Condition": {"ArnEquals": {"aws:SourceArn": rule_arn}},
49+
}
50+
],
51+
}
52+
return document

samtranslator/model/stepfunctions/events.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from samtranslator.model.intrinsics import fnSub
99
from samtranslator.translator import logical_id_generator
1010
from samtranslator.model.exceptions import InvalidEventException, InvalidResourceException
11+
from samtranslator.model.eventbridge_utils import EventBridgeRuleUtils
1112
from samtranslator.translator.arn_generator import ArnGenerator
1213
from samtranslator.swagger.swagger import SwaggerEditor
1314
from samtranslator.open_api.open_api import OpenApiEditor
@@ -81,6 +82,8 @@ class Schedule(EventSource):
8182
"Enabled": PropertyType(False, is_type(bool)),
8283
"Name": PropertyType(False, is_str()),
8384
"Description": PropertyType(False, is_str()),
85+
"DeadLetterConfig": PropertyType(False, is_type(dict)),
86+
"RetryPolicy": PropertyType(False, is_type(dict)),
8487
}
8588

8689
def to_cloudformation(self, resource, **kwargs):
@@ -107,11 +110,18 @@ def to_cloudformation(self, resource, **kwargs):
107110

108111
role = self._construct_role(resource, permissions_boundary)
109112
resources.append(role)
110-
events_rule.Targets = [self._construct_target(resource, role)]
113+
114+
source_arn = events_rule.get_runtime_attr("arn")
115+
dlq_queue_arn = None
116+
if self.DeadLetterConfig is not None:
117+
EventBridgeRuleUtils.validate_dlq_config(self.logical_id, self.DeadLetterConfig)
118+
dlq_queue_arn, dlq_resources = EventBridgeRuleUtils.get_dlq_queue_arn_and_resources(self, source_arn)
119+
resources.extend(dlq_resources)
120+
events_rule.Targets = [self._construct_target(resource, role, dlq_queue_arn)]
111121

112122
return resources
113123

114-
def _construct_target(self, resource, role):
124+
def _construct_target(self, resource, role, dead_letter_queue_arn=None):
115125
"""Constructs the Target property for the EventBridge Rule.
116126
117127
:returns: the Target property
@@ -125,6 +135,12 @@ def _construct_target(self, resource, role):
125135
if self.Input is not None:
126136
target["Input"] = self.Input
127137

138+
if self.DeadLetterConfig is not None:
139+
target["DeadLetterConfig"] = {"Arn": dead_letter_queue_arn}
140+
141+
if self.RetryPolicy is not None:
142+
target["RetryPolicy"] = self.RetryPolicy
143+
128144
return target
129145

130146

@@ -138,6 +154,8 @@ class CloudWatchEvent(EventSource):
138154
"Pattern": PropertyType(False, is_type(dict)),
139155
"Input": PropertyType(False, is_str()),
140156
"InputPath": PropertyType(False, is_str()),
157+
"DeadLetterConfig": PropertyType(False, is_type(dict)),
158+
"RetryPolicy": PropertyType(False, is_type(dict)),
141159
}
142160

143161
def to_cloudformation(self, resource, **kwargs):
@@ -162,11 +180,19 @@ def to_cloudformation(self, resource, **kwargs):
162180

163181
role = self._construct_role(resource, permissions_boundary)
164182
resources.append(role)
165-
events_rule.Targets = [self._construct_target(resource, role)]
183+
184+
source_arn = events_rule.get_runtime_attr("arn")
185+
dlq_queue_arn = None
186+
if self.DeadLetterConfig is not None:
187+
EventBridgeRuleUtils.validate_dlq_config(self.logical_id, self.DeadLetterConfig)
188+
dlq_queue_arn, dlq_resources = EventBridgeRuleUtils.get_dlq_queue_arn_and_resources(self, source_arn)
189+
resources.extend(dlq_resources)
190+
191+
events_rule.Targets = [self._construct_target(resource, role, dlq_queue_arn)]
166192

167193
return resources
168194

169-
def _construct_target(self, resource, role):
195+
def _construct_target(self, resource, role, dead_letter_queue_arn=None):
170196
"""Constructs the Target property for the CloudWatch Events/EventBridge Rule.
171197
172198
:returns: the Target property
@@ -182,6 +208,13 @@ def _construct_target(self, resource, role):
182208

183209
if self.InputPath is not None:
184210
target["InputPath"] = self.InputPath
211+
212+
if self.DeadLetterConfig is not None:
213+
target["DeadLetterConfig"] = {"Arn": dead_letter_queue_arn}
214+
215+
if self.RetryPolicy is not None:
216+
target["RetryPolicy"] = self.RetryPolicy
217+
185218
return target
186219

187220

samtranslator/validator/sam_schema/schema.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,34 @@
368368
},
369369
"Pattern": {
370370
"type": "object"
371+
},
372+
"DeadLetterConfig": {
373+
"additionalProperties": false,
374+
"properties": {
375+
"Arn": {
376+
"type": "string"
377+
},
378+
"Type": {
379+
"type": "string"
380+
},
381+
"QueueLogicalId": {
382+
"type": "string"
383+
}
384+
},
385+
"type": "object"
386+
},
387+
"RetryPolicy": {
388+
"additionalProperties": false,
389+
"minProperties": 1,
390+
"properties": {
391+
"MaximumEventAgeInSeconds": {
392+
"type": "number"
393+
},
394+
"MaximumRetryAttempts": {
395+
"type": "number"
396+
}
397+
},
398+
"type": "object"
371399
}
372400
},
373401
"required": [

0 commit comments

Comments
 (0)