aws-lambda-sync
Version:
This npm module is used to upload and download lambda functions from AWS
412 lines (365 loc) • 14.1 kB
JavaScript
let download = require("download-file");
let cmd = require("node-cmd");
var zipper = require("zip-local");
let extract = require("extract-zip");
var fs = require("fs");
var path = require("path");
const hidefile = require("hidefile");
let downloadFile = async function(dir, filename, url) {
let options = {
directory: dir,
filename: filename
};
return new Promise((success, failure) => {
download(url, options, function(err) {
if (err) {
failure(err);
} else {
success("done");
}
});
});
};
let extractZip = async function(source, target) {
return new Promise((success, failure) => {
extract(source, { dir: target }, function(err) {
if (err) {
failure(err);
} else {
success("done");
}
});
});
};
let getAllFunctionList = async function(profile) {
return new Promise((success, failure) => {
cmd.get(`aws lambda --profile ${profile} list-functions`, function(err, data, stderr) {
if (err || stderr) {
failure(err || stderr);
} else {
success(data);
}
});
});
};
let uploadFunction = async function(name, zipFile, profile) {
return new Promise((success, failure) => {
cmd.get(`aws lambda update-function-code --profile ${profile} --function-name ${name} --zip-file ${zipFile}`, function(err, data, stderr) {
if (err || stderr) {
failure(err || stderr);
} else {
success(data);
}
});
});
};
let createFunction = async function(name, zipFile, profile) {
return new Promise((success, failure) => {
cmd.get(`aws lambda create-function-code --profile ${profile} --function-name ${name} --zip-file ${zipFile}`, function(err, data, stderr) {
if (err || stderr) {
failure(err || stderr);
} else {
success(data);
}
});
});
};
let getFunctionDescription = async function(name, profile) {
return new Promise((success, failure) => {
cmd.get(`aws lambda get-function --profile ${profile} --function-name ${name}`, function(err, data, stderr) {
if (err || stderr) {
failure(err || stderr);
} else {
success(data);
}
});
});
};
Object.defineProperty(Date.prototype, "YYYYMMDDHHMMSS", {
value: function() {
function pad2(n) {
// always returns a string
return (n < 10 ? "0" : "") + n;
}
return this.getFullYear() + pad2(this.getMonth() + 1) + pad2(this.getDate()) + pad2(this.getHours()) + pad2(this.getMinutes()) + pad2(this.getSeconds());
}
});
function sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
function createBackupPaths() {
if (!fs.existsSync(BASE_SOURCE_PATH + ".backup")) {
fs.mkdirSync(BASE_SOURCE_PATH + ".backup");
}
if (!fs.existsSync(BASE_SOURCE_PATH + ".config")) {
fs.mkdirSync(BASE_SOURCE_PATH + ".config");
}
hidefile.hideSync(BASE_SOURCE_PATH + ".backup");
hidefile.hideSync(BASE_SOURCE_PATH + ".config");
}
function validateArgs() {
let index_file_path = 2;
let index_profile = 3;
let index_action = 4;
let index_function_name = 5;
let min_arg_count = 6;
if (process.argv.length >= min_arg_count) {
let basepath = path.resolve(process.argv[index_file_path]);
if (basepath.endsWith("/")) {
BASE_SOURCE_PATH = basepath;
} else {
BASE_SOURCE_PATH = basepath + "/";
}
console.log("Processing the source path " + BASE_SOURCE_PATH);
PROFILE = process.argv[index_profile];
if (VALID_ACTIONS.includes(process.argv[index_action].toLowerCase())) {
ACTION = process.argv[index_action].toLowerCase();
} else {
console.error("Invalid Action");
printError();
return false;
}
if (process.argv[index_function_name] === "ALL" && process.argv.length == min_arg_count) {
IS_ALL = true;
} else if (process.argv[index_function_name].endsWith("*") && process.argv.length == min_arg_count) {
WILD_CARD = process.argv[index_function_name].split("*")[0];
FUNCTION_NAMES.push(process.argv[index_function_name]);
} else if (process.argv[index_function_name] != "ALL" && process.argv.length >= min_arg_count) {
for (let i = index_function_name; i < process.argv.length; i++) {
if (process.argv[i].endsWith("*")) {
console.error("Invalid Combination Wildcard with Function Name");
printError();
return false;
}
FUNCTION_NAMES.push(process.argv[i]);
}
} else {
console.error("Invalid Argument Count");
printError();
return false;
}
console.log("Action " + ACTION + " initiated For " + (IS_ALL ? "ALL" : JSON.stringify(FUNCTION_NAMES)) + " Function(s) from " + BASE_SOURCE_PATH);
} else {
console.error("Invalid Argument Count");
printError();
return false;
}
return true;
}
function printError() {
console.log("Error in Arguments, Try with the following");
console.log("aws_lambda_sync <Source_Path> <profile> download ALL");
console.log("aws_lambda_sync <Source_Path> <profile> download my_lambda1");
console.log("aws_lambda_sync <Source_Path> <profile> download my_lambda1 my_lambda2");
console.log("aws_lambda_sync <Source_Path> <profile> download my_lambda*");
console.log("aws_lambda_sync <Source_Path> <profile> upload ALL");
console.log("aws_lambda_sync <Source_Path> <profile> upload my_lambda1");
console.log("aws_lambda_sync <Source_Path> <profile> upload my_lambda1 my_lambda2");
console.log("aws_lambda_sync <Source_Path> <profile> upload my_lambda*");
}
function getDirectories(path) {
return fs.readdirSync(path).filter(function(file) {
return fs.statSync(path + "/" + file).isDirectory();
});
}
/**
* Global Variables
*/
let ACTION = "";
let VALID_ACTIONS = ["upload", "download"];
let IS_ALL = false;
let FUNCTION_NAMES = [];
let PROFILE;
let WILD_CARD = "w2@w3Sde#";
var BASE_SOURCE_PATH;
let init = async function() {
try {
/**
* Validating Arguments & Creating Paths
*/
if (!validateArgs()) {
return;
}
createBackupPaths();
let allFunctionListResult = JSON.parse(await getAllFunctionList(PROFILE)).Functions;
let function_names = allFunctionListResult.map(function(c) {
return c.FunctionName;
});
const dir_names = getDirectories(BASE_SOURCE_PATH);
if (dir_names.includes("node_modules")) {
dir_names.splice(dir_names.indexOf("node_modules"), 1);
}
if (dir_names.includes(".config")) {
dir_names.splice(dir_names.indexOf(".config"), 1);
}
if (dir_names.includes(".backup")) {
dir_names.splice(dir_names.indexOf(".backup"), 1);
}
console.log("All Functions Count :" + function_names.length);
console.log("All Dir Count :" + dir_names.length);
let new_dirs = dir_names.filter(x => !function_names.includes(x));
console.log("New Dir Count :" + new_dirs.length);
let all_fn_names = function_names.concat(new_dirs);
all_fn_names.map(async functionName => {
let functionDescriptionResult;
if (IS_ALL || FUNCTION_NAMES.includes(functionName) || functionName.startsWith(WILD_CARD)) {
/**
* Variables
*/
var localSourceFolder = BASE_SOURCE_PATH + `${functionName}`;
var localSourceFolderPath = localSourceFolder + "/";
var backupFolderPath = BASE_SOURCE_PATH + ".backup/";
var configFolderPath = BASE_SOURCE_PATH + ".config/";
var zipFileName = `${functionName}.zip`;
var zipFilePath = BASE_SOURCE_PATH + zipFileName;
var awsBackupZipFileName = `${functionName}_` + new Date().YYYYMMDDHHMMSS() + "_AWS.zip";
var locBackupZipFileName = `${functionName}_` + new Date().YYYYMMDDHHMMSS() + "_LOC.zip";
var awsBackupZipFilePath = backupFolderPath + awsBackupZipFileName;
var locBackupZipFilePath = backupFolderPath + locBackupZipFileName;
var configFileName = configFolderPath + `/${functionName}.config`;
/**
* Delete Old Uploaded/Downloaded Zip from Local if exists
*/
if (fs.existsSync(zipFilePath)) {
fs.unlinkSync(zipFilePath);
}
functionDescriptionResult = JSON.parse(await getFunctionDescription(functionName, PROFILE));
if (ACTION === "download") {
/**
* Download Steps
*
* 1. Delete the Downloaded Zip, if exist (already uploaded/cancelled)
* check for local changes in git, if so throw error
* get functionDescriptionResult
if revision number is different from functionDescriptionResult and local config then do the following, else return
* 2. Backup Local Folder, if exist
* 3. Download Zip File from AWS
* 4. Delete Local Folder, if Exist
* 5. Extract the Downloaded Zip
* save the config
* 6. Delete the Downloaded Zip
*/
/**
* Backup Local Folder if exists
*/
if (fs.existsSync(localSourceFolder)) {
/**
* Allow if only the source and destinations having different revision ID
*/
if (fs.existsSync(configFileName)) {
let localConfig = JSON.parse(fs.readFileSync(configFileName));
if (localConfig.RevisionId === functionDescriptionResult.Configuration.RevisionId) {
console.log("Skipping Download for " + functionName + " Reason : No Changes in AWS");
return;
}
}
zipper.sync
.zip(localSourceFolderPath)
.compress()
.save(locBackupZipFilePath);
if (fs.existsSync(locBackupZipFilePath)) {
console.log("Local Backup Completed for ", functionName);
} else {
console.log("Local Backup Failed for ", functionName);
return;
}
}
/**
* Download Zip from AWS
*/
await downloadFile(BASE_SOURCE_PATH, zipFileName, functionDescriptionResult.Code.Location);
fs.writeFileSync(configFileName, JSON.stringify(functionDescriptionResult.Configuration));
await sleep(500);
if (fs.existsSync(zipFilePath)) {
console.log("Download Completed for Function", functionName);
/**
* Delete Local Folder
*/
if (fs.existsSync(localSourceFolder)) {
fs.rmdirSync(localSourceFolder, { recursive: true });
}
fs.mkdirSync(localSourceFolder);
/**
* Extract downloaded Zip to Local Folder
*/
await extractZip(zipFilePath, path.resolve(localSourceFolderPath));
/**
* Delete downloaded Zip from Local
*/
fs.unlinkSync(zipFilePath);
console.log("Extract Completed for Function", functionName);
} else {
console.log("Download Failed for Function", functionName);
return;
}
} else if (ACTION === "upload" && !new_dirs.includes(functionName)) {
/**
* Upload Steps
*
* 1. Delete the Downloaded Zip, if exist (already uploaded/cancelled)
* get functionDescriptionResult
* if revision number is same then continue Upload, else ask to download and update
* 2. Zip the local folder
* 3. Download and Backup the function from AWS
* 4. Upload the Zip Folder
* update the config
* 5. Delete the Downloaded Zip
*/
/**
* Allow if only the source and destinations having same revision ID
*/
if (fs.existsSync(configFileName)) {
let localConfig = JSON.parse(fs.readFileSync(configFileName));
if (localConfig.RevisionId !== functionDescriptionResult.Configuration.RevisionId) {
console.log("Skipping Upload for " + functionName + " Reason : Code Changed in AWS." + "\nplease pull/download the latest changes from AWS and merge your changes then upload");
return;
}
}
/**
* Zip Local Folder to upload
*/
zipper.sync
.zip(localSourceFolderPath)
.compress()
.save(zipFilePath);
console.log("Local Zip creation to upload Completed for ", functionName);
/**
* Backup from AWS to Local Folder
*/
await downloadFile(backupFolderPath, awsBackupZipFileName, functionDescriptionResult.Code.Location);
await sleep(500);
if (fs.existsSync(awsBackupZipFilePath)) {
console.log("AWS Backup Completed for ", functionName);
} else {
console.log("Backup Zip Not Exists, AWS Backup Failed for ", functionName);
return;
}
if (fs.existsSync(zipFilePath)) {
/**
* Upload to AWS
*/
await uploadFunction(functionName, "fileb://" + zipFilePath, PROFILE);
/**
* Delete Local zip Uploaded
*/
fs.unlinkSync(zipFilePath);
console.log("Upload Completed for ", functionName);
} else {
console.log("Local Zip Not Exists, Failed to Upload for ", functionName);
return;
}
} else if (ACTION === "upload" && new_dirs.includes(functionName)) {
/**
* Create New Function
*/
}
}
});
} catch (e) {
console.log("error", e);
}
};
init();