Skip to content

Commit 0167216

Browse files
committed
add schedule events
1 parent 44697c1 commit 0167216

File tree

5 files changed

+256
-14
lines changed

5 files changed

+256
-14
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
'use strict';
2+
3+
const _ = require('lodash');
4+
const BbPromise = require('bluebird');
5+
6+
module.exports = {
7+
compileScheduledEvents() {
8+
_.forEach(this.getAllStateMachines(), (stateMachineName) => {
9+
const stateMachineObj = this.getStateMachine(stateMachineName);
10+
let scheduleNumberInFunction = 0;
11+
12+
if (stateMachineObj.events) {
13+
_.forEach(stateMachineObj.events, (event) => {
14+
if (event.schedule) {
15+
scheduleNumberInFunction++;
16+
let ScheduleExpression;
17+
let State;
18+
let Input;
19+
let InputPath;
20+
let Name;
21+
let Description;
22+
23+
// TODO validate rate syntax
24+
if (typeof event.schedule === 'object') {
25+
if (!event.schedule.rate) {
26+
const errorMessage = [
27+
`Missing "rate" property for schedule event in stateMachine ${stateMachineName}`,
28+
' The correct syntax is: schedule: rate(10 minutes)',
29+
' OR an object with "rate" property.',
30+
' Please check the README for more info.',
31+
].join('');
32+
throw new this.serverless.classes
33+
.Error(errorMessage);
34+
}
35+
ScheduleExpression = event.schedule.rate;
36+
State = 'ENABLED';
37+
if (event.schedule.enabled === false) {
38+
State = 'DISABLED';
39+
}
40+
Input = event.schedule.input;
41+
InputPath = event.schedule.inputPath;
42+
Name = event.schedule.name;
43+
Description = event.schedule.description;
44+
45+
if (Input && InputPath) {
46+
const errorMessage = [
47+
'You can\'t set both input & inputPath properties at the',
48+
'same time for schedule events.',
49+
'Please check the AWS docs for more info',
50+
].join('');
51+
throw new this.serverless.classes
52+
.Error(errorMessage);
53+
}
54+
55+
if (Input && typeof Input === 'object') {
56+
Input = JSON.stringify(Input);
57+
}
58+
if (Input && typeof Input === 'string') {
59+
// escape quotes to favor JSON.parse
60+
Input = Input.replace(/\"/g, '\\"'); // eslint-disable-line
61+
}
62+
} else if (typeof event.schedule === 'string') {
63+
ScheduleExpression = event.schedule;
64+
State = 'ENABLED';
65+
} else {
66+
const errorMessage = [
67+
`Schedule event of stateMachine ${stateMachineName} is not an object nor a string`,
68+
' The correct syntax is: schedule: rate(10 minutes)',
69+
' OR an object with "rate" property.',
70+
' Please check the README for more info.',
71+
].join('');
72+
throw new this.serverless.classes
73+
.Error(errorMessage);
74+
}
75+
76+
const stateMachineLogicalId = this
77+
.getStateMachineLogicalId(stateMachineName, stateMachineObj.name);
78+
const scheduleLogicalId = this
79+
.getScheduleLogicalId(stateMachineName, scheduleNumberInFunction);
80+
const scheduleIamRoleLogicalId = this
81+
.getScheduleToStepFunctionsIamRoleLogicalId(stateMachineName);
82+
const scheduleId = this.getScheduleId(stateMachineName);
83+
const policyName = this.getSchedulePolicyName(stateMachineName);
84+
85+
const scheduleTemplate = `
86+
{
87+
"Type": "AWS::Events::Rule",
88+
"Properties": {
89+
"ScheduleExpression": "${ScheduleExpression}",
90+
"State": "${State}",
91+
${Name ? `"Name": "${Name}",` : ''}
92+
${Description ? `"Description": "${Description}",` : ''}
93+
"Targets": [{
94+
${Input ? `"Input": "${Input}",` : ''}
95+
${InputPath ? `"InputPath": "${InputPath}",` : ''}
96+
"Arn": { "Ref": "${stateMachineLogicalId}" },
97+
"Id": "${scheduleId}",
98+
"RoleArn": {
99+
"Fn::GetAtt": [
100+
"${scheduleIamRoleLogicalId}",
101+
"Arn"
102+
]
103+
}
104+
}]
105+
}
106+
}
107+
`;
108+
109+
const iamRoleTemplate = `
110+
{
111+
"Type": "AWS::IAM::Role",
112+
"Properties": {
113+
"AssumeRolePolicyDocument": {
114+
"Version": "2012-10-17",
115+
"Statement": [
116+
{
117+
"Effect": "Allow",
118+
"Principal": {
119+
"Service": "events.amazonaws.com"
120+
},
121+
"Action": "sts:AssumeRole"
122+
}
123+
]
124+
},
125+
"Policies": [
126+
{
127+
"PolicyName": "${policyName}",
128+
"PolicyDocument": {
129+
"Version": "2012-10-17",
130+
"Statement": [
131+
{
132+
"Effect": "Allow",
133+
"Action": [
134+
"states:StartExecution"
135+
],
136+
"Resource": {
137+
"Ref": "${stateMachineLogicalId}"
138+
}
139+
}
140+
]
141+
}
142+
}
143+
]
144+
}
145+
}
146+
`;
147+
148+
const newScheduleObject = {
149+
[scheduleLogicalId]: JSON.parse(scheduleTemplate),
150+
};
151+
152+
const newPermissionObject = {
153+
[scheduleIamRoleLogicalId]: JSON.parse(iamRoleTemplate),
154+
};
155+
156+
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
157+
newScheduleObject, newPermissionObject);
158+
}
159+
});
160+
}
161+
});
162+
return BbPromise.resolve();
163+
},
164+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict';
2+
3+
const expect = require('chai').expect;
4+
const Serverless = require('serverless/lib/Serverless');
5+
const AwsProvider = require('serverless/lib/plugins/aws/provider/awsProvider');
6+
const ServerlessStepFunctions = require('./../../../index');
7+
8+
describe('#httpValidate()', () => {
9+
let serverless;
10+
let serverlessStepFunctions;
11+
12+
beforeEach(() => {
13+
serverless = new Serverless();
14+
serverless.setProvider('aws', new AwsProvider(serverless));
15+
const options = {
16+
stage: 'dev',
17+
region: 'us-east-1',
18+
};
19+
serverlessStepFunctions = new ServerlessStepFunctions(serverless, options);
20+
});
21+
});

lib/index.js

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const httpIamRole = require('./deploy/events/apiGateway/iamRole');
1212
const httpDeployment = require('./deploy/events/apiGateway/deployment');
1313
const httpRestApi = require('./deploy/events/apiGateway/restApi');
1414
const httpInfo = require('./deploy/events/apiGateway/endpointInfo');
15+
const compileScheduledEvents = require('./deploy/events/schedule/compileScheduledEvents');
1516
const invoke = require('./invoke/invoke');
1617
const yamlParser = require('./yamlParser');
1718
const naming = require('./naming');
@@ -42,7 +43,8 @@ class ServerlessStepFunctions {
4243
httpDeployment,
4344
invoke,
4445
yamlParser,
45-
naming
46+
naming,
47+
compileScheduledEvents
4648
);
4749

4850
this.commands = {
@@ -91,21 +93,23 @@ class ServerlessStepFunctions {
9193
.then(this.compileIamRole)
9294
.then(this.compileStateMachines)
9395
.then(this.compileActivities),
94-
'package:compileEvents': () => {
95-
this.pluginhttpValidated = this.httpValidate();
96+
'package:compileEvents': () =>
97+
this.compileScheduledEvents().then(() => {
98+
this.pluginhttpValidated = this.httpValidate();
9699

97-
if (this.pluginhttpValidated.events.length === 0) {
98-
return BbPromise.resolve();
99-
}
100+
if (this.pluginhttpValidated.events.length === 0) {
101+
return BbPromise.resolve();
102+
}
100103

101-
return BbPromise.bind(this)
102-
.then(this.compileRestApi)
103-
.then(this.compileResources)
104-
.then(this.compileMethods)
105-
.then(this.compileCors)
106-
.then(this.compileHttpIamRole)
107-
.then(this.compileDeployment);
108-
},
104+
return BbPromise.bind(this)
105+
.then(this.compileRestApi)
106+
.then(this.compileResources)
107+
.then(this.compileMethods)
108+
.then(this.compileCors)
109+
.then(this.compileHttpIamRole)
110+
.then(this.compileDeployment);
111+
}
112+
),
109113
'after:deploy:deploy': () => BbPromise.bind(this)
110114
.then(this.getEndpointInfo)
111115
.then(this.display),

lib/naming.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,29 @@ module.exports = {
5151
getApiToStepFunctionsIamRoleLogicalId() {
5252
return 'ApigatewayToStepFunctionsRole';
5353
},
54+
55+
// Schedule
56+
getScheduleId(stateMachineName) {
57+
return `${stateMachineName}StepFunctionsSchedule`;
58+
},
59+
60+
getScheduleLogicalId(stateMachineName, scheduleIndex) {
61+
return `${this.provider.naming
62+
.getNormalizedFunctionName(stateMachineName)}StepFunctionsEventsRuleSchedule${scheduleIndex}`;
63+
},
64+
65+
getScheduleToStepFunctionsIamRoleLogicalId(stateMachineName) {
66+
return `${this.provider.naming.getNormalizedFunctionName(
67+
stateMachineName)}ScheduleToStepFunctionsRole`;
68+
},
69+
70+
getSchedulePolicyName(stateMachineName) {
71+
return [
72+
this.provider.getStage(),
73+
this.provider.getRegion(),
74+
this.provider.serverless.service.service,
75+
stateMachineName,
76+
'schedule',
77+
].join('-');
78+
},
5479
};

lib/naming.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,32 @@ describe('#naming', () => {
8787
.equal('dev-step-functions-stepfunctions');
8888
});
8989
});
90+
91+
describe('#getScheduleId()', () => {
92+
it('should normalize the stateMachine output name', () => {
93+
expect(serverlessStepFunctions.getScheduleId('stateMachine')).to
94+
.equal('stateMachineStepFunctionsSchedule');
95+
});
96+
});
97+
98+
describe('#getScheduleLogicalId()', () => {
99+
it('should normalize the stateMachine output name and add the standard suffix', () => {
100+
expect(serverlessStepFunctions.getScheduleLogicalId('stateMachine', 1)).to
101+
.equal('StateMachineStepFunctionsEventsRuleSchedule1');
102+
});
103+
});
104+
105+
describe('#getScheduleToStepFunctionsIamRoleLogicalId()', () => {
106+
it('should normalize the stateMachine output name', () => {
107+
expect(serverlessStepFunctions.getScheduleToStepFunctionsIamRoleLogicalId('stateMachine')).to
108+
.equal('StateMachineScheduleToStepFunctionsRole');
109+
});
110+
});
111+
112+
describe('#getSchedulePolicyName()', () => {
113+
it('should use the stage and service name', () => {
114+
expect(serverlessStepFunctions.getSchedulePolicyName('stateMachine')).to
115+
.equal('dev-us-east-1-step-functions-stateMachine-schedule');
116+
});
117+
});
90118
});

0 commit comments

Comments
 (0)