UNPKG

osls

Version:

Open-source alternative to Serverless Framework

352 lines (314 loc) 12.6 kB
'use strict'; const ServerlessError = require('../../../../../serverless-error'); const resolveLambdaTarget = require('../../../utils/resolve-lambda-target'); const { log, style } = require('@serverless/utils/log'); const rateSyntax = '^rate\\((?:1 (?:minute|hour|day)|(?:1\\d+|[2-9]\\d*) (?:minute|hour|day)s)\\)$'; const cronSyntax = '^cron\\(\\S+ \\S+ \\S+ \\S+ \\S+ \\S+\\)$'; const scheduleSyntax = `${rateSyntax}|${cronSyntax}`; const METHOD_SCHEDULER = 'scheduler'; const METHOD_EVENT_BUS = 'eventBus'; class AwsCompileScheduledEvents { constructor(serverless) { this.serverless = serverless; this.provider = this.serverless.getProvider('aws'); this.hooks = { 'package:compileEvents': async () => this.compileScheduledEvents(), }; this.serverless.configSchemaHandler.defineFunctionEvent('aws', 'schedule', { anyOf: [ { type: 'string', pattern: scheduleSyntax }, { type: 'object', properties: { rate: { type: 'array', minItems: 1, items: { anyOf: [ { $ref: '#/definitions/awsCfFunction' }, { type: 'string', pattern: scheduleSyntax, }, ], }, }, enabled: { type: 'boolean' }, name: { type: 'string', minLength: 1, maxLength: 64, pattern: '[\\.\\-_A-Za-z0-9]+' }, description: { type: 'string', maxLength: 512 }, input: { anyOf: [ { type: 'string', maxLength: 8192 }, { type: 'object', oneOf: [ { properties: { body: { type: 'string', maxLength: 8192 }, }, required: ['body'], additionalProperties: false, }, { not: { required: ['body'], }, }, ], }, ], }, inputPath: { type: 'string', maxLength: 256 }, inputTransformer: { type: 'object', properties: { inputTemplate: { type: 'string', minLength: 1, maxLength: 8192, }, inputPathsMap: { type: 'object' }, }, required: ['inputTemplate'], additionalProperties: false, }, method: { type: 'string', enum: [METHOD_EVENT_BUS, METHOD_SCHEDULER], }, timezone: { type: 'string', pattern: '[\\w\\-\\/]+', }, }, required: ['rate'], additionalProperties: false, }, ], }); } compileScheduledEvents() { const schedulerStatement = { Effect: 'Allow', Action: ['lambda:InvokeFunction'], Resource: [], }; const resources = this.serverless.service.provider.compiledCloudFormationTemplate.Resources; let hasSchedulerEvents = false; this.serverless.service.getAllFunctions().forEach((functionName) => { const functionObj = this.serverless.service.getFunction(functionName); let scheduleNumberInFunction = 0; let functionHasSchedulerEvent = false; if (functionObj.events) { functionObj.events.forEach((event) => { if (event.schedule) { let ScheduleExpressions; let State; let Input; let InputPath; let InputTransformer; let Name; let Description; let method; let roleArn; let timezone; if (typeof event.schedule === 'object') { ScheduleExpressions = 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; timezone = event.schedule.timezone; Description = event.schedule.description; const functionLogicalId = this.provider.naming.getLambdaLogicalId(functionName); const functionResource = resources[functionLogicalId]; roleArn = functionResource.Properties.Role; method = event.schedule.method || METHOD_EVENT_BUS; if (ScheduleExpressions.length > 1 && Name) { throw new ServerlessError( 'You cannot specify a name when defining more than one rate expression', 'SCHEDULE_NAME_NOT_ALLOWED_MULTIPLE_RATES' ); } if (Input && typeof Input === 'object') { if (typeof Input.body === 'string') { Input.body = JSON.parse(Input.body); } Input = JSON.stringify(Input); } if (Input && typeof Input === 'string' && method !== METHOD_SCHEDULER) { // escape quotes to favor JSON.parse Input = Input.replace(/"/g, '\\"'); } if (InputTransformer) { if (method === METHOD_SCHEDULER) { throw new ServerlessError( 'Cannot setup "schedule" event: "inputTransformer" is not supported with "scheduler" mode', 'SCHEDULE_PARAMETER_NOT_SUPPORTED' ); } else { InputTransformer = this.formatInputTransformer(InputTransformer); } } if (InputPath && method === METHOD_SCHEDULER) { throw new ServerlessError( 'Cannot setup "schedule" event: "inputPath" is not supported with "scheduler" mode', 'SCHEDULE_PARAMETER_NOT_SUPPORTED' ); } if (timezone && method !== METHOD_SCHEDULER) { throw new ServerlessError( 'Cannot setup "schedule" event: "timezone" is only supported with "scheduler" mode', 'SCHEDULE_PARAMETER_NOT_SUPPORTED' ); } } else { ScheduleExpressions = [event.schedule]; State = 'ENABLED'; } const lambdaTarget = resolveLambdaTarget(functionName, functionObj); const lambdaTargetJson = JSON.stringify(lambdaTarget); const dependsOn = functionObj && functionObj.targetAlias ? functionObj.targetAlias.logicalId : undefined; const scheduleId = this.provider.naming.getScheduleId(functionName); for (const ScheduleExpression of ScheduleExpressions) { scheduleNumberInFunction++; if (method === METHOD_SCHEDULER) { hasSchedulerEvents = true; functionHasSchedulerEvent = true; const scheduleLogicalId = this.provider.naming.getSchedulerScheduleLogicalId( functionName, scheduleNumberInFunction ); resources[scheduleLogicalId] = { Type: 'AWS::Scheduler::Schedule', DependsOn: dependsOn, Properties: { ScheduleExpression, State, Target: { Arn: lambdaTarget, RoleArn: roleArn, Input, }, FlexibleTimeWindow: { Mode: 'OFF', }, Name, Description, ScheduleExpressionTimezone: timezone, }, }; } else { const scheduleLogicalId = this.provider.naming.getScheduleLogicalId( functionName, scheduleNumberInFunction ); const lambdaPermissionLogicalId = this.provider.naming.getLambdaSchedulePermissionLogicalId( functionName, scheduleNumberInFunction ); let templateScheduleExpression; if (typeof ScheduleExpression === 'string') { templateScheduleExpression = `"${ScheduleExpression}"`; } else { templateScheduleExpression = JSON.stringify(ScheduleExpression); } const scheduleTemplate = ` { "Type": "AWS::Events::Rule", ${dependsOn ? `"DependsOn": "${dependsOn}",` : ''} "Properties": { "ScheduleExpression": ${templateScheduleExpression}, "State": "${State}", ${Name ? `"Name": "${Name}",` : ''} ${Description ? `"Description": "${Description}",` : ''} "Targets": [{ ${Input ? `"Input": "${Input}",` : ''} ${InputPath ? `"InputPath": "${InputPath}",` : ''} ${InputTransformer ? `"InputTransformer": ${InputTransformer},` : ''} "Arn": ${lambdaTargetJson}, "Id": "${scheduleId}" }] } } `; const permissionTemplate = ` { "Type": "AWS::Lambda::Permission", ${dependsOn ? `"DependsOn": "${dependsOn}",` : ''} "Properties": { "FunctionName": ${lambdaTargetJson}, "Action": "lambda:InvokeFunction", "Principal": "events.amazonaws.com", "SourceArn": { "Fn::GetAtt": ["${scheduleLogicalId}", "Arn"] } } } `; const newScheduleObject = { [scheduleLogicalId]: JSON.parse(scheduleTemplate), }; const newPermissionObject = { [lambdaPermissionLogicalId]: JSON.parse(permissionTemplate), }; Object.assign(resources, newScheduleObject, newPermissionObject); } } } }); } if (functionHasSchedulerEvent) { const functionArnWithVars = 'arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}' + `:function:${functionObj.name}`; schedulerStatement.Resource.push( { 'Fn::Sub': functionArnWithVars, }, { 'Fn::Sub': `${functionArnWithVars}:*`, } ); } }); if (hasSchedulerEvents) { if (!resources.IamRoleLambdaExecution) { log.info( `Remember to add required EventBridge Scheduler permissions to your execution role. Documentation: ${style.link( 'https://docs.aws.amazon.com/scheduler/latest/UserGuide/setting-up.html#setting-up-execution-role' )}` ); } else { const lambdaAssumeStatement = resources.IamRoleLambdaExecution.Properties.AssumeRolePolicyDocument.Statement.find( (statement) => statement.Principal.Service.includes('lambda.amazonaws.com') ); if (lambdaAssumeStatement) { lambdaAssumeStatement.Principal.Service.push('scheduler.amazonaws.com'); } const policyDocumentStatements = resources.IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement; policyDocumentStatements.push(schedulerStatement); } } } formatInputTransformer(inputTransformer) { 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;