2024-05-29 11:51:04 -04:00

1083 lines
35 KiB
JavaScript

import AWS from '../../../../aws/sdk-v2.js'
import crypto from 'crypto'
import fs from 'fs'
import _ from 'lodash'
import path from 'path'
import ServerlessError from '../../../../serverless-error.js'
import deepSortObjectByKey from '../../../../utils/deep-sort-object-by-key.js'
import getHashForFilePath from '../lib/get-hash-for-file-path.js'
import resolveLambdaTarget from '../../utils/resolve-lambda-target.js'
import parseS3URI from '../../utils/parse-s3-uri.js'
import utils from '@serverlessinc/sf-core/src/utils.js'
const { log } = utils
const defaultCors = {
allowedOrigins: ['*'],
allowedHeaders: [
'Content-Type',
'X-Amz-Date',
'Authorization',
'X-Api-Key',
'X-Amz-Security-Token',
'X-Amzn-Trace-Id',
],
allowedMethods: ['*'],
}
const runtimeManagementMap = new Map([
['auto', 'Auto'],
['onFunctionUpdate', 'FunctionUpdate'],
['manual', 'Manual'],
])
class AwsCompileFunctions {
constructor(serverless, options) {
this.serverless = serverless
this.options = options
const serviceDir = this.serverless.serviceDir || ''
this.packagePath =
this.serverless.service.package.path ||
path.join(serviceDir || '.', '.serverless')
this.provider = this.serverless.getProvider('aws')
this.ensureTargetExecutionPermission = _.memoize(
this.ensureTargetExecutionPermission,
)
if (
this.serverless.service.provider.name === 'aws' &&
this.serverless.service.provider.versionFunctions == null
) {
this.serverless.service.provider.versionFunctions = true
}
this.hooks = {
initialize: () => {
if (
this.serverless.service.provider.lambdaHashingVersion ===
'20200924' &&
!this.options['enforce-hash-update']
) {
this.serverless._logDeprecation(
'LAMBDA_HASHING_VERSION_PROPERTY',
'Resolution of lambda version hashes with the "20200924" algorithm is deprecated.' +
' It is highly recommend to migrate to new default algorithm. Please see' +
' the deprecation documentation for more details about the migration process.',
)
}
if (
this.serverless.service.provider.lambdaHashingVersion === '20201221'
) {
this.serverless._logDeprecation(
'LAMBDA_HASHING_VERSION_PROPERTY',
'Setting "20201221" for "provider.lambdaHashingVersion" is no longer effective as' +
' new hashing algorithm is now used by default. You can safely remove this' +
' property from your configuration.',
)
}
},
'package:compileFunctions': async () =>
this.downloadPackageArtifacts().then(this.compileFunctions.bind(this)),
}
}
compileRole(newFunction, role) {
const compiledFunction = newFunction
if (typeof role === 'string') {
if (role.startsWith('arn:')) {
// 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,
)
}
} else 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 {
// role is an "Fn::ImportValue" or "Fn::Sub" object
compiledFunction.Properties.Role = role
}
}
async downloadPackageArtifact(functionName) {
const { region } = this.options
const S3 = new AWS.S3({ region })
const functionObject = this.serverless.service.getFunction(functionName)
if (functionObject.image) return
const artifactFilePath =
_.get(functionObject, 'package.artifact') ||
_.get(this, 'serverless.service.package.artifact')
const s3Object = parseS3URI(artifactFilePath)
if (!s3Object) return
log.info(`Downloading ${s3Object.Key} from bucket ${s3Object.Bucket}`)
await new Promise((resolve, reject) => {
const tmpDir = this.serverless.utils.getTmpDirPath()
const filePath = path.join(tmpDir, path.basename(s3Object.Key))
const readStream = S3.getObject(s3Object).createReadStream()
const writeStream = fs.createWriteStream(filePath)
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)
})
})
}
async addFileToHash(filePath, hash) {
const lambdaHashingVersion =
this.serverless.service.provider.lambdaHashingVersion
if (
lambdaHashingVersion < 20201221 &&
!this.options['enforce-hash-update']
) {
await addFileContentsToHashes(filePath, [hash])
} else {
const filePathHash = await getHashForFilePath(filePath)
hash.write(filePathHash)
}
}
async compileFunction(functionName) {
const cfTemplate =
this.serverless.service.provider.compiledCloudFormationTemplate
const functionResource = this.cfLambdaFunctionTemplate()
const functionObject = this.serverless.service.getFunction(functionName)
functionObject.package = functionObject.package || {}
const enforceHashUpdate = this.options['enforce-hash-update']
if (!functionObject.handler && !functionObject.image) {
throw new ServerlessError(
`Either "handler" or "image" property needs to be set on function "${functionName}"`,
'FUNCTION_NEITHER_HANDLER_NOR_IMAGE_DEFINED_ERROR',
)
}
if (functionObject.handler && functionObject.image) {
throw new ServerlessError(
`Either "handler" or "image" property (not both) needs to be set on function "${functionName}".`,
'FUNCTION_BOTH_HANDLER_AND_IMAGE_DEFINED_ERROR',
)
}
let functionImageUri
let functionImageSha
if (functionObject.image) {
;({ functionImageUri, functionImageSha } =
await this.provider.resolveImageUriAndSha(functionName))
if (_.isObject(functionObject.image)) {
const imageConfig = {}
if (functionObject.image.command) {
imageConfig.Command = functionObject.image.command
}
if (functionObject.image.entryPoint) {
imageConfig.EntryPoint = functionObject.image.entryPoint
}
if (functionObject.image.workingDirectory) {
imageConfig.WorkingDirectory = functionObject.image.workingDirectory
}
if (Object.keys(imageConfig).length) {
functionResource.Properties.ImageConfig = imageConfig
}
}
}
// publish these properties to the platform
functionObject.memory =
functionObject.memorySize ||
this.serverless.service.provider.memorySize ||
1024
if (!functionObject.timeout) {
functionObject.timeout = this.serverless.service.provider.timeout || 6
}
let artifactFilePath
if (functionObject.handler) {
const serviceArtifactFileName =
this.provider.naming.getServiceArtifactName()
const functionArtifactFileName =
this.provider.naming.getFunctionArtifactName(functionName)
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.serviceDir,
'.serverless',
artifactFileName,
)
}
const runtimeManagement = this.provider.resolveFunctionRuntimeManagement(
functionObject.runtimeManagement,
)
if (runtimeManagement.mode !== 'auto') {
functionResource.Properties.RuntimeManagementConfig = {
UpdateRuntimeOn: runtimeManagementMap.get(runtimeManagement.mode),
}
if (runtimeManagement.mode === 'manual') {
functionResource.Properties.RuntimeManagementConfig.RuntimeVersionArn =
runtimeManagement.arn
}
}
functionObject.runtime = this.provider.getRuntime(functionObject.runtime)
functionResource.Properties.Handler = functionObject.handler
functionResource.Properties.Code.S3Bucket = this.serverless.service
.package.deploymentBucket
? this.serverless.service.package.deploymentBucket
: { Ref: 'ServerlessDeploymentBucket' }
functionResource.Properties.Code.S3Key = `${
this.serverless.service.package.artifactDirectoryName
}/${artifactFilePath.split(path.sep).pop()}`
functionResource.Properties.Runtime = functionObject.runtime
} else {
functionResource.Properties.Code.ImageUri = functionImageUri
functionResource.Properties.PackageType = 'Image'
}
functionResource.Properties.FunctionName = functionObject.name
functionResource.Properties.MemorySize = functionObject.memory
functionResource.Properties.Timeout = functionObject.timeout
const functionArchitecture =
functionObject.architecture ||
this.serverless.service.provider.architecture
if (functionArchitecture)
functionResource.Properties.Architectures = [functionArchitecture]
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 = []
Object.entries(tags).forEach(([Key, Value]) => {
functionResource.Properties.Tags.push({ Key, Value })
})
}
if (functionObject.ephemeralStorageSize) {
functionResource.Properties.EphemeralStorage = {
Size: functionObject.ephemeralStorageSize,
}
}
if (functionObject.onError) {
const arn = functionObject.onError
if (typeof arn === 'string') {
const iamRoleLambdaExecution =
cfTemplate.Resources.IamRoleLambdaExecution
functionResource.Properties.DeadLetterConfig = {
TargetArn: arn,
}
// update the PolicyDocument statements (if default policy is used)
if (iamRoleLambdaExecution) {
iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement.push(
{
Effect: 'Allow',
Action: ['sns:Publish'],
Resource: [arn],
},
)
}
} else {
functionResource.Properties.DeadLetterConfig = {
TargetArn: arn,
}
}
}
let kmsKeyArn
if (this.serverless.service.provider.kmsKeyArn) {
kmsKeyArn = this.serverless.service.provider.kmsKeyArn
}
if (functionObject.kmsKeyArn) kmsKeyArn = functionObject.kmsKeyArn
if (kmsKeyArn) {
if (typeof kmsKeyArn === 'string') {
functionResource.Properties.KmsKeyArn = kmsKeyArn
// update the PolicyDocument statements (if default policy is used)
const iamRoleLambdaExecution =
cfTemplate.Resources.IamRoleLambdaExecution
if (iamRoleLambdaExecution) {
iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement =
_.unionWith(
iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument
.Statement,
[
{
Effect: 'Allow',
Action: ['kms:Decrypt'],
Resource: [kmsKeyArn],
},
],
_.isEqual,
)
}
} else {
functionResource.Properties.KmsKeyArn = kmsKeyArn
}
}
const tracing =
functionObject.tracing ||
(this.serverless.service.provider.tracing &&
this.serverless.service.provider.tracing.lambda)
if (tracing) {
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,
)
}
}
if (
functionObject.environment ||
this.serverless.service.provider.environment
) {
functionResource.Properties.Environment = {}
functionResource.Properties.Environment.Variables = Object.assign(
{},
this.serverless.service.provider.environment,
functionObject.environment,
)
}
const role = this.provider.getCustomExecutionRole(functionObject)
this.compileRole(functionResource, role || 'IamRoleLambdaExecution')
// ensure provider VPC is not used if function VPC explicitly unset
if (functionObject.vpc !== null && functionObject.vpc !== false) {
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
}
}
const fileSystemConfig = functionObject.fileSystemConfig
if (fileSystemConfig) {
if (!functionResource.Properties.VpcConfig) {
const errorMessage = [
`Function "${functionName}": when using fileSystemConfig, `,
'ensure that function has vpc configured ',
'on function or provider level',
].join('')
throw new ServerlessError(
errorMessage,
'LAMBDA_FILE_SYSTEM_CONFIG_MISSING_VPC',
)
}
const iamRoleLambdaExecution = cfTemplate.Resources.IamRoleLambdaExecution
const stmt = {
Effect: 'Allow',
Action: [
'elasticfilesystem:ClientMount',
'elasticfilesystem:ClientWrite',
],
Resource: [fileSystemConfig.arn],
}
// update the PolicyDocument statements (if default policy is used)
if (iamRoleLambdaExecution) {
iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement.push(
stmt,
)
}
const cfFileSystemConfig = {
Arn: fileSystemConfig.arn,
LocalMountPath: fileSystemConfig.localMountPath,
}
functionResource.Properties.FileSystemConfigs = [cfFileSystemConfig]
}
if (
functionObject.reservedConcurrency ||
functionObject.reservedConcurrency === 0
) {
functionResource.Properties.ReservedConcurrentExecutions =
functionObject.reservedConcurrency
}
if (
!functionObject.disableLogs &&
!functionObject?.logs?.logGroup &&
!this.serverless.service.provider.logs?.lambda?.logGroup
) {
functionResource.DependsOn = [
this.provider.naming.getLogGroupLogicalId(functionName),
].concat(functionResource.DependsOn || [])
}
if (functionObject.layers) {
functionResource.Properties.Layers = functionObject.layers
} else if (this.serverless.service.provider.layers) {
// To avoid unwanted side effects ensure to not reference same array instace on each function
functionResource.Properties.Layers = Array.from(
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
if (
shouldVersionFunction ||
functionObject.provisionedConcurrency ||
functionObject.snapStart
) {
// 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.
if (enforceHashUpdate) {
functionResource.Properties.Description =
'temporary-description-to-enforce-hash-update'
}
const versionHash = crypto.createHash('sha256')
versionHash.setEncoding('base64')
const layerConfigurations = _.cloneDeep(
extractLayerConfigurationsFromFunction(
functionResource.Properties,
cfTemplate,
),
)
const versionResource = this.cfLambdaVersionTemplate()
if (functionImageSha) {
versionResource.Properties.CodeSha256 = functionImageSha
} else {
const fileHash = await getHashForFilePath(artifactFilePath)
versionResource.Properties.CodeSha256 = fileHash
await this.addFileToHash(artifactFilePath, versionHash)
}
// Include all referenced layer code in the version id hash
const layerArtifactPaths = []
layerConfigurations.forEach((layer) => {
const layerArtifactPath = this.provider.resolveLayerArtifactName(
layer.name,
)
layerArtifactPaths.push(layerArtifactPath)
})
for (const layerArtifactPath of layerArtifactPaths.sort()) {
await this.addFileToHash(layerArtifactPath, versionHash)
}
// Include function and layer configuration details in the version id hash
for (const layerConfig of layerConfigurations) {
delete layerConfig.properties.Content.S3Key
}
const functionProperties = _.cloneDeep(functionResource.Properties)
// In `image` case, we assume it's path to ECR image digest
if (!functionObject.image) delete functionProperties.Code
// Properties applied to function globally (not specific to version or alias)
delete functionProperties.ReservedConcurrentExecutions
delete functionProperties.Tags
const lambdaHashingVersion =
this.serverless.service.provider.lambdaHashingVersion
if (
lambdaHashingVersion < 20201221 &&
!this.options['enforce-hash-update']
) {
// sort the layer configurations for hash consistency
const sortedLayerConfigurations = {}
const byKey = ([key1], [key2]) => key1.localeCompare(key2)
for (const {
name,
properties: layerProperties,
} of layerConfigurations) {
sortedLayerConfigurations[name] = _.fromPairs(
Object.entries(layerProperties).sort(byKey),
)
}
functionProperties.layerConfigurations = sortedLayerConfigurations
const sortedFunctionProperties = _.fromPairs(
Object.entries(functionProperties).sort(byKey),
)
versionHash.write(JSON.stringify(sortedFunctionProperties))
} else {
functionProperties.layerConfigurations = layerConfigurations
versionHash.write(
JSON.stringify(deepSortObjectByKey(functionProperties)),
)
}
versionHash.end()
const versionDigest = versionHash.read()
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 && functionObject.snapStart) {
throw new ServerlessError(
`Functions with enabled SnapStart does not support provisioned concurrency. Please remove at least one of the settings on function "${functionName}".`,
'FUNCTION_BOTH_PROVISIONED_CONCURRENCY_AND_SNAPSTART_ENABLED_ERROR',
)
}
if (functionObject.provisionedConcurrency) {
if (!shouldVersionFunction) delete versionResource.DeletionPolicy
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:
functionObject.provisionedConcurrency,
},
},
DependsOn: functionLogicalId,
}
cfTemplate.Resources[aliasLogicalId] = aliasResource
}
if (functionObject.snapStart) {
if (!shouldVersionFunction) delete versionResource.DeletionPolicy
functionResource.Properties.SnapStart = {
ApplyOn: 'PublishedVersions',
}
const aliasLogicalId =
this.provider.naming.getLambdaSnapStartAliasLogicalId(functionName)
const aliasName =
this.provider.naming.getLambdaSnapStartEnabledAliasName()
functionObject.targetAlias = {
name: aliasName,
logicalId: aliasLogicalId,
}
const aliasResource = {
Type: 'AWS::Lambda::Alias',
Properties: {
FunctionName: { Ref: functionLogicalId },
FunctionVersion: { 'Fn::GetAtt': [versionLogicalId, 'Version'] },
Name: aliasName,
},
DependsOn: functionLogicalId,
}
cfTemplate.Resources[aliasLogicalId] = aliasResource
}
}
if (functionObject.logs || this.serverless.service.provider.logs?.lambda) {
const functionLogConfig = functionObject.logs
const providerLogConfig = this.serverless.service.provider.logs.lambda
const applicationLogLevel =
functionLogConfig?.applicationLogLevel ||
providerLogConfig?.applicationLogLevel
const logFormat =
functionLogConfig?.logFormat || providerLogConfig?.logFormat
const logGroup =
functionLogConfig?.logGroup || providerLogConfig?.logGroup
const systemLogLevel =
functionLogConfig?.systemLogLevel || providerLogConfig?.systemLogLevel
const finalizedLogConfiguration = {}
if (applicationLogLevel && logFormat && logFormat === 'JSON') {
finalizedLogConfiguration.ApplicationLogLevel = applicationLogLevel
}
if (logFormat) {
finalizedLogConfiguration.LogFormat = logFormat
}
if (logGroup) {
finalizedLogConfiguration.LogFormat = logGroup
}
if (systemLogLevel && logFormat && logFormat === 'JSON') {
finalizedLogConfiguration.SystemLogLevel = systemLogLevel
}
if (Object.keys(finalizedLogConfiguration).length > 0) {
functionResource.Properties.LoggingConfig = finalizedLogConfiguration
}
}
this.compileFunctionUrl(functionName)
this.compileFunctionEventInvokeConfig(functionName)
}
compileFunctionUrl(functionName) {
const functionObject = this.serverless.service.getFunction(functionName)
const cfTemplate =
this.serverless.service.provider.compiledCloudFormationTemplate
const { url } = functionObject
if (!url) return
let auth = 'NONE'
let cors = null
if (url.authorizer === 'aws_iam') {
auth = 'AWS_IAM'
}
if (url.cors) {
cors = Object.assign({}, defaultCors)
if (url.cors.allowedOrigins) {
cors.allowedOrigins = _.uniq(url.cors.allowedOrigins)
} else if (url.cors.allowedOrigins === null) {
delete cors.allowedOrigins
}
if (url.cors.allowedHeaders) {
cors.allowedHeaders = _.uniq(url.cors.allowedHeaders)
} else if (url.cors.allowedHeaders === null) {
delete cors.allowedHeaders
}
if (url.cors.allowedMethods) {
cors.allowedMethods = _.uniq(url.cors.allowedMethods)
} else if (url.cors.allowedMethods === null) {
delete cors.allowedMethods
}
if (url.cors.allowCredentials) cors.allowCredentials = true
if (url.cors.exposedResponseHeaders) {
cors.exposedResponseHeaders = _.uniq(url.cors.exposedResponseHeaders)
}
cors.maxAge = url.cors.maxAge
}
const urlResource = {
Type: 'AWS::Lambda::Url',
Properties: {
AuthType: auth,
TargetFunctionArn: resolveLambdaTarget(functionName, functionObject),
},
DependsOn: _.get(functionObject.targetAlias, 'logicalId'),
}
if (cors) {
urlResource.Properties.Cors = {
AllowCredentials: cors.allowCredentials,
AllowHeaders: cors.allowedHeaders && Array.from(cors.allowedHeaders),
AllowMethods: cors.allowedMethods && Array.from(cors.allowedMethods),
AllowOrigins: cors.allowedOrigins && Array.from(cors.allowedOrigins),
ExposeHeaders:
cors.exposedResponseHeaders &&
Array.from(cors.exposedResponseHeaders),
MaxAge: cors.maxAge,
}
}
if (url.invokeMode === 'RESPONSE_STREAM') {
urlResource.Properties.InvokeMode = url.invokeMode
}
const logicalId =
this.provider.naming.getLambdaFunctionUrlLogicalId(functionName)
cfTemplate.Resources[logicalId] = urlResource
cfTemplate.Outputs[
this.provider.naming.getLambdaFunctionUrlOutputLogicalId(functionName)
] = {
Description: 'Lambda Function URL',
Value: {
'Fn::GetAtt': [logicalId, 'FunctionUrl'],
},
}
if (auth === 'NONE') {
cfTemplate.Resources[
this.provider.naming.getLambdaFnUrlPermissionLogicalId(functionName)
] = {
Type: 'AWS::Lambda::Permission',
Properties: {
FunctionName: resolveLambdaTarget(functionName, functionObject),
Action: 'lambda:InvokeFunctionUrl',
Principal: '*',
FunctionUrlAuthType: auth,
},
DependsOn: _.get(functionObject.targetAlias, 'logicalId'),
}
}
}
compileFunctionEventInvokeConfig(functionName) {
const functionObject = this.serverless.service.getFunction(functionName)
const { destinations, maximumEventAge, maximumRetryAttempts } =
functionObject
if (!destinations && !maximumEventAge && maximumRetryAttempts == null) {
return
}
const destinationConfig = {}
if (destinations) {
const executionRole = this.provider.getCustomExecutionRole(functionObject)
const hasAccessPoliciesHandledExternally = Boolean(executionRole)
if (destinations.onSuccess) {
destinationConfig.OnSuccess = {
Destination: this.getDestinationsArn(destinations.onSuccess),
}
if (!hasAccessPoliciesHandledExternally) {
this.ensureTargetExecutionPermission(destinations.onSuccess)
}
}
if (destinations.onFailure) {
destinationConfig.OnFailure = {
Destination: this.getDestinationsArn(destinations.onFailure),
}
if (!hasAccessPoliciesHandledExternally) {
this.ensureTargetExecutionPermission(destinations.onFailure)
}
}
}
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: destinationConfig,
Qualifier: functionObject.targetAlias
? functionObject.targetAlias.name
: '$LATEST',
},
DependsOn: _.get(functionObject.targetAlias, 'logicalId'),
}
if (maximumEventAge) {
resource.Properties.MaximumEventAgeInSeconds = maximumEventAge
}
if (maximumRetryAttempts != null) {
resource.Properties.MaximumRetryAttempts = maximumRetryAttempts
}
cfResources[
this.provider.naming.getLambdaEventConfigLogicalId(functionName)
] = resource
}
getDestinationsArn(destinationsProperty) {
if (typeof destinationsProperty === 'object') {
return destinationsProperty.arn
}
return destinationsProperty.startsWith('arn:')
? destinationsProperty
: this.provider.resolveFunctionArn(destinationsProperty)
}
// Memoized in a constructor
ensureTargetExecutionPermission(destinationsProperty) {
const iamPolicyStatements =
this.serverless.service.provider.compiledCloudFormationTemplate.Resources
.IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement
const action = (() => {
if (typeof destinationsProperty === 'object') {
if (destinationsProperty.type === 'function')
return 'lambda:InvokeFunction'
if (destinationsProperty.type === 'sqs') return 'sqs:SendMessage'
if (destinationsProperty.type === 'sns') return 'sns:Publish'
if (destinationsProperty.type === 'eventBus') return 'events:PutEvents'
}
if (typeof destinationsProperty === 'string') {
if (
!destinationsProperty.startsWith('arn:') ||
destinationsProperty.includes(':function:')
) {
return 'lambda:InvokeFunction'
}
if (destinationsProperty.includes(':sqs:')) return 'sqs:SendMessage'
if (destinationsProperty.includes(':sns:')) return 'sns:Publish'
if (destinationsProperty.includes(':event-bus/'))
return 'events:PutEvents'
}
throw new ServerlessError(
`Unsupported destination target ${destinationsProperty}`,
'UNSUPPORTED_DESTINATION_TARGET',
)
})()
let ResourceArn
if (typeof destinationsProperty === 'object') {
ResourceArn = destinationsProperty.arn
} else {
// 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
ResourceArn = destinationsProperty.startsWith('arn:')
? destinationsProperty
: {
'Fn::Sub': `arn:\${AWS::Partition}:lambda:\${AWS::Region}:\${AWS::AccountId}:function:${
this.serverless.service.getFunction(destinationsProperty).name
}`,
}
}
iamPolicyStatements.push({
Effect: 'Allow',
Action: action,
Resource: ResourceArn,
})
}
async downloadPackageArtifacts() {
const allFunctions = this.serverless.service.getAllFunctions()
// download package artifact sequentially one after another
for (const functionName of allFunctions) {
await this.downloadPackageArtifact(functionName)
}
}
async compileFunctions() {
const allFunctions = this.serverless.service.getAllFunctions()
return Promise.all(
allFunctions.map((functionName) => this.compileFunction(functionName)),
)
}
cfLambdaFunctionTemplate() {
return {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {},
},
}
}
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',
}
}
}
async function addFileContentsToHashes(filePath, hashes) {
return new Promise((resolve, reject) => {
const readStream = fs.createReadStream(filePath)
readStream
.on('data', (chunk) => {
hashes.forEach((hash) => {
hash.write(chunk)
})
})
.on('close', () => {
resolve()
})
.on('error', (error) => {
reject(new Error(`Could not add file content to hash: ${error}`))
})
})
}
function extractLayerConfigurationsFromFunction(
functionProperties,
cfTemplate,
) {
const layerConfigurations = []
if (!functionProperties.Layers) return layerConfigurations
functionProperties.Layers.forEach((potentialLocalLayerObject) => {
if (potentialLocalLayerObject.Ref) {
const configuration = cfTemplate.Resources[potentialLocalLayerObject.Ref]
if (!configuration) {
log.info(
`Could not find reference to layer: ${potentialLocalLayerObject.Ref}.`,
)
return
}
layerConfigurations.push({
name: configuration._serverlessLayerName,
ref: potentialLocalLayerObject.Ref,
properties: configuration.Properties,
})
}
})
return layerConfigurations
}
export default AwsCompileFunctions