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