serverless
Version:
Serverless Framework - Build web, mobile and IoT applications with serverless architectures using AWS Lambda, Azure Functions, Google CloudFunctions & more
189 lines (169 loc) • 7.21 kB
JavaScript
;
const _ = require('lodash');
const rateSyntaxPattern = /^rate\((?:1 (?:minute|hour|day)|(?:1\d+|[2-9]\d*) (?:minute|hour|day)s)\)$/;
const cronSyntaxPattern = /^cron\(\S+ \S+ \S+ \S+ \S+ \S+\)$/;
class AwsCompileScheduledEvents {
constructor(serverless) {
this.serverless = serverless;
this.provider = this.serverless.getProvider('aws');
this.hooks = {
'package:compileEvents': this.compileScheduledEvents.bind(this),
};
}
buildValidationErrorMessage(functionName) {
return [
`"rate" property for schedule event is missing or invalid in function ${functionName}.`,
' The correct syntax is: `schedule: rate(10 minutes)`, `schedule: cron(0 12 * * ? *)`',
' OR an object with "rate" property.',
' Please check the docs for more info:',
' https://serverless.com/framework/docs/providers/aws/events/schedule/',
].join('');
}
compileScheduledEvents() {
this.serverless.service.getAllFunctions().forEach(functionName => {
const functionObj = this.serverless.service.getFunction(functionName);
let scheduleNumberInFunction = 0;
if (functionObj.events) {
functionObj.events.forEach(event => {
if (event.schedule) {
scheduleNumberInFunction++;
let ScheduleExpression;
let State;
let Input;
let InputPath;
let InputTransformer;
let Name;
let Description;
if (typeof event.schedule === 'object') {
if (!this.validateScheduleSyntax(event.schedule.rate)) {
const errorMessage = this.buildValidationErrorMessage(functionName);
throw new this.serverless.classes.Error(errorMessage);
}
ScheduleExpression = event.schedule.rate;
State = 'ENABLED';
if (event.schedule.enabled === false) {
State = 'DISABLED';
}
Input = event.schedule.input;
InputPath = event.schedule.inputPath;
InputTransformer = event.schedule.inputTransformer;
Name = event.schedule.name;
Description = event.schedule.description;
const inputOptions = [Input, InputPath, InputTransformer].filter(i => i);
if (inputOptions.length > 1) {
const errorMessage = [
'You can only set one of input, inputPath, or inputTransformer ',
'properties at the same time for schedule events. ',
'Please check the AWS docs for more info',
].join('');
throw new this.serverless.classes.Error(errorMessage);
}
if (Input && typeof Input === 'object') {
if (typeof Input.body === 'string') {
try {
Input.body = JSON.parse(Input.body);
} catch (error) {
const errorMessage = [
'The body of the schedule event associated with',
` ${functionName} was passed as a string`,
' but it failed to parse to a JSON object.',
' Please check the docs for more info.',
].join('');
throw new this.serverless.classes.Error(errorMessage);
}
}
Input = JSON.stringify(Input);
}
if (Input && typeof Input === 'string') {
// escape quotes to favor JSON.parse
Input = Input.replace(/\"/g, '\\"'); // eslint-disable-line
}
if (InputTransformer) {
InputTransformer = this.formatInputTransformer(InputTransformer);
}
} else if (this.validateScheduleSyntax(event.schedule)) {
ScheduleExpression = event.schedule;
State = 'ENABLED';
} else {
const errorMessage = this.buildValidationErrorMessage(functionName);
throw new this.serverless.classes.Error(errorMessage);
}
const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(functionName);
const scheduleLogicalId = this.provider.naming.getScheduleLogicalId(
functionName,
scheduleNumberInFunction
);
const lambdaPermissionLogicalId = this.provider.naming.getLambdaSchedulePermissionLogicalId(
functionName,
scheduleNumberInFunction
);
const scheduleId = this.provider.naming.getScheduleId(functionName);
const scheduleTemplate = `
{
"Type": "AWS::Events::Rule",
"Properties": {
"ScheduleExpression": "${ScheduleExpression}",
"State": "${State}",
${Name ? `"Name": "${Name}",` : ''}
${Description ? `"Description": "${Description}",` : ''}
"Targets": [{
${Input ? `"Input": "${Input}",` : ''}
${InputPath ? `"InputPath": "${InputPath}",` : ''}
${InputTransformer ? `"InputTransformer": ${InputTransformer},` : ''}
"Arn": { "Fn::GetAtt": ["${lambdaLogicalId}", "Arn"] },
"Id": "${scheduleId}"
}]
}
}
`;
const permissionTemplate = `
{
"Type": "AWS::Lambda::Permission",
"Properties": {
"FunctionName": { "Fn::GetAtt": ["${lambdaLogicalId}", "Arn"] },
"Action": "lambda:InvokeFunction",
"Principal": "events.amazonaws.com",
"SourceArn": { "Fn::GetAtt": ["${scheduleLogicalId}", "Arn"] }
}
}
`;
const newScheduleObject = {
[scheduleLogicalId]: JSON.parse(scheduleTemplate),
};
const newPermissionObject = {
[lambdaPermissionLogicalId]: JSON.parse(permissionTemplate),
};
_.merge(
this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
newScheduleObject,
newPermissionObject
);
}
});
}
});
}
validateScheduleSyntax(input) {
return (
typeof input === 'string' && (rateSyntaxPattern.test(input) || cronSyntaxPattern.test(input))
);
}
formatInputTransformer(inputTransformer) {
if (!inputTransformer.inputTemplate) {
throw new this.serverless.classes.Error(
'The inputTemplate key is required when specifying an ' +
'inputTransformer for a schedule event'
);
}
const cfmOutput = {
// InputTemplate is required
InputTemplate: inputTransformer.inputTemplate,
};
// InputPathsMap is optional
if (inputTransformer.inputPathsMap) {
cfmOutput.InputPathsMap = inputTransformer.inputPathsMap;
}
return JSON.stringify(cfmOutput);
}
}
module.exports = AwsCompileScheduledEvents;