UNPKG

kes

Version:

Making deployment to AWS using CloudFormation easier and fun

285 lines (253 loc) 8.99 kB
'use strict'; const AWS = require('aws-sdk'); const get = require('lodash.get'); const fs = require('fs-extra'); const path = require('path'); const utils = 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) */ class Lambda { constructor(config) { this.config = config; this.cf_basename = path.basename(config.cfFile, '.template.yml'); this.kesFolder = config.kesFolder; this.distFolder = path.join(this.kesFolder, 'dist'); this.buildFolder = path.join(this.kesFolder, 'build', this.cf_basename); this.bucket = get(config, 'bucket'); this.key = path.join(this.config.stack, 'lambdas'); this.grouped = {}; } /** * Adds hash value, bucket name, and remote and local paths * for lambdas that have source value. * * If a s3Source is usaed, only add remote and bucket values * @param {object} lambda the lambda object * @return {object} the lambda object */ buildS3Path(lambda) { if (lambda.source) { // get hash lambda.hash = this.getHash(lambda.source).toString(); lambda.bucket = this.bucket; if (!this.grouped.hasOwnProperty(lambda.hash)) { this.grouped[lambda.hash] = lambda.name; } // local zip const lambdaName = this.grouped[lambda.hash]; const zipFile = `${lambda.hash}-${lambdaName}.zip`; lambda.local = path.join(this.buildFolder, zipFile); // remote address lambda.remote = path.join(this.key, zipFile); } else if (lambda.s3Source) { lambda.remote = lambda.s3Source.key; lambda.bucket = lambda.s3Source.bucket; } return lambda; } /** * calculate the hash value for a given path * @param {string} folderName directory path * @param {string} method hash type, default to shasum * @return {buffer} hash value */ getHash(folderName, method) { if (!method) { method = 'shasum'; } const alternativeMethod = 'sha1sum'; let hash = utils.exec(`find ${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; } /** * zip a given lambda function source code * * @param {Object} lambda the lambda object * @returns {Promise} returns the promise of the lambda object */ zipLambda(lambda) { console.log(`Zipping ${lambda.local}`); // skip if the file with the same hash is zipped if (fs.existsSync(lambda.local)) { const stats = fs.statSync(lambda.local); if (stats.isFile() && stats.size > 0) { return Promise.resolve(lambda); } } return utils.zip(lambda.local, [lambda.source]).then(() => { console.log(`Zipped ${lambda.local}`); return lambda; }); } /** * 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((data) => { if (data.ContentLength !== params.Body.byteLength) { throw new Error('File sizes don\'t match'); } 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. * * @param {Object} lambda the lambda object. * @returns {Promise} returns the promise of updated lambda object */ zipAndUploadLambda(lambda) { // only zip lambda if it's not zipped const match = lambda.source.match(/.\.zip/); if (match) { lambda.local = lambda.source; return this.uploadLambda(lambda); } return this.zipLambda(lambda) .then(l => this.uploadLambda(l)); } /** * Zips and Uploads lambda functions in the configuration 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) { // create the lambda folder fs.mkdirpSync(this.buildFolder); let lambdas = this.config.lambdas; // if the lambdas is not an array but a object, convert it to a list if (!Array.isArray(this.config.lambdas)) { lambdas = Object.keys(this.config.lambdas).map(name => { const lambda = this.config.lambdas[name]; lambda.name = name; return lambda; }); } // install npm packages lambdas.filter(l => l.npmSource).forEach(l => utils.exec(`npm install ${l.npmSource.name}@${l.npmSource.version}`)); // build lambda path for lambdas that are zipped and uploaded lambdas = lambdas.map(l => this.buildS3Path(l)); // zip and upload only unique hashes let uniqueHashes = {}; lambdas.filter(l => l.source).forEach(l => { uniqueHashes[l.hash] = l; }); const jobs = Object.keys(uniqueHashes).map(l => this.zipAndUploadLambda(uniqueHashes[l])); return Promise.all(jobs) .then(() => { // we handle lambdas as both arrays and key/objects // below condition is intended to for cases where // the lambda is returned as a list if (Array.isArray(this.config.lambdas)) { this.config.lambdas = lambdas; return this.config; } const tmp = {}; lambdas.forEach(l => (tmp[l.name] = l)); this.config.lambdas = tmp; return this.config; }) .catch((e) => { // if the zip operation stops for any of the lambdas because the source // file is missing, zip files with the size of 0 are created. Removing // the build folder ensures that we start fresh in the next round of zipping if (e.message.includes('ENOENT')) { fs.removeSync(this.buildFolder); } throw e; }); } else return Promise.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; Object.keys(this.config.lambdas).forEach(n => { if (n === name) { lambda = this.config.lambdas[n]; } }); if (!lambda) { throw new Error('Lambda function is not defined in config.yml'); } const stack = this.config.stackName; lambda = this.buildS3Path(lambda); console.log(`Updating ${name}`); let promise; const match = lambda.source.match(/.\.zip/); if (match) { lambda.local = lambda.source; promise = l.updateFunctionCode({ FunctionName: `${stack}-${name}`, ZipFile: fs.readFileSync(lambda.source) }).promise(); } else { promise = this.zipLambda(lambda).then(lambda => l.updateFunctionCode({ FunctionName: `${stack}-${name}`, ZipFile: fs.readFileSync(lambda.local) }).promise()); } return promise.then((r) => console.log(`Lambda function ${name} has been updated`)); } } module.exports = Lambda;