serverless/lib/plugins/aws/deploy/lib/upload-artifacts.js
2022-02-25 20:12:02 +01:00

223 lines
7.5 KiB
JavaScript

'use strict';
const _ = require('lodash');
const fsp = require('fs').promises;
const path = require('path');
const crypto = require('crypto');
const limit = require('ext/promise/limit').bind(Promise);
const filesize = require('filesize');
const normalizeFiles = require('../../lib/normalize-files');
const getLambdaLayerArtifactPath = require('../../utils/get-lambda-layer-artifact-path');
const ServerlessError = require('../../../../serverless-error');
const setS3UploadEncryptionOptions = require('../../../../aws/set-s3-upload-encryption-options');
const { progress, log } = require('@serverless/utils/log');
const MAX_CONCURRENT_ARTIFACTS_UPLOADS =
Number(process.env.SLS_MAX_CONCURRENT_ARTIFACTS_UPLOADS) || 3;
module.exports = {
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 fileHash = crypto.createHash('sha256').update(content).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,
})
);
if (this.console.isEnabled) {
uploadPromises.push(this.console.uploadOtelExtensionLayer());
}
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,
});
}
},
};