UNPKG

jaws-framework

Version:

JAWS is the serverless framework powered by Amazon Web Services.

946 lines (780 loc) 22.4 kB
'use strict'; /** * JAWS Services: AWS */ var Promise = require('bluebird'), AWS = require('aws-sdk'), path = require('path'), os = require('os'), JawsError = require('../jaws-error/index'), utils = require('../utils'), async = require('async'), fs = require('fs'); Promise.promisifyAll(fs); exports.validLambdaRegions = [ 'us-east-1', 'us-west-2', //oregon 'eu-west-1', //Ireland 'ap-northeast-1', //Tokyo ]; /** * Set AWS SDK Creds and region from a given profile * * @param awsProfile * @param awsRegion */ module.exports.configAWS = function(awsProfile, awsRegion) { // Check Profile Exists this.profilesGet(awsProfile); // Set Credentials AWS.config.credentials = new AWS.SharedIniFileCredentials({ profile: awsProfile, }); // Set Region AWS.config.update({ region: awsRegion, }); }; /** * Get the directory containing AWS configuration files * * @returns {string} */ module.exports.getConfigDir = function() { var env = process.env; var home = env.HOME || env.USERPROFILE || (env.HOMEPATH ? ((env.HOMEDRIVE || 'C:/') + env.HOMEPATH) : null); if (!home) { throw new JawsError('Cant find homedir', JawsError.errorCodes.MISSING_HOMEDIR); } return path.join(home, '.aws'); }; /** * Gets a map of AWS profiles from ~/.aws/credentials * * @returns {*} */ module.exports.profilesMap = function() { var credsPath = path.join(this.getConfigDir(), 'credentials'); return AWS.util.ini.parse(AWS.util.readFileSync(credsPath)); }; /** * Set a new AWS profile on the filesystem. * * Creates entry in ~/.aws/config and credentials * * @param awsProfile * @param awsRegion * @param accessKeyId * @param secretKey */ module.exports.profilesSet = function(awsProfile, awsRegion, accessKeyId, secretKey) { utils.jawsDebug('Setting new AWS profile:', awsProfile); var configDir = this.getConfigDir(), credsPath = path.join(configDir, 'credentials'), configPath = path.join(configDir, 'config'); if (!utils.dirExistsSync(configDir)) { fs.mkdirSync(configDir, parseInt('0700', 8)); //change to 0o700 in es6 } fs.appendFileSync( credsPath, '[' + awsProfile + ']' + os.EOL + 'aws_access_key_id = ' + accessKeyId.trim() + os.EOL + 'aws_secret_access_key = ' + secretKey.trim() + os.EOL); var profileNameForConfig = (awsProfile == 'default') ? 'default' : 'profile ' + awsProfile; fs.appendFileSync( configPath, '[' + profileNameForConfig + ']' + os.EOL + 'region = ' + awsRegion + os.EOL); }; /** * Get AWS profiles from the filesystem * * @param awsProfile * @returns {list} profiles */ module.exports.profilesGet = function(awsProfile) { var profiles = this.profilesMap(); if (!profiles[awsProfile]) { throw new JawsError('Cant find profile ' + awsProfile + ' in ~/.aws/credentials', awsProfile); } return profiles; }; /** * IAM: Get role * * @param awsProfile * @param awsRegion * @param roleName * @returns {Promise} */ exports.iamGetRole = function(awsProfile, awsRegion, roleName) { var _this = this; return new Promise(function(resolve, reject) { // Config AWS _this.configAWS(awsProfile, awsRegion); // Instantiate var IAM = new AWS.IAM({ apiVersion: '2010-05-08', }); var params = { RoleName: roleName, }; IAM.getRole(params, function(error, data) { if (error) { return reject(new JawsError( error.message, JawsError.errorCodes.UNKNOWN)); } else { return resolve(data); } }); }); }; /** * CloudFormation: Describe Stack * * @param awsProfile * @param awsRegion * @param stackName * @returns {Promise} */ exports.cfDescribeStacks = function(awsProfile, awsRegion, stackName) { var _this = this; return new Promise(function(resolve, reject) { // Config AWS _this.configAWS(awsProfile, awsRegion); // Instantiate var CF = new AWS.CloudFormation({ apiVersion: '2010-05-15', }); var params = { StackName: stackName, }; CF.describeStacks(params, function(error, data) { if (error) { return reject(new JawsError( error.message, JawsError.errorCodes.UNKNOWN)); } else { return resolve(data); } }); }); }; /** * CloudFormation: Describe Stack Resource * @param awsProfile * @param awsRegion * @param stackId * @param cfResourceId * @returns {Promise} */ exports.cfDescribeStackResource = function(awsProfile, awsRegion, stackId, cfResourceId) { var _this = this; return new Promise(function(resolve, reject) { // Config AWS _this.configAWS(awsProfile, awsRegion); // Instantiate var CF = new AWS.CloudFormation({ apiVersion: '2010-05-15', }); var params = { LogicalResourceId: cfResourceId, StackName: stackId, }; CF.describeStackResource(params, function(error, data) { if (error) { return reject(new JawsError( error.message, JawsError.errorCodes.UNKNOWN)); } else { return resolve(data); } }); }); }; /** * CloudFormation: List Stack Resources * @param awsProfile * @param awsRegion * @param stackName * @param cfResourceId * @returns {Promise} */ exports.cfListStackResources = function(awsProfile, awsRegion, stackName, nextToken) { var _this = this; return new Promise(function(resolve, reject) { // Config AWS _this.configAWS(awsProfile, awsRegion); // Instantiate var CF = Promise.promisifyAll(new AWS.CloudFormation({ apiVersion: '2010-05-15', })); var params = { StackName: stackName, NextToken: nextToken, }; return CF.listStackResources(params, function(error, data) { if (error) { return reject(new JawsError( error.message, JawsError.errorCodes.UNKNOWN)); } else { return resolve(data); } }); }); }; /** * CloudFormation: Get Resources Stack Name * @param stage * @param projName * @returns {string} */ exports.cfGetResourcesStackName = function(stage, projName) { return [stage, projName, 'r'].join('-'); // stack names are alphanumeric + -, no _ :( }; /** * CloudFormation: Get Lambdas Stack Name * @param stage * @param projName * @returns {string} */ exports.cfGetLambdasStackName = function(stage, projName) { return [stage, projName, 'l'].join('-'); // stack names are alphanumeric + -, no _ :( }; /** * CloudFormation: Get existing lambdas stack template body * * @param awsProfile * @param awsRegion * @param stage * @param projName * @returns {Promise} string of JSON template body */ exports.cfGetLambdasStackTemplate = function(awsProfile, awsRegion, stage, projName) { var _this = this; _this.configAWS(awsProfile, awsRegion); var CF = Promise.promisifyAll(new AWS.CloudFormation({ apiVersion: '2010-05-15', })); return CF.getTemplateAsync({ StackName: _this.cfGetLambdasStackName(stage, projName) }) .then(function(data) { return data.TemplateBody; }); }; /** * S3: CF File On S3 * @param awsProfile * @param projRootPath * @param awsRegion * @param bucketName * @param projName * @param projStage * @param type * @returns {*} */ exports.putCfFile = function(awsProfile, projRootPath, awsRegion, bucketName, projName, projStage, type) { if (['lambdas', 'resources'].indexOf(type) == -1) { Promise.reject(new JawsError('Type ' + type + ' invalid. Must be lambdas or resources', JawsError.errorCodes.UNKNOWN)); } var d = new Date(), cfPath = path.join(projRootPath, 'cloudformation', projStage, awsRegion, type + '-cf.json'), key = ['JAWS', projName, projStage, 'cloudformation/' + type].join('/') + '@' + d.getTime() + '.json', params = { Bucket: bucketName, Key: key, ACL: 'private', ContentType: 'application/json', Body: fs.readFileSync(cfPath), }; return this.putS3Object(awsProfile, awsRegion, params) .then(function() { //Really AWS - TemplateURL is an https:// URL. You force us to lookup endpt vs bucket/key attrs!?!? wtf not cool var s3 = new AWS.S3(); //Seriously, not cool... return 'https://' + s3.endpoint.hostname + '/' + bucketName + '/' + key; }) }; /** * CloudFormation: Create Lambdas Stack * @param JAWS * @param stage * @param region * @param lambdaRoleArn * @returns {*} */ exports.cfCreateLambdasStack = function(JAWS, stage, region, lambdaRoleArn) { var _this = this, awsProfile = JAWS._meta.profile, projRootPath = JAWS._meta.projectRootPath, bucketName = JAWS.getJawsBucket(region, stage), projName = JAWS._meta.projectJson.name; _this.configAWS(awsProfile, region); var CF = Promise.promisifyAll(new AWS.CloudFormation({ apiVersion: '2010-05-15', })), stackName = _this.cfGetLambdasStackName(stage, projName); var params = { StackName: stackName, Capabilities: [], OnFailure: 'ROLLBACK', Parameters: [{ ParameterKey: 'aaLambdaRoleArn', ParameterValue: lambdaRoleArn, UsePreviousValue: false, },], Tags: [{ Key: 'STAGE', Value: stage, },], }; return _this.putCfFile(awsProfile, projRootPath, region, bucketName, projName, stage, 'lambdas') .then(function(templateUrl) { params.TemplateURL = templateUrl; return CF.createStackAsync(params); }); }; /** * CloudFormation: Update Lambdas Stack * @param JAWS * @param stage * @param region * @param lambdaRoleArn * @returns {*} */ exports.cfUpdateLambdasStack = function(JAWS, stage, region, lambdaRoleArn) { var _this = this, awsProfile = JAWS._meta.profile, projRootPath = JAWS._meta.projectRootPath, bucketName = JAWS.getJawsBucket(region, stage), projName = JAWS._meta.projectJson.name; _this.configAWS(awsProfile, region); var CF = Promise.promisifyAll(new AWS.CloudFormation({ apiVersion: '2010-05-15', })), stackName = _this.cfGetLambdasStackName(stage, projName); var params = { StackName: stackName, Capabilities: [], UsePreviousTemplate: false, Parameters: [{ ParameterKey: 'aaLambdaRoleArn', ParameterValue: lambdaRoleArn, UsePreviousValue: false, },] }; return _this.putCfFile(awsProfile, projRootPath, region, bucketName, projName, stage, 'lambdas') .then(function(templateUrl) { params.TemplateURL = templateUrl; return CF.updateStackAsync(params); }); }; /** * CloudFormation: Create Resources Stack * @param awsProfile * @param awsRegion * @param projRootPath * @param projName * @param projStage * @param projNotificationEmail * @returns {Promise} */ exports.cfCreateResourcesStack = function(awsProfile, awsRegion, projRootPath, projName, projStage, projDomain, projNotificationEmail, jawsBucket) { var _this = this; _this.configAWS(awsProfile, awsRegion); var CF = Promise.promisifyAll(new AWS.CloudFormation({ apiVersion: '2010-05-15', })); var stackName = _this.cfGetResourcesStackName(projStage, projName); var resourcesTemplate = require('../templates/resources-cf.json'); var params = { StackName: stackName, Capabilities: [ 'CAPABILITY_IAM', ], TemplateBody: JSON.stringify(resourcesTemplate), OnFailure: 'ROLLBACK', Parameters: [{ ParameterKey: 'aaProjectName', ParameterValue: projName, UsePreviousValue: false, }, { ParameterKey: 'aaStage', ParameterValue: projStage, UsePreviousValue: false, }, { ParameterKey: 'aaJawsBucket', ParameterValue: jawsBucket, UsePreviousValue: false, }, { ParameterKey: 'aaDataModelStage', ParameterValue: projStage, UsePreviousValue: false, }, { ParameterKey: 'aaProjectDomain', ParameterValue: 'mydomain.com', UsePreviousValue: false, }, { ParameterKey: 'aaNotficationEmail', ParameterValue: projNotificationEmail, UsePreviousValue: false, }, { ParameterKey: 'aaDefaultDynamoRWThroughput', ParameterValue: '1', UsePreviousValue: false, },], Tags: [{ Key: 'STAGE', Value: projStage, },], }; // Create CloudFormation Stack return CF.createStackAsync(params); }; /** * CloudFormation: Update Resources Stack * @param JAWS * @param stage * @param region * @returns {*} */ exports.cfUpdateResourcesStack = function(JAWS, stage, region) { var _this = this, awsProfile = JAWS._meta.profile, projRootPath = JAWS._meta.projectRootPath, bucketName = JAWS.getJawsBucket(region, stage), projName = JAWS._meta.projectJson.name; _this.configAWS(awsProfile, region); var CF = Promise.promisifyAll(new AWS.CloudFormation({ apiVersion: '2010-05-15', })), stackName = _this.cfGetResourcesStackName(stage, projName); var params = { StackName: stackName, Capabilities: [ 'CAPABILITY_IAM', ], UsePreviousTemplate: false, Parameters: [] }; return _this.putCfFile(awsProfile, projRootPath, region, bucketName, projName, stage, 'resources') .then(function(templateUrl) { params.TemplateURL = templateUrl; return CF.updateStackAsync(params); }); }; /** * CloudFormation: Monitor CF Stack Status (Create/Update) * @param cfData * @param awsProfile * @param region * @param createOrUpdate create|update * @param checkFreq default 5000 ms * @returns {Promise} */ exports.monitorCf = function(cfData, awsProfile, region, createOrUpdate, checkFreq) { var _this = this, stackStatusComplete, validStatuses; if (!checkFreq) checkFreq = 5000; if (createOrUpdate == 'create') { stackStatusComplete = 'CREATE_COMPLETE'; validStatuses = ['CREATE_IN_PROGRESS', stackStatusComplete]; } else if (createOrUpdate == 'update') { stackStatusComplete = 'UPDATE_COMPLETE'; validStatuses = ['UPDATE_IN_PROGRESS', 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS', stackStatusComplete]; } else { Promise.reject(new JawsError('Must specify create or update', JawsError.errorCodes.UNKNOWN)); } return new Promise(function(resolve, reject) { var stackStatus = null, stackData = null; async.whilst( function() { return stackStatus !== stackStatusComplete; }, function(callback) { setTimeout(function() { _this.cfDescribeStacks(awsProfile, region, cfData.StackId) .then(function(data) { stackData = data; stackStatus = stackData.Stacks[0].StackStatus; if (!stackStatus || validStatuses.indexOf(stackStatus) === -1) { console.log((data.Stacks && data.Stacks.length ? data.Stacks[0] : data)); return reject(new JawsError( 'Something went wrong while ' + createOrUpdate + 'ing your cloudformation')); } else { return callback(); } }); }, checkFreq); }, function() { return resolve(stackData.Stacks[0]); } ); }); }; /** * S3: Create Bucket * @param awsProfile * @param awsRegion * @param bucketName * @returns {*} */ exports.createBucket = function(awsProfile, awsRegion, bucketName) { this.configAWS(awsProfile, awsRegion); var s3 = Promise.promisifyAll(new AWS.S3()); return s3.getBucketAclAsync({Bucket: bucketName}) .then(function() { }) .error(function(err) { if (err.code == 'AccessDenied') { throw new JawsError( 'Bucket ' + bucketName + ' already exists and you do not have permissions to use it', JawsError.errorCodes.ACCESS_DENIED ); } return s3.createBucketAsync({ Bucket: bucketName, ACL: 'private', }); }); }; /** * S3: Put Object * @param awsProfile * @param awsRegion * @param params * @returns {*} */ exports.putS3Object = function(awsProfile, awsRegion, params) { this.configAWS(awsProfile, awsRegion); var s3 = Promise.promisifyAll(new AWS.S3()); return s3.putObjectAsync(params); }; /** * S3: Get Object * @param awsProfile * @param awsRegion * @param params * @returns {Promise} s3 data object response */ exports.getS3Object = function(awsProfile, awsRegion, params) { this.configAWS(awsProfile, awsRegion); var s3 = Promise.promisifyAll(new AWS.S3()); return s3.getObjectAsync(params); }; /** * S3: Get the env file for a given stage * @param awsProfile * @param awsRegion * @param bucketName * @param projectName * @param stage * @returns {Promise} s3 data object response */ exports.getEnvFile = function(awsProfile, awsRegion, bucketName, projectName, stage) { var key = ['JAWS', projectName, stage, 'envVars', '.env'].join('/'), params = { Bucket: bucketName, Key: key, }; utils.jawsDebug('env var s3 key: ' + key); return this.getS3Object(awsProfile, awsRegion, params); }; /** * S3: Put up the env file for a given stage * @param awsProfile * @param awsRegion * @param bucketName * @param projectName * @param stage */ exports.putEnvFile = function(awsProfile, awsRegion, bucketName, projectName, stage, contents) { var params = { Bucket: bucketName, Key: ['JAWS', projectName, stage, 'envVars', '.env'].join('/'), ACL: 'private', ContentType: 'text/plain', Body: contents, }; return this.putS3Object(awsProfile, awsRegion, params); }; /** * S3: Put up deployment zip for a given stage * @param awsProfile * @param awsRegion * @param bucketName * @param projectName * @param stage * @returns {Promise} key of zip file in s3 */ exports.putLambdaZip = function(awsProfile, awsRegion, bucketName, projectName, stage, lambdaName, zipBuffer) { var d = new Date(), key = ['JAWS', projectName, stage, 'lambdas', lambdaName + '@' + d.getTime() + '.zip'].join('/'), params = { Bucket: bucketName, Key: key, ACL: 'private', ContentType: 'application/zip', Body: zipBuffer, }; utils.jawsDebug('lambda zip s3 key: ' + key); return this.putS3Object(awsProfile, awsRegion, params) .then(function() { return key; }); }; /** * CloudWatchLogs: Get Log Streams * @param logGroupName * @param limit * @returns {Promise} */ exports.cwGetLogStreams = function(logGroupName, limit) { return new Promise(function(resolve, reject) { // Instantiate var cwLogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28', }); var params = { logGroupName: logGroupName, descending: true, limit: limit || 5, orderBy: 'LastEventTime', }; cwLogs.describeLogStreams(params, function(error, data) { if (error) { return reject(new JawsError( error.message, JawsError.errorCodes.UNKNOWN)); } else { return resolve(data); } }); }); }; /** * CloudWatchLogs: Get Log Stream Events * @param logGroupName * @param logStreamName * @returns {Promise} */ exports.cwGetStreamEvents = function(logGroupName, logStreamName) { return new Promise(function(resolve, reject) { // Instantiate var cwLogs = new AWS.CloudWatchLogs({ apiVersion: '2014-03-28', }); var params = { logGroupName: logGroupName, logStreamName: logStreamName, }; cwLogs.getLogEvents(params, function(err, data) { if (error) { return reject(new JawsError( error.message, JawsError.errorCodes.UNKNOWN)); } else { return resolve(data); } }); }); }; /** * Lambda: List Functions * @param awsProfile * @param awsRegion * @param bucketName * @returns {*} */ exports.lambdaListFunctions = function(awsProfile, awsRegion) { this.configAWS(awsProfile, awsRegion); var lambda = new AWS.Lambda(); var params = {}; return new Promise(function(resolve, reject) { lambda.listFunctions(params, function(err, data) { if (err) { return reject(err); } return resolve(data); }); }); }; /** * Lambda: Get Policy * @param awsProfile * @param awsRegion * @param bucketName * @returns {*} */ exports.lambdaGetPolicy = function(awsProfile, awsRegion, functionName) { this.configAWS(awsProfile, awsRegion); var lambda = new AWS.Lambda(); var params = { FunctionName: functionName.trim() }; return new Promise(function(resolve, reject) { lambda.getPolicy(params, function(err, data) { if (err) { return reject(err); } return resolve(data); }); }); }; /** * Lambda: Add Permission * @param awsProfile * @param awsRegion * @param bucketName * @returns {*} */ exports.lambdaAddPermission = function(awsProfile, awsRegion, permissionStatement) { this.configAWS(awsProfile, awsRegion); var lambda = new AWS.Lambda(); return new Promise(function(resolve, reject) { lambda.addPermission(permissionStatement, function(err, data) { if (err) { return reject(err); } return resolve(data); }); }); }; /** * Lambda: Remove Permission * @param awsProfile * @param awsRegion * @param bucketName * @returns {*} */ exports.lambdaRemovePermission = function(awsProfile, awsRegion, functionName, statementId) { this.configAWS(awsProfile, awsRegion); var lambda = new AWS.Lambda(); var params = { FunctionName: functionName, /* required */ StatementId: statementId, /* required */ }; return new Promise(function(resolve, reject) { lambda.removePermission(params, function(err, data) { if (err) { return reject(err); } return resolve(data); }); }); };