kes
Version:
Making deployment to AWS using CloudFormation easier and fun
300 lines (267 loc) • 9.43 kB
JavaScript
'use strict';
const get = require('lodash.get');
const Handlebars = require('handlebars');
const forge = require('node-forge');
const AWS = require('aws-sdk');
const path = require('path');
const fs = require('fs-extra');
const Lambda = require('./lambda');
const Config = require('./config');
const utils = require('./utils');
/**
* The main Kes class. This class is used in the command module to create
* the CLI interface for kes. This class can be extended in order to override
* and modify the behaviour of kes cli.
*
* @example
* const { Kes } = require('kes');
*
* const options = { stack: 'myStack' };
* const kes = new Kes(options);
*
* // create a new stack
* kes.createStack()
* .then(() => updateStack())
* .then(() => describeCF())
* .then(() => updateSingleLambda('myLambda'))
* .catch(e => console.log(e));
*
* @param {Object} options a js object that includes required options.
* @param {String} options.stack the stack name (required)
* @param {String} [options.stage='dev'] the stage name
* @param {String} [options.region='us-east-1'] the aws region
* @param {String} [options.profile=null] the profile name
* @param {String} [options.kesFolder='.kes'] the path to the kes folder
* @param {String} [options.configFile='config.yml'] the path to the config.yml
* @param {String} [options.stageFile='stage.yml'] the path to the stage.yml
* @param {String} [options.envFile='.env'] the path to the .env file
* @param {String} [options.cfFile='cloudformation.template.yml'] the path to the CF template
*/
class Kes {
constructor(options) {
this.options = options;
this.region = get(options, 'region');
this.profile = get(options, 'profile', null);
this.role = get(options, 'role', process.env.AWS_DEPLOYMENT_ROLE);
this.kesFolder = get(options, 'kesFolder', path.join(process.cwd(), '.kes'));
this.configFile = get(options, 'configFile', path.join(this.kesFolder, 'config.yml'));
this.stageFile = get(options, 'stageFile', path.join(this.kesFolder, 'stage.yml'));
this.envFile = get(options, 'envFile', path.join(this.kesFolder, '.env'));
this.cfFile = get(options, 'cfFile', path.join(this.kesFolder, 'cloudformation.template.yml'));
//local env file
const configInstance = new Config(options.stack, options.stage, this.configFile, this.stageFile, this.envFile);
this.config = configInstance.parse();
this.stage = this.config.stage || 'dev';
this.stack = this.config.stackName;
this.name = `${this.stack}-${this.stage}`;
this.bucket = get(this.config, 'buckets.internal');
this.templateUrl = `https://s3.amazonaws.com/${this.bucket}/${this.name}/cloudformation.yml`;
utils.configureAws(this.region, this.profile, this.role);
}
/**
* Updates code of a deployed lambda function
*
* @param {String} name the name of the lambda function defined in config.yml
* @return {Promise} returns the promise of an AWS response object
*/
updateSingleLambda(name) {
const lambda = new Lambda(this.config, this.kesFolder, this.bucket, this.name);
return lambda.updateSingleLambda(name);
}
/**
* Compiles a CloudFormation template in Yaml format.
*
* Reads the configuration yaml from `.kes/config.yml`.
*
* Writes the template to `.kes/cloudformation.yml`.
*
* Uses `.kes/cloudformation.template.yml` as the base template
* for generating the final CF template.
*
* @return {Promise} returns the promise of an AWS response object
*/
compileCF() {
const t = fs.readFileSync(this.cfFile, 'utf8');
Handlebars.registerHelper('ToMd5', function(value) {
if (value) {
const md = forge.md.md5.create();
md.update(value);
return md.digest().toHex();
}
return value;
});
Handlebars.registerHelper('ToJson', function(value) {
return JSON.stringify(value);
});
const template = Handlebars.compile(t);
const destPath = path.join(this.kesFolder, 'cloudformation.yml');
const lambda = new Lambda(this.config, this.kesFolder, this.bucket, this.name);
return lambda.process().then((config) => {
this.config = config;
console.log(`Template saved to ${destPath}`);
return fs.writeFileSync(destPath, template(this.config));
});
}
/**
* This is just a wrapper around AWS s3.upload method.
* It uploads a given string to a S3 object.
*
* @param {String} bucket the s3 bucket name
* @param {String} key the path and name of the object
* @param {String} body the content of the object
* @returns {Promise} returns the promise of an AWS response object
*/
uploadToS3(bucket, key, body) {
const s3 = new AWS.S3();
console.log(`Uploaded: s3://${bucket}/${key}`);
return s3.upload({ Bucket: bucket, Key: key, Body: body }).promise();
}
/**
* Uploads the Cloud Formation template to a given S3 location
*
* @returns {Promise} returns the promise of an AWS response object
*/
uploadCF() {
// build the template first
return this.compileCF().then(() => {
// make sure cloudformation template exists
try {
fs.accessSync(path.join(this.cfFile));
}
catch (e) {
throw new Error('cloudformation.yml is missing.');
}
// upload CF template to S3
if (this.bucket) {
return this.uploadToS3(
this.bucket,
`${this.name}/cloudformation.yml`,
fs.readFileSync(path.join(this.kesFolder, 'cloudformation.yml'))
);
}
else {
console.log('Skipping CF template upload because internal bucket value is not provided.');
return true;
}
});
}
/**
* Calls CloudFormation's update-stack or create-stack methods
*
* @param {String} op possible values are 'create' and 'update'
* @returns {Promise} returns the promise of an AWS response object
*/
cloudFormation(op) {
const cf = new AWS.CloudFormation();
let opFn = op === 'create' ? cf.createStack : cf.updateStack;
const wait = op === 'create' ? 'stackCreateComplete' : 'stackUpdateComplete';
const cfParams = [];
// add custom params from the config file if any
if (this.config.params) {
this.config.params.forEach((p) => {
cfParams.push({
ParameterKey: p.name,
ParameterValue: p.value,
UsePreviousValue: p.usePrevious || false
//NoEcho: p.noEcho || true
});
});
}
let capabilities = [];
if (this.config.capabilities) {
capabilities = this.config.capabilities.map(c => c);
}
const params = {
StackName: this.name,
Parameters: cfParams,
Capabilities: capabilities
};
if (this.bucket) {
params.TemplateURL = this.templateUrl;
}
else {
params.TemplateBody = fs.readFileSync(path.join(this.kesFolder, 'cloudformation.yml')).toString();
}
opFn = opFn.bind(cf);
return opFn(params).promise().then(() => {
console.log('Waiting for the CF operation to complete');
return cf.waitFor(wait, { StackName: this.name }).promise()
.then(r => console.log(`CF operation is in state of ${r.Stacks[0].StackStatus}`))
.catch(e => {
if (e) {
if (e.message.includes('Resource is not in the state')) {
console.log('CF create/update failed. Check the logs');
}
throw e;
}
});
})
.catch((e) => {
if (e.message === 'No updates are to be performed.') {
console.log(e.message);
return e.message;
}
else {
console.log('There was an error creating/updating the CF stack');
throw e;
}
});
}
/**
* Validates the CF template
*
* @returns {Promise} returns the promise of an AWS response object
*/
validateTemplate() {
console.log('Validating the template');
const url = `https://s3.amazonaws.com/${this.bucket}/${this.name}/cloudformation.yml`;
const params = {};
if (this.bucket) {
params.TemplateURL = url;
}
else {
params.TemplateBody = fs.readFileSync(path.join(this.kesFolder, 'cloudformation.yml')).toString();
}
// Build and upload the CF template
const cf = new AWS.CloudFormation();
return cf.validateTemplate(params)
.promise().then(() => console.log('Template is valid'));
}
/**
* Describes the cloudformation stack deployed
*
* @returns {Promise} returns the promise of an AWS response object
*/
describeCF() {
const cf = new AWS.CloudFormation();
return cf.describeStacks({
StackName: `${this.name}`
}).promise();
}
/**
* Generic create/update method for CloudFormation
*
* @param {String} op possible values are 'create' and 'update'
* @returns {Promise} returns the promise of an AWS response object
*/
opsStack(ops) {
return this.uploadCF().then(() => this.cloudFormation(ops));
}
/**
* Creates a CloudFormation stack for the class instance
*
* @returns {Promise} returns the promise of an AWS response object
*/
createStack() {
return this.opsStack('create');
}
/**
* Updates an existing CloudFormation stack for the class instance
*
* @returns {Promise} returns the promise of an AWS response object
*/
updateStack() {
return this.opsStack('update');
}
}
module.exports = Kes;