diff --git a/docs/README.md b/docs/README.md index 21d9c5c33..dd1d645b3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -27,6 +27,8 @@ Serverless provides (such as service creation, deployment, removal, function inv - AWS plugins - [awsCompileFunctions](/docs/plugins/aws/awsCompileFunctions.md) - Compiles the functions to CloudFormation resources - [awsCompileS3Events](/docs/plugins/aws/awsCompileS3Events.md) - Compiles the S3 events to CloudFormation resources + - [awsCompileScheduledEvents](/docs/plugins/aws/awsCompileScheduledEvents.md) - Compiles the Scheduled events to + CloudFormation resources - [awsDeploy](/docs/plugins/aws/awsDeploy.md) - Deploys the Serverless service to AWS - [awsInvoke](/docs/plugins/aws/awsInvoke.md) - Invokes a AWS lambda function - [awsRemove](/docs/plugins/aws/awsRemove.md) - Removes the service with all it's resources from AWS diff --git a/docs/plugins/aws/awsCompileScheduledEvents.md b/docs/plugins/aws/awsCompileScheduledEvents.md new file mode 100644 index 000000000..b88bdf6af --- /dev/null +++ b/docs/plugins/aws/awsCompileScheduledEvents.md @@ -0,0 +1,13 @@ +# awsCompileScheduledEvents + +This plugins compiles the function schedule event to to a CloudFormation resource. + +## How it works + +`awsCompileScheduledEvents` hooks into the [`deploy:compileEvents`](/docs/plugins/core/deploy.md) hook. + +It loops over all functions which are defined in `serverless.yaml`. For each function that has a schedule event defined, a CloudWatch schedule event rule will be created with a status of "enabled" and targeting the lambda function the event is defined within. + +Furthermore a lambda permission for the current function is created which makes is possible to invoke the function at the specified schedule. + +Those two resources are then merged into the `serverless.service.resources.aws.Resources` section. diff --git a/lib/plugins/Plugins.json b/lib/plugins/Plugins.json index 298e6e61f..2e44496c4 100644 --- a/lib/plugins/Plugins.json +++ b/lib/plugins/Plugins.json @@ -6,6 +6,7 @@ "./remove/remove.js", "./awsCompileFunctions/awsCompileFunctions.js", "./awsCompileS3Events/awsCompileS3Events.js", + "./awsCompileScheduledEvents/awsCompileScheduledEvents.js", "./awsDeploy/awsDeploy.js", "./awsInvoke/awsInvoke.js", "./awsRemove/awsRemove.js", diff --git a/lib/plugins/awsCompileScheduledEvents/awsCompileScheduledEvents.js b/lib/plugins/awsCompileScheduledEvents/awsCompileScheduledEvents.js new file mode 100644 index 000000000..ba8436a6b --- /dev/null +++ b/lib/plugins/awsCompileScheduledEvents/awsCompileScheduledEvents.js @@ -0,0 +1,67 @@ +'use strict'; + +const merge = require('lodash').merge; + +class AwsCompileScheduledEvents { + constructor(serverless) { + this.serverless = serverless; + + this.hooks = { + 'deploy:compileEvents': this.compileScheduledEvents.bind(this), + }; + } + + compileScheduledEvents() { + if (!this.serverless.service.resources.aws.Resources) { + throw new this.serverless.Error( + 'This plugin needs access to Resources section of the AWS CloudFormation template'); + } + + this.serverless.service.getAllFunctions().forEach((functionName) => { + const functionObj = this.serverless.service.getFunction(functionName); + + // checking all three levels in the obj tree + // to avoid "can't read property of undefined" error + if (functionObj.events && functionObj.events.aws && functionObj.events.aws.schedule) { + const scheduleTemplate = ` + { + "Type": "AWS::Events::Rule", + "Properties": { + "ScheduleExpression": "${functionObj.events.aws.schedule}", + "State": "ENABLED", + "Targets": [{ + "Arn": { "Fn::GetAtt": ["${functionName}", "Arn"] }, + "Id": "${functionName}ScheduleEvent" + }] + } + } + `; + + const permissionTemplate = ` + { + "Type": "AWS::Lambda::Permission", + "Properties": { + "FunctionName": { "Fn::GetAtt": ["${functionName}", "Arn"] }, + "Action": "lambda:InvokeFunction", + "Principal": "events.amazonaws.com", + "SourceArn": { "Fn::GetAtt": ["${functionName}ScheduleEvent", "Arn"] } + } + } + `; + + const newScheduleObject = { + [`${functionName}ScheduleEvent`]: JSON.parse(scheduleTemplate), + }; + + const newPermissionObject = { + [`${functionName}ScheduleEventPermission`]: JSON.parse(permissionTemplate), + }; + + merge(this.serverless.service.resources.aws.Resources, + newScheduleObject, newPermissionObject); + } + }); + } +} + +module.exports = AwsCompileScheduledEvents; diff --git a/lib/plugins/awsCompileScheduledEvents/tests/awsCompileScheduledEvents.js b/lib/plugins/awsCompileScheduledEvents/tests/awsCompileScheduledEvents.js new file mode 100644 index 000000000..eccfd554e --- /dev/null +++ b/lib/plugins/awsCompileScheduledEvents/tests/awsCompileScheduledEvents.js @@ -0,0 +1,72 @@ +'use strict'; + +const expect = require('chai').expect; +const AwsCompileScheduledEvents = require('../awsCompileScheduledEvents'); +const Serverless = require('../../../Serverless'); + +describe('awsCompileScheduledEvents', () => { + let serverless; + let awsCompileScheduledEvents; + + beforeEach(() => { + serverless = new Serverless(); + serverless.init(); + serverless.service.resources = { aws: { Resources: {} } }; + awsCompileScheduledEvents = new AwsCompileScheduledEvents(serverless); + awsCompileScheduledEvents.serverless.service.service = 'new-service'; + }); + + describe('#compileScheduledEvents()', () => { + it('should throw an error if the aws resource is not available', () => { + awsCompileScheduledEvents.serverless.service.resources.aws.Resources = false; + expect(() => awsCompileScheduledEvents.compileScheduledEvents()).to.throw(Error); + }); + + it('should compile scheduled events into CF resources', () => { + awsCompileScheduledEvents.serverless.service.functions = { + hello: { + events: { + aws: { + schedule: 'rate(10 minutes)', + }, + }, + }, + }; + + const scheduleResrouce = ` + { + "Type": "AWS::Events::Rule", + "Properties": { + "ScheduleExpression": "rate(10 minutes)", + "State": "ENABLED", + "Targets": [{ + "Arn": { "Fn::GetAtt": ["hello", "Arn"] }, + "Id": "helloScheduleEvent" + }] + } + } + `; + + const permissionResource = ` + { + "Type": "AWS::Lambda::Permission", + "Properties": { + "FunctionName": { "Fn::GetAtt": ["hello", "Arn"] }, + "Action": "lambda:InvokeFunction", + "Principal": "events.amazonaws.com", + "SourceArn": { "Fn::GetAtt": ["helloScheduleEvent", "Arn"] } + } + } + `; + + awsCompileScheduledEvents.compileScheduledEvents(); + + expect(awsCompileScheduledEvents.serverless.service + .resources.aws.Resources.helloScheduleEvent) + .to.deep.equal(JSON.parse(scheduleResrouce)); + expect(awsCompileScheduledEvents.serverless.service + .resources.aws.Resources.helloScheduleEventPermission) + .to.deep.equal(JSON.parse(permissionResource)); + }); + }); +}); diff --git a/lib/templates/serverless.yaml b/lib/templates/serverless.yaml index 887895053..f8f0d7c1b 100644 --- a/lib/templates/serverless.yaml +++ b/lib/templates/serverless.yaml @@ -32,7 +32,7 @@ functions: # if this gets too big, you can always use JSON-REF # - first-bucket http_endpoint: post: users/create - scheduled: 5 * * * * + # schedule: rate(10 minutes) azure: http_endpoint: direction: in diff --git a/tests/all.js b/tests/all.js index b6950e4fe..c5ca0c1f8 100644 --- a/tests/all.js +++ b/tests/all.js @@ -22,3 +22,4 @@ require('../lib/plugins/awsRemove/tests/all'); require('../lib/plugins/awsInvoke/tests/awsInvoke'); require('../lib/plugins/awsCompileFunctions/tests/awsCompileFunctions'); require('../lib/plugins/awsCompileS3Events/tests/awsCompileS3Events'); +require('../lib/plugins/awsCompileScheduledEvents/tests/awsCompileScheduledEvents'); diff --git a/tests/integration_test.js b/tests/integration_test.js index 7a0aa5170..08456bf8e 100644 --- a/tests/integration_test.js +++ b/tests/integration_test.js @@ -31,7 +31,7 @@ describe('Service Lifecyle Integration Test', () => { stageName } --region ${ regionName - }`); + }`, { stdio: 'inherit' }); process.chdir(path.join(tmpDir, serviceName)); expect(serverless.utils @@ -50,7 +50,7 @@ describe('Service Lifecyle Integration Test', () => { stageName } --region ${ regionName - }`); + }`, { stdio: 'inherit' }); return CF.describeStacksPromised({ StackName: `${serviceName}-${stageName}` }) .then(d => expect(d.Stacks[0].StackStatus).to.be.equal('UPDATE_COMPLETE')); @@ -83,7 +83,7 @@ describe('Service Lifecyle Integration Test', () => { stageName } --region ${ regionName - }`); + }`, { stdio: 'inherit' }); }); it('should invoke updated function from aws', function () { @@ -103,7 +103,7 @@ describe('Service Lifecyle Integration Test', () => { stageName } --region ${ regionName - }`); + }`, { stdio: 'inherit' }); return CF.describeStacksPromised({ StackName: `${serviceName}-${stageName}` }) .then(d => expect(d.Stacks[0].StackStatus).to.be.equal('DELETE_COMPLETE'))