kes
Version:
Making deployment to AWS using CloudFormation easier and fun
242 lines (212 loc) • 7.49 kB
JavaScript
'use strict';
const AWS = require('aws-sdk');
const fs = require('fs-extra');
const path = require('path');
const { exec, getZipName } = require('./utils');
/**
* Copy, zip and upload lambda functions to S3
*
* @param {Object} config the configuration object
* @param {String} kesFolder the path to the `.kes` folder
* @param {String} bucket the S3 bucket name
* @param {String} key the main folder to store the data in the bucket (stack + stage)
*/
class Lambda {
constructor(config, kesFolder, bucket, key) {
this.config = config;
this.kesFolder = kesFolder;
this.distFolder = path.join(this.kesFolder, 'dist');
this.buildFolder = path.join(this.kesFolder, 'build');
this.bucket = bucket;
this.key = path.join(key, 'lambdas');
this.grouped = {};
}
/**
* Creates a hash of keys and bucket names for the lambdas
* in the deployment based on their source path
*
* @private
* @param {String} local the path to the lambda zip file on the host machine
* @param {String} key the key to the lambda zip on S3
* @param {String} source the path to the original code
*
* @return {Object}
*/
updateGroup(local, key, source) {
const tmp = {
local,
remote: key,
bucket: this.bucket
};
this.grouped[source] = tmp;
return tmp;
}
/**
* Updates the lambda object with the bucket, s3 zip file path and
* local zip file location
*
* @param {Object} lambda the lambda object
* @returns {Object} returns the updated lambda object
*/
updateLambda(lambda) {
const tmp = this.grouped[lambda.source];
Object.assign(lambda, tmp);
return lambda;
}
getHash(folderName, method = 'shasum') {
const alternativeMethod = 'sha1sum';
let hash = exec(`find ${path.join(this.distFolder, folderName)} -type f | \
xargs ${method} | ${method} | awk '{print $1}' ${''}`, false);
hash = hash.toString().replace(/\n/, '');
if (hash.length === 0) {
if (method === alternativeMethod) {
throw new Error('You must either have shasum or sha1sum');
}
console.log(`switching to ${alternativeMethod}`);
return this.getHash(folderName, alternativeMethod);
}
return hash;
}
/**
* Copy source code of a given lambda function, zips it, calculate
* the hash of the source code and updates the lambda object with
* the hash, local and remote locations of the code
*
* @param {Object} lambda the lambda object
* @returns {Object} returns the updated lambda object
*/
zipLambda(lambda) {
// if lambda share source with another lambda,
// check if there is already a hash, if so, set the hash and return
if (this.grouped[lambda.source]) {
return lambda;
}
const folderName = getZipName(lambda.handler);
const lambdaPath = path.join(this.distFolder, folderName);
exec(`mkdir -p ${lambdaPath}; cp -r ${lambda.source} ${lambdaPath}/`);
exec(`cd ${this.distFolder} && zip -r ../build/${folderName} ${folderName}`);
const zipFile = `${folderName}.zip`;
const hash = this.getHash(folderName);
const key = path.join(this.key, hash.toString(), zipFile);
const localPath = path.join(this.buildFolder, zipFile);
return Object.assign(lambda, this.updateGroup(localPath, key, lambda.source));
}
/**
* Uploads the zipped lambda code to a given s3 bucket
* if the zip file already exists on S3 it skips the upload
*
* @param {Object} lambda the lambda object. It must have the following properties
* @param {String} lambda.bucket the s3 buckt name
* @param {String} lambda.remote the lambda code's key (path and filename) on s3
* @param {String} lambda.local the zip files location on local machine
* @returns {Promise} returns the promise of updated lambda object
*/
uploadLambda(lambda) {
const s3 = new AWS.S3();
const params = {
Bucket: this.bucket,
Key: lambda.remote,
Body: fs.readFileSync(lambda.local)
};
return new Promise((resolve, reject) => {
// check if it is already uploaded
s3.headObject({
Bucket: this.bucket,
Key: lambda.remote
}).promise().then(() => {
console.log(`Already Uploaded: s3://${this.bucket}/${lambda.remote}`);
return resolve(lambda);
}).catch(() => {
s3.upload(params, (e, r) => {
if (e) return reject(e);
console.log(`Uploaded: s3://${this.bucket}/${lambda.remote}`);
return resolve(lambda);
});
});
});
}
/**
* Zips and Uploads a lambda function. If the source of the function
* is already zipped and uploaded, it skips the step only updates the
* lambda config object.
*
* If the lambda config includes a link to zip file on S3, it skips
* the whole step.
*
* @param {Object} lambda the lambda object.
* @returns {Promise} returns the promise of updated lambda object
*/
zipAndUploadLambda(lambda) {
if (lambda.source) {
if (this.grouped[lambda.source]) {
return new Promise(resolve => resolve(this.updateLambda(lambda)));
}
lambda = this.zipLambda(lambda);
return this.uploadLambda(lambda);
}
else if (lambda.s3Source) {
lambda.remote = lambda.s3Source.key;
lambda.bucket = lambda.s3Source.bucket;
return new Promise(resolve => resolve(lambda));
}
return new Promise(resolve => resolve(lambda));
}
/**
* Zips and Uploads lambda functions in the congifuration object.
* If the source of the function
* is already zipped and uploaded, it skips the step only updates the
* lambda config object.
*
* If the lambda config includes a link to zip file on S3, it skips
* the whole step.
*
* @returns {Promise} returns the promise of updated configuration object
*/
process() {
if (this.config.lambdas) {
// remove the build folder if exists
fs.removeSync(this.buildFolder);
// create the lambda folder
fs.mkdirpSync(this.buildFolder);
// zip and upload lambdas
const jobs = this.config.lambdas.map(l => this.zipAndUploadLambda(l));
return new Promise((resolve, reject) => {
Promise.all(jobs).then(lambdas => {
this.config.lambdas = lambdas;
return resolve(this.config);
}).catch(e => reject(e));
});
}
return new Promise(resolve => resolve(this.config));
}
/**
* Uploads the zip code of a single lambda function to AWS Lambda
*
* @param {String} name name of the lambda function
* @returns {Promise} returns AWS response for lambda code update operation
*/
updateSingleLambda(name) {
const l = new AWS.Lambda();
// create the lambda folder if it doesn't already exist
fs.mkdirpSync(this.buildFolder);
let lambda;
this.config.lambdas.forEach(l => {
if (l.name === name) {
lambda = l;
}
});
if (!lambda) {
throw new Error('Lambda function is not defined in config.yml');
}
const stack = this.config.stackName;
const stage = this.config.stage;
console.log(`Updating ${lambda.name}`);
lambda = this.zipLambda(lambda);
return l.updateFunctionCode({
FunctionName: `${stack}-${stage}-${lambda.name}`,
ZipFile: fs.readFileSync(lambda.local)
}).promise()
.then((r) => console.log(`Lambda function ${lambda.name} has been updated`));
}
}
module.exports = Lambda;