'use strict'; const crypto = require('crypto'); const fs = require('fs'); const _ = require('lodash'); const path = require('path'); class AwsCompileFunctions { constructor(serverless, options) { this.serverless = serverless; this.options = options; const servicePath = this.serverless.config.servicePath || ''; this.packagePath = this.serverless.service.package.path || path.join(servicePath || '.', '.serverless'); this.provider = this.serverless.getProvider('aws'); this.compileFunctions = this.compileFunctions.bind(this); this.compileFunction = this.compileFunction.bind(this); if (this.serverless.service.provider.versionFunctions === undefined || this.serverless.service.provider.versionFunctions === null) { this.serverless.service.provider.versionFunctions = true; } this.hooks = { 'package:compileFunctions': this.compileFunctions, }; } compileRole(newFunction, role) { const compiledFunction = newFunction; const unnsupportedRoleError = new this.serverless.classes .Error(`Unsupported role provided: "${JSON.stringify(role)}"`); switch (typeof role) { case 'object': if ('Fn::GetAtt' in role) { // role is an "Fn::GetAtt" object compiledFunction.Properties.Role = role; compiledFunction.DependsOn = [role['Fn::GetAtt'][0]]; } else if ('Fn::ImportValue' in role) { // role is an "Fn::ImportValue" object compiledFunction.Properties.Role = role; } else { throw unnsupportedRoleError; } break; case 'string': if (role.startsWith('arn:aws')) { // role is a statically definied iam arn compiledFunction.Properties.Role = role; } else if (role === 'IamRoleLambdaExecution') { // role is the default role generated by the framework compiledFunction.Properties.Role = { 'Fn::GetAtt': [role, 'Arn'] }; compiledFunction.DependsOn = [ 'IamRoleLambdaExecution', ]; } else { // role is a Logical Role Name compiledFunction.Properties.Role = { 'Fn::GetAtt': [role, 'Arn'] }; compiledFunction.DependsOn = [role]; } break; default: throw unnsupportedRoleError; } } compileFunction(functionName) { const newFunction = this.cfLambdaFunctionTemplate(); const functionObject = this.serverless.service.getFunction(functionName); functionObject.package = functionObject.package || {}; const serviceArtifactFileName = this.provider.naming.getServiceArtifactName(); const functionArtifactFileName = this.provider.naming.getFunctionArtifactName(functionName); let artifactFilePath = functionObject.package.artifact || this.serverless.service.package.artifact; if (!artifactFilePath || (this.serverless.service.artifact && !functionObject.package.artifact)) { let artifactFileName = serviceArtifactFileName; if (this.serverless.service.package.individually || functionObject.package.individually) { artifactFileName = functionArtifactFileName; } artifactFilePath = path.join(this.serverless.config.servicePath , '.serverless', artifactFileName); } if (this.serverless.service.package.deploymentBucket) { newFunction.Properties.Code.S3Bucket = this.serverless.service.package.deploymentBucket; } const s3Folder = this.serverless.service.package.artifactDirectoryName; const s3FileName = artifactFilePath.split(path.sep).pop(); newFunction.Properties.Code.S3Key = `${s3Folder}/${s3FileName}`; if (!functionObject.handler) { const errorMessage = [ `Missing "handler" property in function "${functionName}".`, ' Please make sure you point to the correct lambda handler.', ' For example: handler.hello.', ' Please check the docs for more info', ].join(''); throw new this.serverless.classes .Error(errorMessage); } const Handler = functionObject.handler; const FunctionName = functionObject.name; const MemorySize = Number(functionObject.memorySize) || Number(this.serverless.service.provider.memorySize) || 1024; const Timeout = Number(functionObject.timeout) || Number(this.serverless.service.provider.timeout) || 6; const Runtime = functionObject.runtime || this.serverless.service.provider.runtime || 'nodejs4.3'; newFunction.Properties.Handler = Handler; newFunction.Properties.FunctionName = FunctionName; newFunction.Properties.MemorySize = MemorySize; newFunction.Properties.Timeout = Timeout; newFunction.Properties.Runtime = Runtime; if (functionObject.description) { newFunction.Properties.Description = functionObject.description; } if (functionObject.tags && typeof functionObject.tags === 'object') { newFunction.Properties.Tags = []; _.forEach(functionObject.tags, (Value, Key) => { newFunction.Properties.Tags.push({ Key, Value }); }); } if (functionObject.onError) { const arn = functionObject.onError; if (typeof arn === 'string') { const splittedArn = arn.split(':'); if (splittedArn[0] === 'arn' && (splittedArn[2] === 'sns' || splittedArn[2] === 'sqs')) { const dlqType = splittedArn[2]; const iamRoleLambdaExecution = this.serverless.service.provider .compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution; let stmt; newFunction.Properties.DeadLetterConfig = { TargetArn: arn, }; if (dlqType === 'sns') { stmt = { Effect: 'Allow', Action: [ 'sns:Publish', ], Resource: [arn], }; } else if (dlqType === 'sqs') { const errorMessage = [ 'onError currently only supports SNS topic arns due to a', ' race condition when using SQS queue arns and updating the IAM role.', ' Please check the docs for more info.', ].join(''); throw new this.serverless.classes.Error(errorMessage); } // update the PolicyDocument statements (if default policy is used) if (iamRoleLambdaExecution) { iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement.push(stmt); } } else { const errorMessage = 'onError config must be a SNS topic arn or SQS queue arn'; throw new this.serverless.classes.Error(errorMessage); } } else if (this.isArnRefOrImportValue(arn)) { newFunction.Properties.DeadLetterConfig = { TargetArn: arn, }; } else { const errorMessage = [ 'onError config must be provided as an arn string,', ' Ref or Fn::ImportValue', ].join(''); throw new this.serverless.classes.Error(errorMessage); } } let kmsKeyArn; const serviceObj = this.serverless.service.serviceObject; if ('awsKmsKeyArn' in functionObject) { kmsKeyArn = functionObject.awsKmsKeyArn; } else if (serviceObj && 'awsKmsKeyArn' in serviceObj) { kmsKeyArn = serviceObj.awsKmsKeyArn; } if (kmsKeyArn) { const arn = kmsKeyArn; if (typeof arn === 'string') { const splittedArn = arn.split(':'); if (splittedArn[0] === 'arn' && (splittedArn[2] === 'kms')) { const iamRoleLambdaExecution = this.serverless.service.provider .compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution; newFunction.Properties.KmsKeyArn = arn; const stmt = { Effect: 'Allow', Action: [ 'kms:Decrypt', ], Resource: [arn], }; // update the PolicyDocument statements (if default policy is used) if (iamRoleLambdaExecution) { iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement = _.unionWith( iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement, [stmt], _.isEqual ); } } else { const errorMessage = 'awsKmsKeyArn config must be a KMS key arn'; throw new this.serverless.classes.Error(errorMessage); } } else { const errorMessage = 'awsKmsKeyArn config must be provided as a string'; throw new this.serverless.classes.Error(errorMessage); } } if (functionObject.environment || this.serverless.service.provider.environment) { newFunction.Properties.Environment = {}; newFunction.Properties.Environment.Variables = Object.assign( {}, this.serverless.service.provider.environment, functionObject.environment ); Object.keys(newFunction.Properties.Environment.Variables).forEach((key) => { // taken from the bash man pages if (!key.match(/^[A-Za-z_][a-zA-Z0-9_]*$/)) { const errorMessage = 'Invalid characters in environment variable'; throw new this.serverless.classes.Error(errorMessage); } }); } if ('role' in functionObject) { this.compileRole(newFunction, functionObject.role); } else if ('role' in this.serverless.service.provider) { this.compileRole(newFunction, this.serverless.service.provider.role); } else { this.compileRole(newFunction, 'IamRoleLambdaExecution'); } if (!functionObject.vpc) functionObject.vpc = {}; if (!this.serverless.service.provider.vpc) this.serverless.service.provider.vpc = {}; newFunction.Properties.VpcConfig = { SecurityGroupIds: functionObject.vpc.securityGroupIds || this.serverless.service.provider.vpc.securityGroupIds, SubnetIds: functionObject.vpc.subnetIds || this.serverless.service.provider.vpc.subnetIds, }; if (!newFunction.Properties.VpcConfig.SecurityGroupIds || !newFunction.Properties.VpcConfig.SubnetIds) { delete newFunction.Properties.VpcConfig; } newFunction.DependsOn = [this.provider.naming.getLogGroupLogicalId(functionName)] .concat(newFunction.DependsOn || []); const functionLogicalId = this.provider.naming .getLambdaLogicalId(functionName); const newFunctionObject = { [functionLogicalId]: newFunction, }; _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, newFunctionObject); const newVersion = this.cfLambdaVersionTemplate(); const content = fs.readFileSync(artifactFilePath); const hash = crypto.createHash('sha256'); hash.setEncoding('base64'); hash.write(content); hash.end(); newVersion.Properties.CodeSha256 = hash.read(); newVersion.Properties.FunctionName = { Ref: functionLogicalId }; if (functionObject.description) { newVersion.Properties.Description = functionObject.description; } // use the SHA in the logical resource ID of the version because // AWS::Lambda::Version resource will not support updates const versionLogicalId = this.provider.naming.getLambdaVersionLogicalId( functionName, newVersion.Properties.CodeSha256); const newVersionObject = { [versionLogicalId]: newVersion, }; if (this.serverless.service.provider.versionFunctions) { _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, newVersionObject); } // Add function versions to Outputs section const functionVersionOutputLogicalId = this.provider.naming .getLambdaVersionOutputLogicalId(functionName); const newVersionOutput = this.cfOutputLatestVersionTemplate(); newVersionOutput.Value = { Ref: versionLogicalId }; if (this.serverless.service.provider.versionFunctions) { _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Outputs, { [functionVersionOutputLogicalId]: newVersionOutput, }); } } compileFunctions() { this.serverless.service .getAllFunctions() .forEach((functionName) => this.compileFunction(functionName)); } // helper functions isArnRefOrImportValue(arn) { return typeof arn === 'object' && _.some(_.keys(arn), (k) => _.includes(['Ref', 'Fn::ImportValue'], k)); } cfLambdaFunctionTemplate() { return { Type: 'AWS::Lambda::Function', Properties: { Code: { S3Bucket: { Ref: 'ServerlessDeploymentBucket', }, S3Key: 'S3Key', }, FunctionName: 'FunctionName', Handler: 'Handler', MemorySize: 'MemorySize', Role: 'Role', Runtime: 'Runtime', Timeout: 'Timeout', }, }; } cfLambdaVersionTemplate() { return { Type: 'AWS::Lambda::Version', // Retain old versions even though they will not be in future // CloudFormation stacks. On stack delete, these will be removed when // their associated function is removed. DeletionPolicy: 'Retain', Properties: { FunctionName: 'FunctionName', CodeSha256: 'CodeSha256', }, }; } cfOutputLatestVersionTemplate() { return { Description: 'Current Lambda function version', Value: 'Value', }; } } module.exports = AwsCompileFunctions;