mirror of
https://github.com/serverless/serverless.git
synced 2026-01-25 15:07:39 +00:00
681 lines
25 KiB
JavaScript
681 lines
25 KiB
JavaScript
'use strict';
|
|
|
|
const AWS = require('aws-sdk');
|
|
const BbPromise = require('bluebird');
|
|
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.ensureTargetExecutionPermission = _.memoize(this.ensureTargetExecutionPermission);
|
|
if (
|
|
this.serverless.service.provider.versionFunctions === undefined ||
|
|
this.serverless.service.provider.versionFunctions === null
|
|
) {
|
|
this.serverless.service.provider.versionFunctions = true;
|
|
}
|
|
|
|
this.hooks = {
|
|
'package:compileFunctions': () =>
|
|
BbPromise.bind(this)
|
|
.then(this.downloadPackageArtifacts)
|
|
.then(this.compileFunctions),
|
|
};
|
|
}
|
|
|
|
compileRole(newFunction, role) {
|
|
const compiledFunction = newFunction;
|
|
const unsupportedRoleError = 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 = (compiledFunction.DependsOn || []).concat(
|
|
role['Fn::GetAtt'][0]
|
|
);
|
|
} else if ('Fn::ImportValue' in role) {
|
|
// role is an "Fn::ImportValue" object
|
|
compiledFunction.Properties.Role = role;
|
|
} else {
|
|
throw unsupportedRoleError;
|
|
}
|
|
break;
|
|
case 'string':
|
|
if (role.startsWith('arn:aws')) {
|
|
// role is a statically defined 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'] };
|
|
} else {
|
|
// role is a Logical Role Name
|
|
compiledFunction.Properties.Role = { 'Fn::GetAtt': [role, 'Arn'] };
|
|
compiledFunction.DependsOn = (compiledFunction.DependsOn || []).concat(role);
|
|
}
|
|
break;
|
|
default:
|
|
throw unsupportedRoleError;
|
|
}
|
|
}
|
|
|
|
downloadPackageArtifact(functionName) {
|
|
const { region } = this.options;
|
|
const S3 = new AWS.S3({ region });
|
|
|
|
const functionObject = this.serverless.service.getFunction(functionName);
|
|
const artifactFilePath =
|
|
_.get(functionObject, 'package.artifact') ||
|
|
_.get(this, 'serverless.service.package.artifact');
|
|
|
|
const regex = new RegExp('s3\\.amazonaws\\.com/(.+)/(.+)');
|
|
const match = artifactFilePath.match(regex);
|
|
|
|
if (match) {
|
|
return new BbPromise((resolve, reject) => {
|
|
const tmpDir = this.serverless.utils.getTmpDirPath();
|
|
const filePath = path.join(tmpDir, match[2]);
|
|
|
|
const readStream = S3.getObject({
|
|
Bucket: match[1],
|
|
Key: match[2],
|
|
}).createReadStream();
|
|
|
|
const writeStream = fs.createWriteStream(filePath);
|
|
|
|
readStream.on('error', error => reject(error));
|
|
readStream
|
|
.pipe(writeStream)
|
|
.on('error', reject)
|
|
.on('close', () => {
|
|
if (functionObject.package.artifact) {
|
|
functionObject.package.artifact = filePath;
|
|
} else {
|
|
this.serverless.service.package.artifact = filePath;
|
|
}
|
|
return resolve(filePath);
|
|
});
|
|
});
|
|
}
|
|
|
|
return BbPromise.resolve();
|
|
}
|
|
|
|
compileFunction(functionName) {
|
|
return BbPromise.try(() => {
|
|
const cfTemplate = this.serverless.service.provider.compiledCloudFormationTemplate;
|
|
const functionResource = 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) {
|
|
functionResource.Properties.Code.S3Bucket = this.serverless.service.package.deploymentBucket;
|
|
}
|
|
|
|
const s3Folder = this.serverless.service.package.artifactDirectoryName;
|
|
const s3FileName = artifactFilePath.split(path.sep).pop();
|
|
functionResource.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 = this.provider.getRuntime(functionObject.runtime);
|
|
|
|
functionResource.Properties.Handler = Handler;
|
|
functionResource.Properties.FunctionName = FunctionName;
|
|
functionResource.Properties.MemorySize = MemorySize;
|
|
functionResource.Properties.Timeout = Timeout;
|
|
functionResource.Properties.Runtime = Runtime;
|
|
|
|
// publish these properties to the platform
|
|
this.serverless.service.functions[functionName].memory = MemorySize;
|
|
this.serverless.service.functions[functionName].timeout = Timeout;
|
|
this.serverless.service.functions[functionName].runtime = Runtime;
|
|
|
|
if (functionObject.description) {
|
|
functionResource.Properties.Description = functionObject.description;
|
|
}
|
|
|
|
if (functionObject.condition) {
|
|
functionResource.Condition = functionObject.condition;
|
|
}
|
|
|
|
if (functionObject.dependsOn) {
|
|
functionResource.DependsOn = (functionResource.DependsOn || []).concat(
|
|
functionObject.dependsOn
|
|
);
|
|
}
|
|
|
|
if (functionObject.tags || this.serverless.service.provider.tags) {
|
|
const tags = Object.assign({}, this.serverless.service.provider.tags, functionObject.tags);
|
|
functionResource.Properties.Tags = [];
|
|
_.forEach(tags, (Value, Key) => {
|
|
functionResource.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 = cfTemplate.Resources.IamRoleLambdaExecution;
|
|
let stmt;
|
|
|
|
functionResource.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.isArnRefGetAttOrImportValue(arn)) {
|
|
functionResource.Properties.DeadLetterConfig = {
|
|
TargetArn: arn,
|
|
};
|
|
} else {
|
|
const errorMessage = [
|
|
'onError config must be provided as an arn string,',
|
|
' Ref, Fn::GetAtt 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 = cfTemplate.Resources.IamRoleLambdaExecution;
|
|
|
|
functionResource.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 if (this.isArnRefGetAttOrImportValue(arn)) {
|
|
functionResource.Properties.KmsKeyArn = arn;
|
|
} else {
|
|
const errorMessage = [
|
|
'awsKmsKeyArn config must be provided as an arn string,',
|
|
' Ref, Fn::GetAtt or Fn::ImportValue',
|
|
].join('');
|
|
throw new this.serverless.classes.Error(errorMessage);
|
|
}
|
|
}
|
|
|
|
const tracing =
|
|
functionObject.tracing ||
|
|
(this.serverless.service.provider.tracing &&
|
|
this.serverless.service.provider.tracing.lambda);
|
|
|
|
if (tracing) {
|
|
if (typeof tracing === 'boolean' || typeof tracing === 'string') {
|
|
let mode = tracing;
|
|
|
|
if (typeof tracing === 'boolean') {
|
|
mode = 'Active';
|
|
}
|
|
|
|
const iamRoleLambdaExecution = cfTemplate.Resources.IamRoleLambdaExecution;
|
|
|
|
functionResource.Properties.TracingConfig = {
|
|
Mode: mode,
|
|
};
|
|
|
|
const stmt = {
|
|
Effect: 'Allow',
|
|
Action: ['xray:PutTraceSegments', 'xray:PutTelemetryRecords'],
|
|
Resource: ['*'],
|
|
};
|
|
|
|
// 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 =
|
|
'tracing requires a boolean value or the "mode" provided as a string';
|
|
throw new this.serverless.classes.Error(errorMessage);
|
|
}
|
|
}
|
|
|
|
if (functionObject.environment || this.serverless.service.provider.environment) {
|
|
functionResource.Properties.Environment = {};
|
|
functionResource.Properties.Environment.Variables = Object.assign(
|
|
{},
|
|
this.serverless.service.provider.environment,
|
|
functionObject.environment
|
|
);
|
|
|
|
let invalidEnvVar = null;
|
|
Object.keys(functionResource.Properties.Environment.Variables).forEach(key => {
|
|
// taken from the bash man pages
|
|
if (!key.match(/^[A-Za-z_][a-zA-Z0-9_]*$/)) {
|
|
invalidEnvVar = `Invalid characters in environment variable ${key}`;
|
|
return false; // break loop with lodash
|
|
}
|
|
const value = functionResource.Properties.Environment.Variables[key];
|
|
if (_.isObject(value)) {
|
|
const isCFRef =
|
|
_.isObject(value) &&
|
|
!Object.keys(value).some(k => k !== 'Ref' && !k.startsWith('Fn::'));
|
|
if (!isCFRef) {
|
|
invalidEnvVar = `Environment variable ${key} must contain string`;
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
|
|
if (invalidEnvVar) throw new this.serverless.classes.Error(invalidEnvVar);
|
|
}
|
|
|
|
if ('role' in functionObject) {
|
|
this.compileRole(functionResource, functionObject.role);
|
|
} else if ('role' in this.serverless.service.provider) {
|
|
this.compileRole(functionResource, this.serverless.service.provider.role);
|
|
} else {
|
|
this.compileRole(functionResource, 'IamRoleLambdaExecution');
|
|
}
|
|
|
|
if (!functionObject.vpc) functionObject.vpc = {};
|
|
if (!this.serverless.service.provider.vpc) this.serverless.service.provider.vpc = {};
|
|
|
|
functionResource.Properties.VpcConfig = {
|
|
SecurityGroupIds:
|
|
functionObject.vpc.securityGroupIds ||
|
|
this.serverless.service.provider.vpc.securityGroupIds,
|
|
SubnetIds: functionObject.vpc.subnetIds || this.serverless.service.provider.vpc.subnetIds,
|
|
};
|
|
|
|
if (
|
|
!functionResource.Properties.VpcConfig.SecurityGroupIds ||
|
|
!functionResource.Properties.VpcConfig.SubnetIds
|
|
) {
|
|
delete functionResource.Properties.VpcConfig;
|
|
}
|
|
|
|
if (functionObject.reservedConcurrency || functionObject.reservedConcurrency === 0) {
|
|
// Try convert reservedConcurrency to integer
|
|
const reservedConcurrency = Number(functionObject.reservedConcurrency);
|
|
functionResource.Properties.ReservedConcurrentExecutions = reservedConcurrency;
|
|
}
|
|
|
|
if (!functionObject.disableLogs) {
|
|
functionResource.DependsOn = [
|
|
this.provider.naming.getLogGroupLogicalId(functionName),
|
|
].concat(functionResource.DependsOn || []);
|
|
}
|
|
|
|
if (functionObject.layers && Array.isArray(functionObject.layers)) {
|
|
functionResource.Properties.Layers = functionObject.layers;
|
|
} else if (
|
|
this.serverless.service.provider.layers &&
|
|
Array.isArray(this.serverless.service.provider.layers)
|
|
) {
|
|
functionResource.Properties.Layers = this.serverless.service.provider.layers;
|
|
}
|
|
|
|
const functionLogicalId = this.provider.naming.getLambdaLogicalId(functionName);
|
|
const newFunctionObject = {
|
|
[functionLogicalId]: functionResource,
|
|
};
|
|
|
|
Object.assign(cfTemplate.Resources, newFunctionObject);
|
|
|
|
const shouldVersionFunction =
|
|
functionObject.versionFunction != null
|
|
? functionObject.versionFunction
|
|
: this.serverless.service.provider.versionFunctions;
|
|
|
|
let versionCompilationPromise;
|
|
if (shouldVersionFunction || functionObject.provisionedConcurrency) {
|
|
// Create hashes for the artifact and the logical id of the version resource
|
|
// The one for the version resource must include the function configuration
|
|
// to make sure that a new version is created on configuration changes and
|
|
// not only on source changes.
|
|
const fileHash = crypto.createHash('sha256');
|
|
const versionHash = crypto.createHash('sha256');
|
|
fileHash.setEncoding('base64');
|
|
versionHash.setEncoding('base64');
|
|
|
|
// Read the file in chunks and add them to the hash (saves memory and performance)
|
|
versionCompilationPromise = BbPromise.fromCallback(cb => {
|
|
const readStream = fs.createReadStream(artifactFilePath);
|
|
|
|
readStream
|
|
.on('data', chunk => {
|
|
fileHash.write(chunk);
|
|
versionHash.write(chunk);
|
|
})
|
|
.on('close', () => {
|
|
cb();
|
|
})
|
|
.on('error', error => {
|
|
cb(error);
|
|
});
|
|
}).then(() => {
|
|
// Include function configuration in version id hash (without the Code part)
|
|
const properties = _.omit(_.get(functionResource, 'Properties', {}), 'Code');
|
|
_.forOwn(properties, value => {
|
|
if (_.isObject(value)) {
|
|
versionHash.write(JSON.stringify(value));
|
|
} else {
|
|
versionHash.write(value != null ? String(value) : '');
|
|
}
|
|
});
|
|
|
|
// Finalize hashes
|
|
fileHash.end();
|
|
versionHash.end();
|
|
|
|
const fileDigest = fileHash.read();
|
|
const versionDigest = versionHash.read();
|
|
|
|
const versionResource = this.cfLambdaVersionTemplate();
|
|
|
|
versionResource.Properties.CodeSha256 = fileDigest;
|
|
versionResource.Properties.FunctionName = { Ref: functionLogicalId };
|
|
if (functionObject.description) {
|
|
versionResource.Properties.Description = functionObject.description;
|
|
}
|
|
|
|
// use the version 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,
|
|
versionDigest
|
|
);
|
|
functionObject.versionLogicalId = versionLogicalId;
|
|
const newVersionObject = {
|
|
[versionLogicalId]: versionResource,
|
|
};
|
|
|
|
Object.assign(cfTemplate.Resources, newVersionObject);
|
|
|
|
// Add function versions to Outputs section
|
|
const functionVersionOutputLogicalId = this.provider.naming.getLambdaVersionOutputLogicalId(
|
|
functionName
|
|
);
|
|
const newVersionOutput = this.cfOutputLatestVersionTemplate();
|
|
|
|
newVersionOutput.Value = { Ref: versionLogicalId };
|
|
|
|
Object.assign(cfTemplate.Outputs, {
|
|
[functionVersionOutputLogicalId]: newVersionOutput,
|
|
});
|
|
|
|
if (!functionObject.provisionedConcurrency) return;
|
|
if (!shouldVersionFunction) delete versionResource.DeletionPolicy;
|
|
|
|
const provisionedConcurrency = Number(functionObject.provisionedConcurrency);
|
|
const aliasLogicalId = this.provider.naming.getLambdaProvisionedConcurrencyAliasLogicalId(
|
|
functionName
|
|
);
|
|
const aliasName = this.provider.naming.getLambdaProvisionedConcurrencyAliasName();
|
|
|
|
functionObject.targetAlias = { name: aliasName, logicalId: aliasLogicalId };
|
|
|
|
const aliasResource = {
|
|
Type: 'AWS::Lambda::Alias',
|
|
Properties: {
|
|
FunctionName: { Ref: functionLogicalId },
|
|
FunctionVersion: { 'Fn::GetAtt': [versionLogicalId, 'Version'] },
|
|
Name: aliasName,
|
|
ProvisionedConcurrencyConfig: {
|
|
ProvisionedConcurrentExecutions: provisionedConcurrency,
|
|
},
|
|
},
|
|
DependsOn: functionLogicalId,
|
|
};
|
|
|
|
cfTemplate.Resources[aliasLogicalId] = aliasResource;
|
|
});
|
|
} else {
|
|
versionCompilationPromise = BbPromise.resolve();
|
|
}
|
|
|
|
return versionCompilationPromise.then(() => this.compileFunctionDestinations(functionName));
|
|
});
|
|
}
|
|
|
|
compileFunctionDestinations(functionName) {
|
|
const functionObject = this.serverless.service.getFunction(functionName);
|
|
const { destinations } = functionObject;
|
|
if (!destinations) return;
|
|
|
|
const cfResources = this.serverless.service.provider.compiledCloudFormationTemplate.Resources;
|
|
const functionLogicalId = this.provider.naming.getLambdaLogicalId(functionName);
|
|
const resource = {
|
|
Type: 'AWS::Lambda::EventInvokeConfig',
|
|
Properties: {
|
|
FunctionName: { Ref: functionLogicalId },
|
|
DestinationConfig: {},
|
|
MaximumEventAgeInSeconds: destinations.maximumEventAge,
|
|
MaximumRetryAttempts: destinations.maximumRetryAttempts,
|
|
Qualifier: functionObject.targetAlias ? functionObject.targetAlias.name : '$LATEST',
|
|
},
|
|
};
|
|
const destinationConfig = resource.Properties.DestinationConfig;
|
|
|
|
const hasAccessPoliciesHandledExternally = Boolean(
|
|
functionObject.role || this.serverless.service.provider.role
|
|
);
|
|
if (destinations.onSuccess) {
|
|
if (!hasAccessPoliciesHandledExternally) {
|
|
this.ensureTargetExecutionPermission(destinations.onSuccess);
|
|
}
|
|
destinationConfig.OnSuccess = {
|
|
Destination: destinations.onSuccess.startsWith('arn:')
|
|
? destinations.onSuccess
|
|
: this.provider.resolveFunctionArn(destinations.onSuccess),
|
|
};
|
|
}
|
|
if (destinations.onFailure) {
|
|
if (!hasAccessPoliciesHandledExternally) {
|
|
this.ensureTargetExecutionPermission(destinations.onFailure);
|
|
}
|
|
destinationConfig.OnFailure = {
|
|
Destination: destinations.onFailure.startsWith('arn:')
|
|
? destinations.onFailure
|
|
: this.provider.resolveFunctionArn(destinations.onFailure),
|
|
};
|
|
}
|
|
|
|
cfResources[this.provider.naming.getLambdaEventConfigLogicalId(functionName)] = resource;
|
|
}
|
|
|
|
// Memoized in a construtor
|
|
ensureTargetExecutionPermission(functionAddress) {
|
|
const iamPolicyStatements = this.serverless.service.provider.compiledCloudFormationTemplate
|
|
.Resources.IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement;
|
|
|
|
const action = (() => {
|
|
if (!functionAddress.startsWith('arn:') || functionAddress.includes(':function:')) {
|
|
return 'lambda:InvokeFunction';
|
|
}
|
|
if (functionAddress.includes(':sqs:')) return 'sqs:SendMessage';
|
|
if (functionAddress.includes(':sns:')) return 'sns:Publish';
|
|
if (functionAddress.includes(':event-bus/')) return 'events:PutEvents';
|
|
throw new this.serverless.classes.Error(`Unsupported destination target ${functionAddress}`);
|
|
})();
|
|
iamPolicyStatements.push({
|
|
Effect: 'Allow',
|
|
Action: action,
|
|
// Note: Cannot address function via { 'Fn::GetAtt': [targetLogicalId, 'Arn'] }
|
|
// as same IAM settings are used for target function and that will introduce
|
|
// circular dependency error. Relying on Fn::Sub as a workaround
|
|
Resource: functionAddress.startsWith('arn:')
|
|
? functionAddress
|
|
: {
|
|
'Fn::Sub': `arn:\${AWS::Partition}:lambda:\${AWS::Region}:\${AWS::AccountId}:function:${
|
|
this.serverless.service.getFunction(functionAddress).name
|
|
}`,
|
|
},
|
|
});
|
|
}
|
|
|
|
downloadPackageArtifacts() {
|
|
const allFunctions = this.serverless.service.getAllFunctions();
|
|
return BbPromise.each(allFunctions, functionName => this.downloadPackageArtifact(functionName));
|
|
}
|
|
|
|
compileFunctions() {
|
|
const allFunctions = this.serverless.service.getAllFunctions();
|
|
return BbPromise.each(allFunctions, functionName => this.compileFunction(functionName));
|
|
}
|
|
|
|
// helper functions
|
|
isArnRefGetAttOrImportValue(arn) {
|
|
return (
|
|
typeof arn === 'object' &&
|
|
Object.keys(arn).some(k => ['Ref', 'Fn::GetAtt', 'Fn::ImportValue'].includes(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;
|