lambdasync
Version:
Easy sync between local code and AWS lambda functions
298 lines (273 loc) • 8.31 kB
JavaScript
const fs = require('fs');
const path = require('path');
const inquirer = require('inquirer');
const copy = require('recursive-copy');
const mkdirp = require('mkdirp');
const minimatch = require('minimatch');
const rimraf = require('rimraf');
const chainData = require('chain-promise-data');
const {description} = require('../../package.json');
const {
promisedExec,
stripLambdaVersion,
markdown,
markdownProperty,
functionExists,
copyPackageJson,
ignoreData,
startWith,
npmInstall,
hashPackageDependencies,
awsPromise,
removeFileExtension,
makeAbsolutePath,
removeCurrentPath
} = require('./util');
const {updateSettings} = require('./settings');
const {
LAMBDASYNC_BIN,
LAMBDASYNC_SRC,
TARGET_ROOT,
PROMPT_CONFIRM_OVERWRITE_FUNCTION,
TARGET_DEPLOY_DIR,
TARGET_HIDDEN_DIR,
DEPENDENCY_HASH_FILE
} = require('./constants.js');
const aws = require('./aws.js');
const {readFile, writeFile} = require('./file.js');
const targetOptions = {cwd: TARGET_ROOT};
let lambda;
let settings;
function deploy(deploySettings) {
settings = deploySettings;
const AWS = aws(settings);
lambda = new AWS.Lambda();
return functionExists(lambda, settings.lambdaName)
.then(functionExists => {
// If function doesn't already exist, or if it was already deployed
// by lambdasync lets just deploy it
if (!functionExists) {
return doDeploy('new');
}
if (settings.lambdaArn) {
return doDeploy('update');
}
// Otherwise if first deploy of existing function let's ask to make sure
return inquirer.prompt([PROMPT_CONFIRM_OVERWRITE_FUNCTION])
.then(function (result) {
if (result.confirm) {
return doDeploy('update');
}
console.log('You answered no, aborting deploy');
});
});
}
function doDeploy(type) {
const deployFunc = type === 'new' ? createFunction : updateFunction;
return startWith({})
.then(chainData(
() => readFile(path.join(TARGET_ROOT, 'package.json'), JSON.parse),
packageJson => ({ packageJson })
))
.then(chainData(createBundle, ignoreData))
.then(deployFunc)
.then(handleSuccess)
.catch(err => {
console.log('No config found, first run: lambdasync init');
console.error(err);
return err;
});
}
function handleSuccess(result) {
promisedExec(LAMBDASYNC_BIN + '/rimraf deploy.zip', targetOptions);
return updateSettings({
lambdaArn: stripLambdaVersion(result.FunctionArn),
lambdaRole: result.Role
})
.then(settings => {
let template = fs.readFileSync(path.join(LAMBDASYNC_SRC, 'markdown', 'function-success.md'), 'utf8');
template += markdownProperty({
key: 'apiGatewayUrl',
label: 'API URL'
}, settings);
console.log(markdown({
templateString: template,
data: settings
}));
return settings;
});
}
function ensureDir(dir) {
return new Promise((resolve, reject) => {
mkdirp(dir, () => {
return resolve();
});
});
}
function createBundle({packageJson = {}}) {
return ensureDir(TARGET_DEPLOY_DIR)
.then(() => clearDeployDir(packageJson))
.then(() => {
copyPackageJson(TARGET_ROOT, TARGET_DEPLOY_DIR);
return {packageJson};
})
.then(chainData(ensureDependencies, ignoreData))
.then(chainData(
({ packageJson }) => {
const { ignore, include } = packageJson.lambdasync || {};
return copyFiles(ignore, include);
},
ignoreData
))
.then(zip)
.then(() => process.chdir(TARGET_ROOT));
}
function shouldIncludeNodeModules(packageJson) {
const { ignore, include } = packageJson.lambdasync || {};
const nodeModulesPath = 'node_modules/module';
const ignoreMatch = ignore ? matchesPatterns(nodeModulesPath, ignore) : false;
return !ignoreMatch;
}
function ensureDependencies({ packageJson }) {
return ensureDir(TARGET_HIDDEN_DIR)
.then(() => {
return new Promise((resolve, reject) => {
fs.stat(path.join(TARGET_DEPLOY_DIR, 'node_modules'), (err, res) => {
if (err) {
return resolve(false);
}
return resolve(res.isDirectory())
})
});
})
.then(chainData(nodeModulesExists => {
const currentDependencyHash = hashPackageDependencies(packageJson);
if (!shouldIncludeNodeModules(packageJson)) {
return {
shouldUpdateNodeModules: false,
currentDependencyHash,
};
}
return readFile(path.join(TARGET_HIDDEN_DIR, DEPENDENCY_HASH_FILE))
.catch(err => '')
.then(oldHash => {
if (oldHash === currentDependencyHash && nodeModulesExists) {
return false;
}
return true;
})
.then(shouldUpdateNodeModules => ({
shouldUpdateNodeModules,
currentDependencyHash,
}));
}))
.then(chainData(({shouldUpdateNodeModules}) => {
if (!shouldUpdateNodeModules) {
return;
}
return updateDependencies();
}, ignoreData))
.then(({ currentDependencyHash }) => writeFile(path.join(TARGET_HIDDEN_DIR, DEPENDENCY_HASH_FILE), currentDependencyHash));
}
function updateDependencies() {
process.chdir(TARGET_DEPLOY_DIR);
return npmInstall('--production')
.then(() => process.chdir(TARGET_ROOT));
}
const lambdasyncIgnores = ['.git/**', '.lambdasync/**', 'lambdasync.json', 'package.json', 'package-lock.json', 'node_modules/**'];
function copyFiles(ignore, include) {
return copy(TARGET_ROOT, TARGET_DEPLOY_DIR, {
filter: function copyFilter(path) {
const ignoreMatch = ignore ? matchesPatterns(path, ignore) : false;
const lambdasyncIgnoreMatch = matchesPatterns(path, lambdasyncIgnores);
const includeMatch = include ? matchesPatterns(path, include) : true;
if (path && includeMatch && !ignoreMatch && !lambdasyncIgnoreMatch) {
return true;
}
return false;
}
});
}
function matchesPatterns(path, patterns = []) {
if (patterns.length === 0) {
return true;
}
return patterns.reduce((res, pattern) => {
if (res) { return res; }
return minimatch(path, pattern, { dot: true });
}, false);
}
function clearDeployDir(packageJson) {
let dirContentPattern = '*';
if (shouldIncludeNodeModules(packageJson)) {
dirContentPattern = '!(node_modules)';
}
return new Promise((resolve, reject) => {
rimraf(`${TARGET_DEPLOY_DIR}/${dirContentPattern}`, (err) => {
if (err) {
return reject(err);
}
return resolve();
});
});
}
function zip() {
return promisedExec(`${LAMBDASYNC_BIN}/bestzip ${TARGET_ROOT}/deploy.zip ./*`, {
cwd: TARGET_DEPLOY_DIR
});
}
function getHandlerPath(entryConfig) {
return `${entryConfig ?
removeFileExtension(removeCurrentPath(makeAbsolutePath(entryConfig))) :
'index'
}.handler`;
}
function updateFunction({packageJson = {}}) {
// If there is a custom entry point set incorporate it into the Handler
const { entry } = packageJson.lambdasync || {};
const Handler = getHandlerPath(entry);
return awsPromise(lambda, 'updateFunctionConfiguration', {
FunctionName: settings.lambdaName,
Handler,
})
.then(updateFunctionCode);
}
function updateFunctionCode() {
return new Promise((resolve, reject) => {
lambda.updateFunctionCode({
FunctionName: settings.lambdaName,
Publish: true,
ZipFile: fs.readFileSync('./deploy.zip')
}, (err, data) => {
if (err) {
return reject(err);
}
return resolve(data);
});
});
}
function createFunction({ packageJson }) {
const { entry } = packageJson.lambdasync || {};
const Handler = getHandlerPath(entry);
return new Promise((resolve, reject) => {
lambda.createFunction({
Code: {
ZipFile: fs.readFileSync('./deploy.zip')
},
FunctionName: settings.lambdaName,
Handler,
Role: settings.lambdaRole,
Runtime: 'nodejs6.10', /* required */
Description: description, // package.json description
MemorySize: 128, // default
Publish: true,
Timeout: 3
}, (err, data) => {
if (err) {
return reject(err);
}
return resolve(data);
});
});
}
module.exports = deploy;