mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
1083 lines
35 KiB
JavaScript
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
|