sls-dev-tools
Version:
The Dev Tools for the Serverless World
422 lines (345 loc) • 14.6 kB
JavaScript
"use strict";
var _constants = require("../../constants");
var _resourceTableConfig = require("./resourceTableConfig");
var _modals = require("../../modals");
var _abbreviateFunction = require("../../utils/abbreviateFunction");
var _services = require("../../services");
var _stackResources = require("../../services/stackResources");
var _padString = require("../../utils/padString");
const contrib = require("blessed-contrib");
const open = require("open");
const {
exec
} = require("child_process");
const emoji = require("node-emoji");
const moment = require("moment");
class ResourceTable {
constructor(application, screen, program, provider, slsDevToolsConfig, profile, location, cloudformation, lambda, cloudwatch, cloudwatchLogs) {
this.application = application;
this.lambdaFunctions = {};
this.fullFunctionNames = {};
this.latestLambdaFunctionsUpdateTimestamp = -1;
this.program = program;
this.cloudformation = cloudformation;
this.table = this.generateLambdaTable();
this.funcName = null;
this.fullFuncName = null;
this.table.rows.on("select", item => {
[this.funcName] = item.data;
this.fullFuncName = this.getFullFunctionName(this.funcName);
});
this.provider = provider;
this.slsDevToolsConfig = slsDevToolsConfig;
this.lambdasDeploymentStatus = {};
this.profile = profile;
this.location = location;
this.type = _resourceTableConfig.RESOURCE_TABLE_TYPE.LAMBDA;
this.lambda = lambda;
this.screen = screen;
this.cloudwatch = cloudwatch;
this.cloudwatchLogs = cloudwatchLogs;
this.setKeypresses();
}
updateAPIs(profile, cloudformation, lambda, cloudwatch, cloudwatchLogs) {
this.profile = profile;
this.cloudformation = cloudformation;
this.lambda = lambda;
this.cloudwatch = cloudwatch;
this.cloudwatchLogs = cloudwatchLogs;
}
getFullFunctionName(abbreviatedFunctionName) {
return this.fullFunctionNames[abbreviatedFunctionName];
}
isOnFocus() {
return this.application.focusIndex === _constants.DASHBOARD_FOCUS_INDEX.RESOURCE_TABLE;
}
isLambdaTable() {
return this.type === _resourceTableConfig.RESOURCE_TABLE_TYPE.LAMBDA;
}
setKeypresses() {
this.screen.key(["l"], () => {
if (this.isOnFocus() && this.application.isModalOpen === false) {
this.application.isModalOpen = true;
return (0, _modals.lambdaStatisticsModal)(this.screen, this.application, this.getCurrentlyOnHoverFullLambdaName(), this.cloudwatchLogs, this.cloudwatch, this.lambda, this.lambdaFunctions[this.getCurrentlyOnHoverFullLambdaName()]);
}
return 0;
});
this.screen.key(["i"], () => {
if (this.isOnFocus() && this.isLambdaTable() && this.application.isModalOpen === false) {
this.application.isModalOpen = true;
const fullFunctionName = this.getCurrentlyOnHoverFullLambdaName();
const previousLambdaPayload = this.application.previousLambdaPayload[fullFunctionName];
return (0, _modals.lambdaInvokeModal)(this.screen, this.application, fullFunctionName, this.lambda, previousLambdaPayload);
}
return 0;
});
this.screen.key(["o"], () => {
if (this.isOnFocus() && this.isLambdaTable() && this.application.isModalOpen === false) {
return this.openLambdaInAWSConsole();
}
return 0;
});
this.screen.key(["d"], () => {
if (this.isOnFocus() && this.isLambdaTable() && this.application.isModalOpen === false) {
return this.deployFunction();
}
return 0;
});
this.screen.key(["right", "left"], () => {
if (this.isOnFocus() && this.application.isModalOpen === false) {
this.switchTable();
}
return 0;
});
this.screen.key(["s"], () => {
if (this.isOnFocus() && this.isLambdaTable() && this.application.isModalOpen === false) {
return this.deployStack();
}
return 0;
});
}
switchTable() {
switch (this.type) {
case _resourceTableConfig.RESOURCE_TABLE_TYPE.LAMBDA:
(0, _resourceTableConfig.switchTableConfig)(this, _resourceTableConfig.RESOURCE_TABLE_TYPE.ALL_RESOURCES);
break;
case _resourceTableConfig.RESOURCE_TABLE_TYPE.ALL_RESOURCES:
(0, _resourceTableConfig.switchTableConfig)(this, _resourceTableConfig.RESOURCE_TABLE_TYPE.LAMBDA);
break;
default:
return 0;
}
return this.updateData();
}
generateLambdaTable() {
return this.application.layoutGrid.set(0, 6, 4, 6, contrib.table, {
keys: true,
fg: "green",
label: _resourceTableConfig.RESOURCE_TABLE_CONFIG[_resourceTableConfig.RESOURCE_TABLE_TYPE.LAMBDA].label,
columnSpacing: 1,
columnWidth: _resourceTableConfig.RESOURCE_TABLE_CONFIG[_resourceTableConfig.RESOURCE_TABLE_TYPE.LAMBDA].columnWidth,
style: {
border: {
fg: "yellow"
}
}
});
}
getCurrentlyOnHoverLambdaName() {
const onHoverRow = this.table.rows.selected;
const [onHoverLambdaName] = this.table.rows.items[onHoverRow].data;
return onHoverLambdaName;
}
getCurrentlyOnHoverFullLambdaName() {
return this.getFullFunctionName(this.getCurrentlyOnHoverLambdaName());
}
async refreshLambdaFunctions() {
const allFunctions = await (0, _services.getLambdaFunctions)(this.lambda);
this.lambdaFunctions = allFunctions.reduce((map, func) => {
// eslint-disable-next-line no-param-reassign
map[func.FunctionName] = func;
return map;
}, {});
}
async updateData() {
const stackResources = await (0, _stackResources.getStackResources)(this.program.stackName, this.cloudformation);
if (stackResources) {
this.application.data = stackResources;
switch (this.type) {
case _resourceTableConfig.RESOURCE_TABLE_TYPE.LAMBDA:
this.updateLambdaTableData(stackResources);
break;
case _resourceTableConfig.RESOURCE_TABLE_TYPE.ALL_RESOURCES:
this.updateAllResourceTableData(stackResources);
break;
default:
break;
}
}
return 0;
}
async updateLambdaTableData(stackResources) {
let latestLastUpdatedTimestamp = -1;
const lambdaFunctionResources = stackResources.StackResourceSummaries.filter(res => {
const isLambdaFunction = res.ResourceType === "AWS::Lambda::Function";
if (isLambdaFunction) {
const lastUpdatedTimestampMilliseconds = moment(res.LastUpdatedTimestamp).valueOf();
if (lastUpdatedTimestampMilliseconds > latestLastUpdatedTimestamp) {
latestLastUpdatedTimestamp = lastUpdatedTimestampMilliseconds;
}
}
return isLambdaFunction;
});
if (latestLastUpdatedTimestamp > this.latestLambdaFunctionsUpdateTimestamp) {
// In case of update in the Lambda function resources,
// instead of getting updated function configurations one by one individually,
// we are getting all the functions' configurations in batch
// even though there will be unrelated ones with the stack.
// Because this should result with less API calls in most cases.
this.refreshLambdaFunctions();
this.latestLambdaFunctionsUpdateTimestamp = latestLastUpdatedTimestamp;
}
this.table.data = lambdaFunctionResources.map(lam => {
const funcName = lam.PhysicalResourceId;
const func = this.lambdaFunctions[funcName];
const shortenedFuncName = (0, _abbreviateFunction.abbreviateFunction)(lam.PhysicalResourceId, this.program.stackName);
this.fullFunctionNames[shortenedFuncName] = funcName;
let timeout = "?";
let memory = "?";
let funcRuntime = "?";
let layersPresent = "?";
if (func) {
funcRuntime = func.Runtime;
timeout = func.Timeout.toString();
memory = func.MemorySize.toString();
layersPresent = func.Layers ? "Y" : "N";
} // Max timout is 900 seconds, align values with whitespace
timeout = (0, _padString.padString)(timeout, 3); // Max memory is 3008 MB, align values with whitespace
memory = (0, _padString.padString)(memory, 4);
return [shortenedFuncName, moment(lam.LastUpdatedTimestamp).format("MMMM Do YYYY, h:mm:ss a"), `${memory} MB`, `${timeout} secs`, funcRuntime, `${layersPresent}`];
});
this.updateLambdaTableRows();
this.updateLambdaDeploymentStatus();
}
updateAllResourceTableData(stackResources) {
const resources = stackResources.StackResourceSummaries;
this.table.data = resources.map(resource => {
const resourceName = resource.LogicalResourceId;
const resourceType = resource.ResourceType.replace("AWS::", "");
return [resourceName, resourceType];
});
this.updateAllResourcesTableRows();
}
openLambdaInAWSConsole() {
if (this.type === _resourceTableConfig.RESOURCE_TABLE_TYPE.LAMBDA) {
return open(`https://${this.program.region}.console.aws.amazon.com/lambda/home?region=${this.program.region}#/functions/${this.getCurrentlyOnHoverFullLambdaName()}?tab=configuration`);
}
return 0;
}
deployFunction() {
const selectedRowIndex = this.table.rows.selected;
if (selectedRowIndex !== -1) {
const selectedLambdaFunctionName = this.getCurrentlyOnHoverLambdaName();
if (this.provider === "serverlessFramework") {
exec(`serverless deploy -f ${selectedLambdaFunctionName} -r ${this.program.region} --aws-profile ${this.profile} ${this.slsDevToolsConfig && this.slsDevToolsConfig.deploymentArgs ? this.slsDevToolsConfig.deploymentArgs : ""}`, {
cwd: this.location
}, (error, stdout) => {
console.log(error);
return this.handleFunctionDeployment(error, stdout, selectedLambdaFunctionName, selectedRowIndex);
});
} else if (this.provider === "SAM") {
console.error("ERROR: UNABLE TO DEPLOY SINGLE FUNCTION WITH SAM. PRESS s TO DEPLOY STACK");
return;
}
this.flashRow(selectedRowIndex);
this.lambdasDeploymentStatus[selectedLambdaFunctionName] = _constants.DEPLOYMENT_STATUS.PENDING;
this.updateLambdaTableRows();
}
}
updateAllResourcesTableRows() {
this.table.setData({
headers: ["logical", "type"],
data: this.table.data
});
}
updateLambdaTableRows() {
const lambdaFunctionsWithDeploymentIndicator = JSON.parse(JSON.stringify(this.table.data));
let deploymentIndicator;
for (let i = 0; i < this.table.data.length; i++) {
deploymentIndicator = null;
switch (this.lambdasDeploymentStatus[this.table.data[i][0]]) {
case _constants.DEPLOYMENT_STATUS.PENDING:
deploymentIndicator = emoji.get("coffee");
break;
case _constants.DEPLOYMENT_STATUS.SUCCESS:
deploymentIndicator = emoji.get("sparkles");
break;
case _constants.DEPLOYMENT_STATUS.ERROR:
deploymentIndicator = emoji.get("x");
break;
default:
break;
}
if (deploymentIndicator) {
lambdaFunctionsWithDeploymentIndicator[i][0] = `${deploymentIndicator} ${this.table.data[i][0]}`;
}
}
this.table.setData({
headers: ["logical", "updated", "memory", "timeout", "runtime", "layers"],
data: lambdaFunctionsWithDeploymentIndicator
});
for (let i = 0; i < this.table.data.length; i++) {
this.table.rows.items[i].data = this.table.data[i];
}
}
handleFunctionDeployment(error, stdout, lambdaName, lambdaIndex) {
if (error) {
console.error(error);
this.lambdasDeploymentStatus[lambdaName] = _constants.DEPLOYMENT_STATUS.ERROR;
} else {
console.log(stdout);
this.lambdasDeploymentStatus[lambdaName] = _constants.DEPLOYMENT_STATUS.SUCCESS;
}
this.unflashRow(lambdaIndex);
this.updateLambdaTableRows();
}
flashRow(rowIndex) {
this.table.rows.items[rowIndex].style.fg = "blue";
this.table.rows.items[rowIndex].style.bg = "green";
}
unflashRow(rowIndex) {
this.table.rows.items[rowIndex].style.fg = () => rowIndex === this.table.rows.selected ? "white" : "green";
this.table.rows.items[rowIndex].style.bg = () => rowIndex === this.table.rows.selected ? "blue" : "default";
}
updateLambdaDeploymentStatus() {
Object.entries(this.lambdasDeploymentStatus).forEach(([key, value]) => {
if (value === _constants.DEPLOYMENT_STATUS.SUCCESS || value === _constants.DEPLOYMENT_STATUS.ERROR) {
this.lambdasDeploymentStatus[key] = undefined;
}
});
}
deployStack() {
if (this.provider === "serverlessFramework") {
exec(`serverless deploy -r ${this.program.region} --aws-profile ${this.profile} ${this.slsDevToolsConfig ? this.slsDevToolsConfig.deploymentArgs : ""}`, {
cwd: this.location
}, (error, stdout) => this.handleStackDeployment(error, stdout));
} else if (this.provider === "SAM") {
exec("sam build", {
cwd: this.location
}, error => {
if (error) {
console.error(error);
Object.keys(this.lambdasDeploymentStatus).forEach( // eslint-disable-next-line no-return-assign
functionName => this.lambdasDeploymentStatus[functionName] = _constants.DEPLOYMENT_STATUS.ERROR);
} else {
exec(`sam deploy --region ${this.program.region} --profile ${this.profile} --stack-name ${this.program.stackName} ${this.slsDevToolsConfig ? this.slsDevToolsConfig.deploymentArgs : ""}`, {
cwd: this.location
}, (deployError, stdout) => this.handleStackDeployment(deployError, stdout));
}
});
}
this.table.data.forEach((v, i) => {
this.flashRow(i);
this.lambdasDeploymentStatus[this.table.rows.items[i].data[0]] = _constants.DEPLOYMENT_STATUS.PENDING;
});
this.updateLambdaTableRows();
}
handleStackDeployment(error, stdout) {
if (error) {
console.error(error);
Object.keys(this.lambdasDeploymentStatus).forEach( // eslint-disable-next-line no-return-assign
functionName => this.lambdasDeploymentStatus[functionName] = _constants.DEPLOYMENT_STATUS.ERROR);
} else {
console.log(stdout);
Object.keys(this.lambdasDeploymentStatus).forEach( // eslint-disable-next-line no-return-assign
functionName => this.lambdasDeploymentStatus[functionName] = _constants.DEPLOYMENT_STATUS.SUCCESS);
}
this.table.data.forEach((v, i) => {
this.unflashRow(i);
});
this.updateLambdaTableRows();
}
}
module.exports = {
ResourceTable
};