mirror of
https://github.com/serverless/serverless.git
synced 2026-01-18 14:58:43 +00:00
372 lines
13 KiB
JavaScript
372 lines
13 KiB
JavaScript
'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 {
|
|
const errorMessage = 'onError config must be provided as a string';
|
|
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
|
|
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;
|