mirror of
https://github.com/serverless/serverless.git
synced 2026-01-18 14:58:43 +00:00
243 lines
7.7 KiB
JavaScript
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,
|
|
})
|
|
}
|
|
},
|
|
}
|