serverless/lib/plugins/aws/deploy/lib/upload-artifacts.js
2024-05-29 11:51:04 -04:00

243 lines
7.7 KiB
JavaScript

import _ from 'lodash'
import fsp from 'fs/promises'
import path from 'path'
import crypto from 'crypto'
import promiseLimit from 'ext/promise/limit.js'
import { filesize } from 'filesize'
import normalizeFiles from '../../lib/normalize-files.js'
import getLambdaLayerArtifactPath from '../../utils/get-lambda-layer-artifact-path.js'
import ServerlessError from '../../../../serverless-error.js'
import setS3UploadEncryptionOptions from '../../../../aws/set-s3-upload-encryption-options.js'
import utils from '@serverlessinc/sf-core/src/utils.js'
const limit = promiseLimit.bind(Promise)
const { log, progress } = utils
const MAX_CONCURRENT_ARTIFACTS_UPLOADS =
Number(process.env.SLS_MAX_CONCURRENT_ARTIFACTS_UPLOADS) || 3
export default {
async getFileStats(filepath) {
try {
return await fsp.stat(filepath)
} catch (error) {
throw new ServerlessError(
`Cannot read file artifact "${filepath}": ${error.message}`,
'INACCESSIBLE_FILE_ARTIFACT',
)
}
},
async uploadArtifacts() {
const artifactFilePaths = [
...(await this.getFunctionArtifactFilePaths()),
...this.getLayerArtifactFilePaths(),
]
if (artifactFilePaths.length === 1) {
const stats = await this.getFileStats(artifactFilePaths[0])
progress.get('main').notice(`Uploading (${filesize(stats.size)})`)
} else {
progress.get('main').notice(`Uploading (0/${artifactFilePaths.length})`)
}
await this.uploadCloudFormationFile()
await this.uploadStateFile()
await this.uploadFunctionsAndLayers()
await this.uploadCustomResources()
},
async uploadCloudFormationFile() {
log.info('Uploading CloudFormation file to S3')
const compiledTemplateFileName =
this.provider.naming.getCompiledTemplateS3Suffix()
const compiledCfTemplate =
this.serverless.service.provider.compiledCloudFormationTemplate
const normCfTemplate =
normalizeFiles.normalizeCloudFormationTemplate(compiledCfTemplate)
const fileHash = crypto
.createHash('sha256')
.update(JSON.stringify(normCfTemplate))
.digest('base64')
let params = {
Bucket: this.bucketName,
Key: `${this.serverless.service.package.artifactDirectoryName}/${compiledTemplateFileName}`,
Body: JSON.stringify(compiledCfTemplate),
ContentType: 'application/json',
Metadata: {
filesha256: fileHash,
},
}
const deploymentBucketObject =
this.serverless.service.provider.deploymentBucketObject
if (deploymentBucketObject) {
params = setS3UploadEncryptionOptions(params, deploymentBucketObject)
}
return this.provider.request('S3', 'upload', params)
},
async uploadStateFile() {
log.info('Uploading State file to S3')
const basename = this.provider.naming.getServiceStateFileName()
const content = await fsp.readFile(
path.join(this.serverless.serviceDir, '.serverless', basename),
'utf-8',
)
const stateObject = JSON.parse(content)
const fileHash = crypto
.createHash('sha256')
.update(JSON.stringify(normalizeFiles.normalizeState(stateObject)))
.digest('base64')
let params = {
Bucket: this.bucketName,
Key: `${this.serverless.service.package.artifactDirectoryName}/${basename}`,
Body: content,
ContentType: 'application/json',
Metadata: { filesha256: fileHash },
}
const deploymentBucketObject =
this.serverless.service.provider.deploymentBucketObject
if (deploymentBucketObject) {
params = setS3UploadEncryptionOptions(params, deploymentBucketObject)
}
return this.provider.request('S3', 'upload', params)
},
async getFunctionArtifactFilePaths() {
const functionNames = this.serverless.service.getAllFunctions()
return _.uniq(
(
await Promise.all(
functionNames.map(async (name) => {
const functionObject = this.serverless.service.getFunction(name)
if (functionObject.image) return null
const functionArtifactFileName =
this.provider.naming.getFunctionArtifactName(name)
functionObject.package = functionObject.package || {}
let artifactFilePath =
functionObject.package.artifact ||
this.serverless.service.package.artifact
if (
!artifactFilePath ||
(this.serverless.service.artifact &&
!functionObject.package.artifact)
) {
if (
this.serverless.service.package.individually ||
functionObject.package.individually
) {
const artifactFileName = functionArtifactFileName
artifactFilePath = path.join(this.packagePath, artifactFileName)
} else {
artifactFilePath = path.join(
this.packagePath,
this.provider.naming.getServiceArtifactName(),
)
}
}
functionObject.artifactSize = (
await this.getFileStats(artifactFilePath)
).size
return artifactFilePath
}),
)
).filter(Boolean),
)
},
getLayerArtifactFilePaths() {
const layerNames = this.serverless.service.getAllLayers()
return layerNames
.map((name) => {
const layerObject = this.serverless.service.getLayer(name)
if (layerObject.artifactAlreadyUploaded) {
log.info(`Skipped uploading ${name}`)
return null
}
return getLambdaLayerArtifactPath(
this.packagePath,
name,
this.provider.serverless.service,
this.provider.naming,
)
})
.filter(Boolean)
},
async uploadFunctionsAndLayers() {
const artifactFilePaths = [
...(await this.getFunctionArtifactFilePaths()),
...this.getLayerArtifactFilePaths(),
]
const shouldReportDetailedProgress = artifactFilePaths.length > 1
let alreadyUploadedCount = 0
const limitedUpload = limit(
MAX_CONCURRENT_ARTIFACTS_UPLOADS,
async ({ filename, s3KeyDirname }) => {
const stats = await this.getFileStats(filename)
const fileName = path.basename(filename)
log.info(
`Uploading service ${fileName} file to S3 (${filesize(stats.size)})`,
)
if (shouldReportDetailedProgress) {
progress
.get(`upload:${fileName}`)
.notice(
`Uploading service ${fileName} file to S3 (${filesize(
stats.size,
)})`,
)
}
const result = await this.uploadZipFile({
filename,
s3KeyDirname,
})
alreadyUploadedCount += 1
if (shouldReportDetailedProgress) {
progress
.get('main')
.notice(
`Uploading (${alreadyUploadedCount}/${artifactFilePaths.length})`,
)
progress.get(`upload:${fileName}`).remove()
}
return result
},
)
const uploadPromises = artifactFilePaths.map((filename) =>
limitedUpload({
filename,
s3KeyDirname: this.serverless.service.package.artifactDirectoryName,
}),
)
await Promise.all(uploadPromises)
},
async uploadCustomResources() {
const artifactFilePath = path.join(
this.serverless.serviceDir,
'.serverless',
this.provider.naming.getCustomResourcesArtifactName(),
)
if (this.serverless.utils.fileExistsSync(artifactFilePath)) {
log.info('Uploading custom CloudFormation resources')
await this.uploadZipFile({
filename: artifactFilePath,
s3KeyDirname: this.serverless.service.package.artifactDirectoryName,
})
}
},
}