diff --git a/lib/deploy/events/schedule/compileScheduledEvents.js b/lib/deploy/events/schedule/compileScheduledEvents.js index a941be42..ee1db6fc 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; @@ -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,29 @@ 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', + ].join(''); + throw new this.serverless.classes + .Error(errorMessage); + } + } + if (InputPath && method === METHOD_SCHEDULER) { + const errorMessage = [ + 'Cannot setup "schedule" event: "inputPath" is not supported with "scheduler" mode', + ].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', + ].join(''); + throw new this.serverless.classes + .Error(errorMessage); + } } else if (typeof event.schedule === 'string') { ScheduleExpression = event.schedule; State = 'ENABLED'; @@ -92,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); @@ -110,8 +140,69 @@ module.exports = { } `; - const scheduleTemplate = ` - { + 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", + "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 { + // else condition for the event rule and + // it define resource template and iamrole for the same + scheduleTemplate = `{ "Type": "AWS::Events::Rule", "Properties": { "ScheduleExpression": "${ScheduleExpression}", @@ -130,11 +221,8 @@ module.exports = { "RoleArn": ${roleArn} }] } - } - `; - - let iamRoleTemplate = ` - { + }`; + iamRoleTemplate = `{ "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -169,8 +257,8 @@ module.exports = { } ] } + }`; } - `; if (permissionsBoundary) { const jsonIamRole = JSON.parse(iamRoleTemplate); jsonIamRole.Properties.PermissionsBoundary = permissionsBoundary; diff --git a/lib/deploy/events/schedule/compileScheduledEvents.test.js b/lib/deploy/events/schedule/compileScheduledEvents.test.js index fbfba6a7..b5baefdd 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.FirstStepFunctionsSchedulerSchedule1.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.FirstStepFunctionsSchedulerSchedule1.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); + }); }); diff --git a/lib/naming.js b/lib/naming.js index bd1eab05..31fcbfb1 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,