@@ -21,6 +21,77 @@ interface NitricScheduleEventBridgeArgs {
21
21
topics : NitricSnsTopic [ ] ;
22
22
}
23
23
24
+ /**
25
+ * Converts a standard Crontab style cron expression to an AWS specific format.
26
+ *
27
+ * AWS appears to use a variation of the Quartz "Unix-like" Cron Expression Format.
28
+ * Notable changes include:
29
+ * - Removing the 'seconds' value (seconds are not supported)
30
+ * - Making the 'year' value mandatory
31
+ * - Providing a value for both Day of Month and Day of Year is not supported.
32
+ *
33
+ * Quartz CronExpression Docs:
34
+ * https://www.javadoc.io/doc/org.quartz-scheduler/quartz/1.8.2/org/quartz/CronExpression.html
35
+ *
36
+ * AWS Specific CronExpressions Doc:
37
+ * https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html#CronExpressions
38
+ *
39
+ * Crontab Expression Docs:
40
+ * https://man7.org/linux/man-pages/man5/crontab.5.html
41
+ *
42
+ * @param crontab the crontab style cron expression string
43
+ * @returns the input cron expression returned in the AWS specific format
44
+ */
45
+ export const cronToAwsCron = ( crontab : string ) : string => {
46
+ let parts = crontab . split ( ' ' ) ;
47
+ if ( parts . length !== 5 ) {
48
+ throw new Error ( `Invalid Expression. Expected 5 expression values, received ${ parts . length } ` ) ;
49
+ }
50
+
51
+ // Replace */x (i.e. "every x minutes") style inputs to the AWS equivalent
52
+ // AWS uses 0 instead of * for these expressions
53
+ parts = parts . map ( ( part ) => part . replace ( / ^ \* (? = \/ .* ) / g, '0' ) ) ;
54
+
55
+ // Only day of week or day of month can be set with AWS, the other must be a ? char
56
+ // See: https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html#CronExpressions - Restrictions
57
+ const DAY_OF_MONTH = 2 ;
58
+ const DAY_OF_WEEK = 4 ;
59
+ if ( parts [ DAY_OF_WEEK ] === '*' ) {
60
+ parts [ DAY_OF_WEEK ] = '?' ;
61
+ } else {
62
+ if ( parts [ DAY_OF_MONTH ] !== '*' ) {
63
+ // TODO: We can support both in future by creating two EventRules - one for DOW, another for DOM.
64
+ throw new Error ( 'Invalid Expression. Day of Month and Day of Week expression component cannot both be set.' ) ;
65
+ }
66
+ parts [ DAY_OF_MONTH ] = '?' ;
67
+ }
68
+
69
+ // We also need to adjust the Day of Week value
70
+ // crontab uses 0-7 (0 or 7 is Sunday)
71
+ // AWS uses 1-7 (Sunday-Saturday)
72
+ parts [ DAY_OF_WEEK ] = parts [ DAY_OF_WEEK ] . split ( '' )
73
+ . map ( ( char ) => {
74
+ let num = parseInt ( char ) ;
75
+
76
+ if ( ! isNaN ( num ) ) {
77
+ // Check for standard 0-6 day range and increment
78
+ if ( num >= 0 && num <= 6 ) {
79
+ return num + 1 ;
80
+ } else {
81
+ // otherwise default to Sunday
82
+ return 1 ;
83
+ }
84
+ } else {
85
+ return char ;
86
+ }
87
+ } )
88
+ . join ( '' ) ;
89
+
90
+ // Add the year component, this doesn't exist in crontab expressions, so we default it to *
91
+ parts = [ ...parts , '*' ] ;
92
+ return parts . join ( ' ' ) ;
93
+ } ;
94
+
24
95
/**
25
96
* Nitric EventBridge based Schedule
26
97
*/
@@ -40,13 +111,20 @@ export class NitricScheduleEventBridge extends pulumi.ComponentResource {
40
111
41
112
this . name = schedule . name ;
42
113
114
+ let awsCronValue = '' ;
115
+ try {
116
+ awsCronValue = cronToAwsCron ( schedule . expression ?. replace ( / [ ' " ] + / g, '' ) ) ;
117
+ } catch ( error ) {
118
+ throw new Error ( `Failed to process expression for schedule ${ this . name } . Details: ${ ( error as Error ) . message } ` ) ;
119
+ }
120
+
43
121
if ( topic ) {
44
122
const rule = new aws . cloudwatch . EventRule (
45
123
`${ schedule . name } Schedule` ,
46
124
{
47
125
description : `Nitric schedule trigger for ${ schedule . name } ` ,
48
126
name : schedule . name ,
49
- scheduleExpression : `cron(${ schedule . expression ?. replace ( / [ ' " ] + / g , '' ) } )` ,
127
+ scheduleExpression : `cron(${ awsCronValue } )` ,
50
128
} ,
51
129
defaultResourceOptions ,
52
130
) ;
@@ -59,6 +137,35 @@ export class NitricScheduleEventBridge extends pulumi.ComponentResource {
59
137
} ,
60
138
defaultResourceOptions ,
61
139
) ;
140
+
141
+ const snsTopicSchedulePolicy = topic . sns . arn . apply ( ( arn ) =>
142
+ aws . iam . getPolicyDocument ( {
143
+ // TODO: According to the docs, 'conditions' are not supported for a policy involving EventBridge
144
+ // See: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-use-resource-based.html#eb-sns-permissions
145
+ // "You can't use of Condition blocks in Amazon SNS topic policies for EventBridge."
146
+ // This means any EventBridge rule will be able to publish to this topic.
147
+ policyId : '__default_policy_ID' ,
148
+ statements : [
149
+ {
150
+ sid : '__default_statement_ID' ,
151
+ effect : 'Allow' ,
152
+ actions : [ 'SNS:Publish' ] ,
153
+ principals : [
154
+ {
155
+ type : 'Service' ,
156
+ identifiers : [ 'events.amazonaws.com' ] ,
157
+ } ,
158
+ ] ,
159
+ resources : [ arn ] ,
160
+ } ,
161
+ ] ,
162
+ } ) ,
163
+ ) ;
164
+
165
+ new aws . sns . TopicPolicy ( `${ schedule . name } Target${ topic . name } Policy` , {
166
+ arn : topic . sns . arn ,
167
+ policy : snsTopicSchedulePolicy . apply ( ( snsTopicPolicy ) => snsTopicPolicy . json ) ,
168
+ } ) ;
62
169
}
63
170
64
171
this . registerOutputs ( {
0 commit comments