Skip to content

Commit

Permalink
Merge pull request #580 from distinction-dev/feat-eventbridge-scheduler
Browse files Browse the repository at this point in the history
Create Schedules using Eventbridge Scheduler
  • Loading branch information
horike37 authored Sep 15, 2023
2 parents 045b2e5 + 5514c54 commit ce5261a
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 10 deletions.
108 changes: 98 additions & 10 deletions lib/deploy/events/schedule/compileScheduledEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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') {
Expand All @@ -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 = [
Expand Down Expand Up @@ -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';
Expand All @@ -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);
Expand All @@ -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}",
Expand All @@ -130,11 +221,8 @@ module.exports = {
"RoleArn": ${roleArn}
}]
}
}
`;

let iamRoleTemplate = `
{
}`;
iamRoleTemplate = `{
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
Expand Down Expand Up @@ -169,8 +257,8 @@ module.exports = {
}
]
}
}`;
}
`;
if (permissionsBoundary) {
const jsonIamRole = JSON.parse(iamRoleTemplate);
jsonIamRole.Properties.PermissionsBoundary = permissionsBoundary;
Expand Down
83 changes: 83 additions & 0 deletions lib/deploy/events/schedule/compileScheduledEvents.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
6 changes: 6 additions & 0 deletions lib/naming.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit ce5261a

Please sign in to comment.