Skip to content

Commit cf8418b

Browse files
authored
feat: allow user to provide role for schedule (#3363)
1 parent 9c59fc7 commit cf8418b

File tree

9 files changed

+473
-16
lines changed

9 files changed

+473
-16
lines changed

samtranslator/internal/schema_source/aws_serverless_statemachine.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class ScheduleEventProperties(BaseModel):
4949
Schedule: Optional[PassThroughProp] = scheduleeventproperties("Schedule")
5050
State: Optional[PassThroughProp] = scheduleeventproperties("State")
5151
Target: Optional[ScheduleTarget] = scheduleeventproperties("Target")
52+
RoleArn: Optional[PassThroughProp] # TODO: add doc
5253

5354

5455
class ScheduleEvent(BaseModel):

samtranslator/model/stepfunctions/events.py

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import json
22
from abc import ABCMeta
3-
from typing import Any, Dict, Optional, cast
3+
from typing import Any, Dict, List, Optional, Union, cast
44

55
from samtranslator.metrics.method_decorator import cw_timer
66
from samtranslator.model import Property, PropertyType, Resource, ResourceMacro
@@ -10,6 +10,7 @@
1010
from samtranslator.model.exceptions import InvalidEventException
1111
from samtranslator.model.iam import IAMRole, IAMRolePolicies
1212
from samtranslator.model.intrinsics import fnSub
13+
from samtranslator.model.stepfunctions.resources import StepFunctionsStateMachine
1314
from samtranslator.model.types import IS_BOOL, IS_DICT, IS_STR, PassThrough
1415
from samtranslator.swagger.swagger import SwaggerEditor
1516
from samtranslator.translator import logical_id_generator
@@ -50,7 +51,13 @@ def _generate_logical_id(self, prefix, suffix, resource_type): # type: ignore[n
5051
generator = logical_id_generator.LogicalIdGenerator(prefix + resource_type, suffix)
5152
return generator.gen()
5253

53-
def _construct_role(self, resource, permissions_boundary=None, prefix=None, suffix=""): # type: ignore[no-untyped-def]
54+
def _construct_role(
55+
self,
56+
resource: StepFunctionsStateMachine,
57+
permissions_boundary: Optional[str],
58+
prefix: Optional[str],
59+
suffix: str = "",
60+
) -> IAMRole:
5461
"""Constructs the IAM Role resource allowing the event service to invoke
5562
the StartExecution API of the state machine resource it is associated with.
5663
@@ -93,6 +100,7 @@ class Schedule(EventSource):
93100
"DeadLetterConfig": PropertyType(False, IS_DICT),
94101
"RetryPolicy": PropertyType(False, IS_DICT),
95102
"Target": Property(False, IS_DICT),
103+
"RoleArn": Property(False, IS_STR),
96104
}
97105

98106
Schedule: PassThrough
@@ -104,6 +112,7 @@ class Schedule(EventSource):
104112
DeadLetterConfig: Optional[Dict[str, Any]]
105113
RetryPolicy: Optional[PassThrough]
106114
Target: Optional[PassThrough]
115+
RoleArn: Optional[PassThrough]
107116

108117
@cw_timer(prefix=SFN_EVETSOURCE_METRIC_PREFIX)
109118
def to_cloudformation(self, resource, **kwargs): # type: ignore[no-untyped-def]
@@ -113,7 +122,7 @@ def to_cloudformation(self, resource, **kwargs): # type: ignore[no-untyped-def]
113122
:returns: a list of vanilla CloudFormation Resources, to which this Schedule event expands
114123
:rtype: list
115124
"""
116-
resources = []
125+
resources: List[Any] = []
117126

118127
permissions_boundary = kwargs.get("permissions_boundary")
119128

@@ -135,8 +144,12 @@ def to_cloudformation(self, resource, **kwargs): # type: ignore[no-untyped-def]
135144
events_rule.Name = self.Name
136145
events_rule.Description = self.Description
137146

138-
role = self._construct_role(resource, permissions_boundary) # type: ignore[no-untyped-call]
139-
resources.append(role)
147+
role: Union[IAMRole, str, Dict[str, Any]]
148+
if self.RoleArn is None:
149+
role = self._construct_role(resource, permissions_boundary, prefix=None)
150+
resources.append(role)
151+
else:
152+
role = self.RoleArn
140153

141154
source_arn = events_rule.get_runtime_attr("arn")
142155
dlq_queue_arn = None
@@ -146,26 +159,44 @@ def to_cloudformation(self, resource, **kwargs): # type: ignore[no-untyped-def]
146159
self, source_arn, passthrough_resource_attributes
147160
)
148161
resources.extend(dlq_resources)
149-
events_rule.Targets = [self._construct_target(resource, role, dlq_queue_arn)] # type: ignore[no-untyped-call]
162+
events_rule.Targets = [self._construct_target(resource, role, dlq_queue_arn)]
150163

151164
return resources
152165

153-
def _construct_target(self, resource, role, dead_letter_queue_arn=None): # type: ignore[no-untyped-def]
154-
"""Constructs the Target property for the EventBridge Rule.
155-
156-
:returns: the Target property
157-
:rtype: dict
166+
def _construct_target(
167+
self,
168+
resource: StepFunctionsStateMachine,
169+
role: Union[IAMRole, str, Dict[str, Any]],
170+
dead_letter_queue_arn: Optional[str],
171+
) -> Dict[str, Any]:
172+
"""_summary_
173+
174+
Parameters
175+
----------
176+
resource
177+
StepFunctionsState machine resource to be generated
178+
role
179+
The role to be used by the Schedule event resource either generated or user provides arn
180+
dead_letter_queue_arn
181+
Dead letter queue associated with the resource
182+
183+
Returns
184+
-------
185+
The Target property
158186
"""
159187
target_id = (
160188
self.Target["Id"]
161189
if self.Target and "Id" in self.Target
162190
else generate_valid_target_id(self.logical_id, EVENT_RULE_SFN_TARGET_SUFFIX)
163191
)
192+
164193
target = {
165194
"Arn": resource.get_runtime_attr("arn"),
166195
"Id": target_id,
167-
"RoleArn": role.get_runtime_attr("arn"),
168196
}
197+
198+
target["RoleArn"] = role.get_runtime_attr("arn") if isinstance(role, IAMRole) else role
199+
169200
if self.Input is not None:
170201
target["Input"] = self.Input
171202

@@ -216,7 +247,7 @@ def to_cloudformation(self, resource, **kwargs): # type: ignore[no-untyped-def]
216247
:returns: a list of vanilla CloudFormation Resources, to which this CloudWatch Events/EventBridge event expands
217248
:rtype: list
218249
"""
219-
resources = []
250+
resources: List[Any] = []
220251

221252
permissions_boundary = kwargs.get("permissions_boundary")
222253

@@ -231,7 +262,11 @@ def to_cloudformation(self, resource, **kwargs): # type: ignore[no-untyped-def]
231262

232263
resources.append(events_rule)
233264

234-
role = self._construct_role(resource, permissions_boundary) # type: ignore[no-untyped-call]
265+
role = self._construct_role(
266+
resource,
267+
permissions_boundary,
268+
prefix=None,
269+
)
235270
resources.append(role)
236271

237272
source_arn = events_rule.get_runtime_attr("arn")
@@ -331,7 +366,7 @@ def to_cloudformation(self, resource, **kwargs): # type: ignore[no-untyped-def]
331366
:returns: a list of vanilla CloudFormation Resources, to which this Api event expands
332367
:rtype: list
333368
"""
334-
resources = []
369+
resources: List[Any] = []
335370

336371
intrinsics_resolver = kwargs.get("intrinsics_resolver")
337372
permissions_boundary = kwargs.get("permissions_boundary")
@@ -340,7 +375,7 @@ def to_cloudformation(self, resource, **kwargs): # type: ignore[no-untyped-def]
340375
# Convert to lower case so that user can specify either GET or get
341376
self.Method = self.Method.lower()
342377

343-
role = self._construct_role(resource, permissions_boundary) # type: ignore[no-untyped-call]
378+
role = self._construct_role(resource, permissions_boundary, prefix=None)
344379
resources.append(role)
345380

346381
explicit_api = kwargs["explicit_api"]

samtranslator/schema/schema.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251046,6 +251046,9 @@
251046251046
"markdownDescription": "A `RetryPolicy` object that includes information about the retry policy settings\\. For more information, see [Event retry policy and using dead\\-letter queues](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html) in the *Amazon EventBridge User Guide*\\. \n*Type*: [RetryPolicy](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-events-rule-target.html#cfn-events-rule-target-retrypolicy) \n*Required*: No \n*AWS CloudFormation compatibility*: This property is passed directly to the [`RetryPolicy`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-events-rule-target.html#cfn-events-rule-target-retrypolicy) property of the `AWS::Events::Rule` `Target` data type\\.",
251047251047
"title": "RetryPolicy"
251048251048
},
251049+
"RoleArn": {
251050+
"$ref": "#/definitions/PassThroughProp"
251051+
},
251049251052
"Schedule": {
251050251053
"allOf": [
251051251054
{

schema_source/sam.schema.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3013,6 +3013,9 @@
30133013
"markdownDescription": "A `RetryPolicy` object that includes information about the retry policy settings\\. For more information, see [Event retry policy and using dead\\-letter queues](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html) in the *Amazon EventBridge User Guide*\\. \n*Type*: [RetryPolicy](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-events-rule-target.html#cfn-events-rule-target-retrypolicy) \n*Required*: No \n*AWS CloudFormation compatibility*: This property is passed directly to the [`RetryPolicy`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-events-rule-target.html#cfn-events-rule-target-retrypolicy) property of the `AWS::Events::Rule` `Target` data type\\.",
30143014
"title": "RetryPolicy"
30153015
},
3016+
"RoleArn": {
3017+
"$ref": "#/definitions/PassThroughProp"
3018+
},
30163019
"Schedule": {
30173020
"allOf": [
30183021
{

tests/model/stepfunctions/test_schedule_event.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,13 @@ def test_to_cloudformation_with_dlq_generated_with_intrinsic_function_custom_log
153153
with self.assertRaises(InvalidEventException):
154154
self.schedule_event_source.to_cloudformation(resource=self.state_machine)
155155

156+
def test_to_cloudformation_with_role_arn_provided(self):
157+
role = "not a real arn"
158+
self.schedule_event_source.RoleArn = role
159+
resources = self.schedule_event_source.to_cloudformation(resource=self.state_machine)
160+
event_rule = resources[0]
161+
self.assertEqual(event_rule.Targets[0]["RoleArn"], "not a real arn")
162+
156163
@parameterized.expand(
157164
[
158165
(True, "Enabled"),
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
Resources:
2+
3+
MyStateMachine:
4+
Type: AWS::Serverless::StateMachine
5+
6+
Properties:
7+
DefinitionUri: s3://sam-demo-bucket/my_state_machine.asl.json
8+
Events:
9+
CWSchedule:
10+
Type: Schedule
11+
Properties:
12+
Schedule: rate(1 minute)
13+
Description: test schedule
14+
Enabled: false
15+
RoleArn: arn:0000000000:iam::role/yoyo
16+
CWScheduleCreateRole:
17+
Type: Schedule
18+
Properties:
19+
Schedule: rate(1 minute)
20+
Description: test schedule
21+
Enabled: false
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
{
2+
"Resources": {
3+
"MyStateMachine": {
4+
"Properties": {
5+
"DefinitionS3Location": {
6+
"Bucket": "sam-demo-bucket",
7+
"Key": "my_state_machine.asl.json"
8+
},
9+
"RoleArn": {
10+
"Fn::GetAtt": [
11+
"MyStateMachineRole",
12+
"Arn"
13+
]
14+
},
15+
"Tags": [
16+
{
17+
"Key": "stateMachine:createdBy",
18+
"Value": "SAM"
19+
}
20+
]
21+
},
22+
"Type": "AWS::StepFunctions::StateMachine"
23+
},
24+
"MyStateMachineCWSchedule": {
25+
"Properties": {
26+
"Description": "test schedule",
27+
"ScheduleExpression": "rate(1 minute)",
28+
"State": "DISABLED",
29+
"Targets": [
30+
{
31+
"Arn": {
32+
"Ref": "MyStateMachine"
33+
},
34+
"Id": "MyStateMachineCWScheduleStepFunctionsTarget",
35+
"RoleArn": "arn:0000000000:iam::role/yoyo"
36+
}
37+
]
38+
},
39+
"Type": "AWS::Events::Rule"
40+
},
41+
"MyStateMachineCWScheduleCreateRole": {
42+
"Properties": {
43+
"Description": "test schedule",
44+
"ScheduleExpression": "rate(1 minute)",
45+
"State": "DISABLED",
46+
"Targets": [
47+
{
48+
"Arn": {
49+
"Ref": "MyStateMachine"
50+
},
51+
"Id": "MyStateMachineCWScheduleCreateRoleStepFunctionsTarget",
52+
"RoleArn": {
53+
"Fn::GetAtt": [
54+
"MyStateMachineCWScheduleCreateRoleRole",
55+
"Arn"
56+
]
57+
}
58+
}
59+
]
60+
},
61+
"Type": "AWS::Events::Rule"
62+
},
63+
"MyStateMachineCWScheduleCreateRoleRole": {
64+
"Properties": {
65+
"AssumeRolePolicyDocument": {
66+
"Statement": [
67+
{
68+
"Action": [
69+
"sts:AssumeRole"
70+
],
71+
"Effect": "Allow",
72+
"Principal": {
73+
"Service": [
74+
"events.amazonaws.com"
75+
]
76+
}
77+
}
78+
],
79+
"Version": "2012-10-17"
80+
},
81+
"Policies": [
82+
{
83+
"PolicyDocument": {
84+
"Statement": [
85+
{
86+
"Action": "states:StartExecution",
87+
"Effect": "Allow",
88+
"Resource": {
89+
"Ref": "MyStateMachine"
90+
}
91+
}
92+
]
93+
},
94+
"PolicyName": "MyStateMachineCWScheduleCreateRoleRoleStartExecutionPolicy"
95+
}
96+
]
97+
},
98+
"Type": "AWS::IAM::Role"
99+
},
100+
"MyStateMachineRole": {
101+
"Properties": {
102+
"AssumeRolePolicyDocument": {
103+
"Statement": [
104+
{
105+
"Action": [
106+
"sts:AssumeRole"
107+
],
108+
"Effect": "Allow",
109+
"Principal": {
110+
"Service": [
111+
"states.amazonaws.com"
112+
]
113+
}
114+
}
115+
],
116+
"Version": "2012-10-17"
117+
},
118+
"ManagedPolicyArns": [],
119+
"Tags": [
120+
{
121+
"Key": "stateMachine:createdBy",
122+
"Value": "SAM"
123+
}
124+
]
125+
},
126+
"Type": "AWS::IAM::Role"
127+
}
128+
}
129+
}

0 commit comments

Comments
 (0)