From ca4a4f76aa935814a6665469dbbcaf5a70fd2473 Mon Sep 17 00:00:00 2001 From: Tejas Dhamecha Date: Fri, 26 May 2023 18:25:43 +0530 Subject: [PATCH 1/4] feat(EventBridge Scheduler): :sparkles: add logic for eventbridge scheduler --- .../events/schedule/compileScheduledEvents.js | 105 ++++++++++++++++-- 1 file changed, 97 insertions(+), 8 deletions(-) diff --git a/lib/deploy/events/schedule/compileScheduledEvents.js b/lib/deploy/events/schedule/compileScheduledEvents.js index a941be42..6d137c86 100644 --- a/lib/deploy/events/schedule/compileScheduledEvents.js +++ b/lib/deploy/events/schedule/compileScheduledEvents.js @@ -10,6 +10,8 @@ module.exports = { _.forEach(this.getAllStateMachines(), (stateMachineName) => { const stateMachineObj = this.getStateMachine(stateMachineName); let scheduleNumberInFunction = 0; + const METHOD_SCHEDULER = 'scheduler'; + const METHOD_EVENT_BUS = 'eventBus'; if (stateMachineObj.events) { _.forEach(stateMachineObj.events, (event) => { @@ -24,6 +26,8 @@ module.exports = { let InputPathsMap; let Name; let Description; + let method; + let timezone; // TODO validate rate syntax if (typeof event.schedule === 'object') { @@ -49,6 +53,8 @@ module.exports = { InputTemplate = InputTransformer && event.schedule.inputTransformer.inputTemplate; Name = event.schedule.name; Description = event.schedule.description; + method = event.schedule.method || METHOD_EVENT_BUS; + timezone = event.schedule.timezone; if ([Input, InputPath, InputTransformer].filter(Boolean).length > 1) { const errorMessage = [ @@ -76,6 +82,34 @@ module.exports = { // escape quotes to favor JSON.parse InputTemplate = InputTemplate.replace(/\"/g, '\\"'); // eslint-disable-line } + if (InputTransformer) { + if (method === METHOD_SCHEDULER) { + const errorMessage = [ + 'Cannot setup "schedule" event: "inputTransformer" is not supported with "scheduler" mode', + 'SCHEDULE_PARAMETER_NOT_SUPPORTED', + ].join(''); + throw new this.serverless.classes + .Error(errorMessage); + } else { + InputTransformer = this.formatInputTransformer(InputTransformer); + } + } + if (InputPath && method === METHOD_SCHEDULER) { + const errorMessage = [ + 'Cannot setup "schedule" event: "inputPath" is not supported with "scheduler" mode', + 'SCHEDULE_PARAMETER_NOT_SUPPORTED', + ].join(''); + throw new this.serverless.classes + .Error(errorMessage); + } + if (timezone && method !== METHOD_SCHEDULER) { + const errorMessage = [ + 'Cannot setup "schedule" event: "timezone" is only supported with "scheduler" mode', + 'SCHEDULE_PARAMETER_NOT_SUPPORTED', + ].join(''); + throw new this.serverless.classes + .Error(errorMessage); + } } else if (typeof event.schedule === 'string') { ScheduleExpression = event.schedule; State = 'ENABLED'; @@ -110,8 +144,66 @@ module.exports = { } `; - const scheduleTemplate = ` - { + let scheduleTemplate; + let iamRoleTemplate; + + if (method === METHOD_SCHEDULER) { + scheduleTemplate = `{ + "Type": "AWS::Scheduler::Schedule", + "Properties": { + "ScheduleExpression": "${ScheduleExpression}", + "State": "${State}", + ${timezone ? `"ScheduleExpressionTimezone": "${timezone}",` : ''} + ${Name ? `"Name": "${Name}",` : ''} + ${Description ? `"Description": "${Description}",` : ''} + "Target": { + "Arn": { "Ref": "${stateMachineLogicalId}" }, + "RoleArn": ${roleArn} + }, + "FlexibleTimeWindow": { + "Mode": "OFF" + } + } + }`; + + iamRoleTemplate = `{ + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "scheduler.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] + }, + "Policies": [ + { + "PolicyName": "${policyName}", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "states:StartExecution" + ], + "Resource": { + "Ref": "${stateMachineLogicalId}" + } + } + ] + } + } + ] + } + }`; + } else { + scheduleTemplate = `{ "Type": "AWS::Events::Rule", "Properties": { "ScheduleExpression": "${ScheduleExpression}", @@ -130,11 +222,8 @@ module.exports = { "RoleArn": ${roleArn} }] } - } - `; - - let iamRoleTemplate = ` - { + }`; + iamRoleTemplate = `{ "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -169,8 +258,8 @@ module.exports = { } ] } + }`; } - `; if (permissionsBoundary) { const jsonIamRole = JSON.parse(iamRoleTemplate); jsonIamRole.Properties.PermissionsBoundary = permissionsBoundary; From df63d4273f65adb8df86d7e20774d9af6174df2c Mon Sep 17 00:00:00 2001 From: Jap Purohit Date: Mon, 29 May 2023 12:16:42 +0530 Subject: [PATCH 2/4] test(EventBridge Scheduler): :white_check_mark: test cases added and restructured the error message --- .../events/schedule/compileScheduledEvents.js | 14 ++-- .../schedule/compileScheduledEvents.test.js | 83 +++++++++++++++++++ 2 files changed, 89 insertions(+), 8 deletions(-) diff --git a/lib/deploy/events/schedule/compileScheduledEvents.js b/lib/deploy/events/schedule/compileScheduledEvents.js index 6d137c86..19095216 100644 --- a/lib/deploy/events/schedule/compileScheduledEvents.js +++ b/lib/deploy/events/schedule/compileScheduledEvents.js @@ -3,6 +3,8 @@ const _ = require('lodash'); const BbPromise = require('bluebird'); +const METHOD_SCHEDULER = 'scheduler'; +const METHOD_EVENT_BUS = 'eventBus'; module.exports = { compileScheduledEvents() { const service = this.serverless.service; @@ -10,8 +12,6 @@ module.exports = { _.forEach(this.getAllStateMachines(), (stateMachineName) => { const stateMachineObj = this.getStateMachine(stateMachineName); let scheduleNumberInFunction = 0; - const METHOD_SCHEDULER = 'scheduler'; - const METHOD_EVENT_BUS = 'eventBus'; if (stateMachineObj.events) { _.forEach(stateMachineObj.events, (event) => { @@ -86,18 +86,14 @@ module.exports = { if (method === METHOD_SCHEDULER) { const errorMessage = [ 'Cannot setup "schedule" event: "inputTransformer" is not supported with "scheduler" mode', - 'SCHEDULE_PARAMETER_NOT_SUPPORTED', ].join(''); throw new this.serverless.classes .Error(errorMessage); - } else { - InputTransformer = this.formatInputTransformer(InputTransformer); } } if (InputPath && method === METHOD_SCHEDULER) { const errorMessage = [ 'Cannot setup "schedule" event: "inputPath" is not supported with "scheduler" mode', - 'SCHEDULE_PARAMETER_NOT_SUPPORTED', ].join(''); throw new this.serverless.classes .Error(errorMessage); @@ -105,7 +101,6 @@ module.exports = { if (timezone && method !== METHOD_SCHEDULER) { const errorMessage = [ 'Cannot setup "schedule" event: "timezone" is only supported with "scheduler" mode', - 'SCHEDULE_PARAMETER_NOT_SUPPORTED', ].join(''); throw new this.serverless.classes .Error(errorMessage); @@ -146,7 +141,8 @@ module.exports = { let scheduleTemplate; let iamRoleTemplate; - + // If condition for the event bridge schedular and it define + // resource template and iamrole for the same if (method === METHOD_SCHEDULER) { scheduleTemplate = `{ "Type": "AWS::Scheduler::Schedule", @@ -203,6 +199,8 @@ module.exports = { } }`; } else { + // else condition for the event rule and + // it define resource template and iamrole for the same scheduleTemplate = `{ "Type": "AWS::Events::Rule", "Properties": { diff --git a/lib/deploy/events/schedule/compileScheduledEvents.test.js b/lib/deploy/events/schedule/compileScheduledEvents.test.js index fbfba6a7..470fd45a 100644 --- a/lib/deploy/events/schedule/compileScheduledEvents.test.js +++ b/lib/deploy/events/schedule/compileScheduledEvents.test.js @@ -447,4 +447,87 @@ describe('#httpValidate()', () => { .FirstScheduleToStepFunctionsRole .Properties.PermissionsBoundary).to.equal('arn:aws:iam::myAccount:policy/permission_boundary'); }); + + it('should have type of AWS::Scheduler::Schedule if method is scheduler', () => { + serverlessStepFunctions.serverless.service.stepFunctions = { + stateMachines: { + first: { + events: [ + { + schedule: { + method: 'scheduler', + rate: 'rate(10 minutes)', + enabled: false, + timezone: 'Asia/Mumbai', + }, + }, + ], + }, + }, + }; + serverlessStepFunctions.compileScheduledEvents(); + expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources.FirstStepFunctionsEventsRuleSchedule1.Type).to.equal('AWS::Scheduler::Schedule'); + }); + + it('should have service as scheduler.amazonaws.com if method is scheduler', () => { + serverlessStepFunctions.serverless.service.stepFunctions = { + stateMachines: { + first: { + events: [ + { + schedule: { + method: 'scheduler', + rate: 'rate(10 minutes)', + enabled: false, + timezone: 'Asia/Mumbai', + }, + }, + ], + }, + }, + }; + serverlessStepFunctions.compileScheduledEvents(); + expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources.FirstScheduleToStepFunctionsRole.Properties.AssumeRolePolicyDocument.Statement[0].Principal.Service).to.equal('scheduler.amazonaws.com'); + }); + + it('should define timezone when schedular and timezone given', () => { + serverlessStepFunctions.serverless.service.stepFunctions = { + stateMachines: { + first: { + events: [ + { + schedule: { + method: 'scheduler', + rate: 'rate(10 minutes)', + enabled: false, + timezone: 'Asia/Mumbai', + }, + }, + ], + }, + }, + }; + serverlessStepFunctions.compileScheduledEvents(); + + expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources.FirstStepFunctionsEventsRuleSchedule1.Properties.ScheduleExpressionTimezone).to.equal('Asia/Mumbai'); + }); + + it('should accept timezone only if method is scheduler', () => { + serverlessStepFunctions.serverless.service.stepFunctions = { + stateMachines: { + first: { + events: [ + { + schedule: { + rate: 'rate(10 minutes)', + enabled: false, + timezone: 'Asia/Mumbai', + }, + }, + ], + }, + }, + }; + expect(() => serverlessStepFunctions.compileScheduledEvents()).to.throw(Error); + }); }); From af4ce54d792c25926a8bec23d5396315065c2349 Mon Sep 17 00:00:00 2001 From: Dan MacTough Date: Tue, 1 Aug 2023 14:00:51 -0400 Subject: [PATCH 3/4] Give scheduler schedules distinct logical ids from event bus schedules --- lib/deploy/events/schedule/compileScheduledEvents.js | 5 +++-- lib/naming.js | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/deploy/events/schedule/compileScheduledEvents.js b/lib/deploy/events/schedule/compileScheduledEvents.js index 19095216..ee1db6fc 100644 --- a/lib/deploy/events/schedule/compileScheduledEvents.js +++ b/lib/deploy/events/schedule/compileScheduledEvents.js @@ -121,8 +121,9 @@ module.exports = { const stateMachineLogicalId = this .getStateMachineLogicalId(stateMachineName, stateMachineObj); - const scheduleLogicalId = this - .getScheduleLogicalId(stateMachineName, scheduleNumberInFunction); + const scheduleLogicalId = method !== METHOD_SCHEDULER ? this + .getScheduleLogicalId(stateMachineName, scheduleNumberInFunction) : this + .getSchedulerScheduleLogicalId(stateMachineName, scheduleNumberInFunction); const scheduleIamRoleLogicalId = this .getScheduleToStepFunctionsIamRoleLogicalId(stateMachineName); const scheduleId = this.getScheduleId(stateMachineName); diff --git a/lib/naming.js b/lib/naming.js index bd1eab05..54171a90 100644 --- a/lib/naming.js +++ b/lib/naming.js @@ -63,6 +63,12 @@ module.exports = { .getNormalizedFunctionName(stateMachineName)}StepFunctionsEventsRuleSchedule${scheduleIndex}`; }, + getSchedulerScheduleLogicalId(stateMachineName, scheduleIndex) { + return `${this.provider.naming.getNormalizedFunctionName( + stateMachineName + )}StepFunctionsSchedulerSchedule${scheduleIndex}`; + }, + getScheduleToStepFunctionsIamRoleLogicalId(stateMachineName) { return `${this.provider.naming.getNormalizedFunctionName( stateMachineName, From 5514c54a59ffdb6a74b993b4add9aabd5267b714 Mon Sep 17 00:00:00 2001 From: Jap Purohit Date: Thu, 14 Sep 2023 18:41:27 +0530 Subject: [PATCH 4/4] fix: :rotating_light: fixing the lint issue and test case --- lib/deploy/events/schedule/compileScheduledEvents.test.js | 4 ++-- lib/naming.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/deploy/events/schedule/compileScheduledEvents.test.js b/lib/deploy/events/schedule/compileScheduledEvents.test.js index 470fd45a..b5baefdd 100644 --- a/lib/deploy/events/schedule/compileScheduledEvents.test.js +++ b/lib/deploy/events/schedule/compileScheduledEvents.test.js @@ -466,7 +466,7 @@ describe('#httpValidate()', () => { }, }; serverlessStepFunctions.compileScheduledEvents(); - expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources.FirstStepFunctionsEventsRuleSchedule1.Type).to.equal('AWS::Scheduler::Schedule'); + expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources.FirstStepFunctionsSchedulerSchedule1.Type).to.equal('AWS::Scheduler::Schedule'); }); it('should have service as scheduler.amazonaws.com if method is scheduler', () => { @@ -509,7 +509,7 @@ describe('#httpValidate()', () => { }; serverlessStepFunctions.compileScheduledEvents(); - expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources.FirstStepFunctionsEventsRuleSchedule1.Properties.ScheduleExpressionTimezone).to.equal('Asia/Mumbai'); + expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate.Resources.FirstStepFunctionsSchedulerSchedule1.Properties.ScheduleExpressionTimezone).to.equal('Asia/Mumbai'); }); it('should accept timezone only if method is scheduler', () => { diff --git a/lib/naming.js b/lib/naming.js index 54171a90..31fcbfb1 100644 --- a/lib/naming.js +++ b/lib/naming.js @@ -64,9 +64,9 @@ module.exports = { }, getSchedulerScheduleLogicalId(stateMachineName, scheduleIndex) { - return `${this.provider.naming.getNormalizedFunctionName( - stateMachineName - )}StepFunctionsSchedulerSchedule${scheduleIndex}`; + return `${this.provider.naming.getNormalizedFunctionName( + stateMachineName, + )}StepFunctionsSchedulerSchedule${scheduleIndex}`; }, getScheduleToStepFunctionsIamRoleLogicalId(stateMachineName) {