cn-shell
Version:
Cloud Native Shell
615 lines (614 loc) • 17.9 kB
JavaScript
"use strict";
var __createBinding =
(this && this.__createBinding) ||
(Object.create
? function (o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (
!desc ||
("get" in desc ? !m.__esModule : desc.writable || desc.configurable)
) {
desc = {
enumerable: true,
get: function () {
return m[k];
},
};
}
Object.defineProperty(o, k2, desc);
}
: function (o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
});
var __setModuleDefault =
(this && this.__setModuleDefault) ||
(Object.create
? function (o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}
: function (o, v) {
o["default"] = v;
});
var __importStar =
(this && this.__importStar) ||
function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null)
for (var k in mod)
if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k))
__createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault =
(this && this.__importDefault) ||
function (mod) {
return mod && mod.__esModule ? mod : { default: mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Shell =
exports.undici =
exports.ConfigTypes =
exports.ConfigMan =
exports.ShellExt =
exports.LogLevel =
exports.Logger =
void 0;
const logger_js_1 = require("./logger.js");
Object.defineProperty(exports, "Logger", {
enumerable: true,
get: function () {
return logger_js_1.Logger;
},
});
Object.defineProperty(exports, "LogLevel", {
enumerable: true,
get: function () {
return logger_js_1.LogLevel;
},
});
const logger_console_js_1 = require("./logger-console.js");
const shell_ext_js_1 = require("./shell-ext.js");
Object.defineProperty(exports, "ShellExt", {
enumerable: true,
get: function () {
return shell_ext_js_1.ShellExt;
},
});
const config_man_js_1 = require("./config-man.js");
Object.defineProperty(exports, "ConfigMan", {
enumerable: true,
get: function () {
return config_man_js_1.ConfigMan;
},
});
Object.defineProperty(exports, "ConfigTypes", {
enumerable: true,
get: function () {
return config_man_js_1.ConfigTypes;
},
});
const shelljs_1 = __importDefault(require("shelljs"));
const undici_1 = require("undici");
const undici = __importStar(require("undici"));
exports.undici = undici;
const http = __importStar(require("node:http"));
const os = __importStar(require("node:os"));
const readline = __importStar(require("node:readline"));
const CFG_LOG_LEVEL = "LOG_LEVEL";
const CFG_LOG_TIMESTAMP = "LOG_TIMESTAMP";
const CFG_LOG_TIMESTAMP_FORMAT = "LOG_TIMESTAMP_FORMAT";
const CFG_HTTP_KEEP_ALIVE_TIMEOUT = "HTTP_KEEP_ALIVE_TIMEOUT";
const CFG_HTTP_HEADER_TIMEOUT = "HTTP_HEADER_TIMEOUT";
const CFG_HTTP_HEALTHCHECK_PORT = "HTTP_HEALTHCHECK_PORT";
const CFG_HTTP_HEALTHCHECK_INTERFACE = "HTTP_HEALTHCHECK_INTERFACE";
const CFG_HTTP_HEALTHCHECK_PATH = "HTTP_HEALTHCHECK_PATH";
const CFG_HTTP_HEALTHCHECK_GOOD_RES = "HTTP_HEALTHCHECK_GOOD_RES";
const CFG_HTTP_HEALTHCHECK_BAD_RES = "HTTP_HEALTHCHECK_BAD_RES";
const DEFAULT_SHELL_CONFIG = {
appVersion: "N/A",
log: {
level: "INFO",
timestamp: false,
timestampFormat: "ISO",
},
http: {
keepAliveTimeout: 65000,
headerTimeout: 66000,
healthcheckPort: 8080,
healthcheckInterface: "",
healthcheckPath: "/healthcheck",
healthcheckGoodRes: 200,
healthcheckBadRes: 503,
},
};
const DEFAULT_CONFIG_OPTIONS = {
envVarPrefix: "CNA_",
};
const DEFAULT_QUESTION_OPTIONS = {
muteAnswer: false,
muteChar: "*",
};
const DEFAULT_HTTP_REQ_POOL_OPTIONS = {};
const DEFAULT_HTTP_REQ_OPTIONS = {
method: "GET",
};
const NODE_ENV =
process.env.NODE_ENV === undefined ? "development" : process.env.NODE_ENV;
const LOGGER_APP_NAME = "App";
class Shell {
_name;
_appVersion;
_configMan;
_logger;
_httpKeepAliveTimeout;
_httpHeaderTimeout;
_healthcheckPort;
_healthcheckInterface;
_healthCheckPath;
_healthCheckGoodResCode;
_healthCheckBadResCode;
_healthcheckServer;
_exts;
_httpReqPools;
cat = shelljs_1.default.cat;
cd = shelljs_1.default.cd;
chmod = shelljs_1.default.chmod;
cp = shelljs_1.default.cp;
dirs = shelljs_1.default.dirs;
echo = shelljs_1.default.echo;
env = shelljs_1.default.env;
testError = shelljs_1.default.error;
exec = shelljs_1.default.exec;
find = shelljs_1.default.find;
grep = shelljs_1.default.grep;
head = shelljs_1.default.head;
ln = shelljs_1.default.ln;
ls = shelljs_1.default.ls;
mkdir = shelljs_1.default.mkdir;
mv = shelljs_1.default.mv;
popd = shelljs_1.default.popd;
pushd = shelljs_1.default.pushd;
pwd = shelljs_1.default.pwd;
rm = shelljs_1.default.rm;
sed = shelljs_1.default.sed;
set = shelljs_1.default.set;
sort = shelljs_1.default.sort;
tail = shelljs_1.default.tail;
tempdir = shelljs_1.default.tempdir;
touch = shelljs_1.default.touch;
uniq = shelljs_1.default.uniq;
constructor(passedConfig) {
let config = {
name: passedConfig.name,
appVersion:
passedConfig.appVersion === undefined
? DEFAULT_SHELL_CONFIG.appVersion
: passedConfig.appVersion,
http: {
...DEFAULT_SHELL_CONFIG.http,
...passedConfig.http,
},
log: {
...DEFAULT_SHELL_CONFIG.log,
...passedConfig.log,
},
};
this._name = config.name;
this._appVersion = config.appVersion;
this._configMan = new config_man_js_1.ConfigMan();
this._exts = [];
this._httpReqPools = {};
this.ls = shelljs_1.default.ls;
if (config.log?.logger !== undefined) {
this._logger = config.log.logger;
} else {
let logTimestamps = this.getConfigBool(CFG_LOG_TIMESTAMP, {
defaultVal: config.log.timestamp,
});
let logTimestampFormat = this.getConfigStr(CFG_LOG_TIMESTAMP_FORMAT, {
defaultVal: config.log.timestampFormat,
});
this._logger = new logger_console_js_1.LoggerConsole(
config.name,
logTimestamps,
logTimestampFormat,
);
}
this._logger.start();
let logLevel = this.getConfigStr(CFG_LOG_LEVEL, {
defaultVal: config.log.level,
});
switch (logLevel.toUpperCase()) {
case "SILENT":
this._logger.level = logger_js_1.LogLevel.LOG_COMPLETE_SILENCE;
break;
case "QUIET":
this._logger.level = logger_js_1.LogLevel.LOG_QUIET;
break;
case "INFO":
this._logger.level = logger_js_1.LogLevel.LOG_INFO;
break;
case "STARTUP":
this._logger.level = logger_js_1.LogLevel.LOG_START_UP;
break;
case "DEBUG":
this._logger.level = logger_js_1.LogLevel.LOG_DEBUG;
break;
case "TRACE":
this._logger.level = logger_js_1.LogLevel.LOG_TRACE;
break;
default:
this._logger.level = logger_js_1.LogLevel.LOG_INFO;
this._logger.warn(
`LogLevel ${logLevel} is unknown. Setting level to INFO.`,
);
break;
}
this._healthcheckInterface = this.getConfigStr(
CFG_HTTP_HEALTHCHECK_INTERFACE,
{ defaultVal: config.http.healthcheckInterface },
);
this._healthcheckPort = this.getConfigNum(CFG_HTTP_HEALTHCHECK_PORT, {
defaultVal: config.http.healthcheckPort,
});
this._httpKeepAliveTimeout = this.getConfigNum(
CFG_HTTP_KEEP_ALIVE_TIMEOUT,
{ defaultVal: config.http.keepAliveTimeout },
);
this._httpHeaderTimeout = this.getConfigNum(CFG_HTTP_HEADER_TIMEOUT, {
defaultVal: config.http.headerTimeout,
});
this._healthCheckPath = this.getConfigStr(CFG_HTTP_HEALTHCHECK_PATH, {
defaultVal: config.http.healthcheckPath,
});
this._healthCheckGoodResCode = this.getConfigNum(
CFG_HTTP_HEALTHCHECK_GOOD_RES,
{ defaultVal: config.http.healthcheckGoodRes },
);
this._healthCheckBadResCode = this.getConfigNum(
CFG_HTTP_HEALTHCHECK_BAD_RES,
{ defaultVal: config.http.healthcheckBadRes },
);
this.startup("Shell created!");
}
async start() {
this.startup("Started!");
return true;
}
async stop() {
this.startup("Stopped!");
return;
}
async healthCheck() {
this.debug("Health check called");
return true;
}
get name() {
return this._name;
}
get appVersion() {
return this._appVersion;
}
get logger() {
return this._logger;
}
set level(level) {
this._logger.level = level;
}
setupHealthcheck() {
if (this._healthcheckInterface.length === 0) {
this.startup(
"No HTTP interface specified for healthcheck endpoint - healthcheck disabled!",
);
return;
}
this.startup("Initialising healthcheck HTTP endpoint ...");
this.startup(`Finding IP for interface (${this._healthcheckInterface})`);
let ifaces = os.networkInterfaces();
this.startup("Interfaces on host: %j", ifaces);
if (ifaces[this._healthcheckInterface] === undefined) {
throw new Error(
`${this._healthcheckInterface} is not an interface on this server`,
);
}
let ip = "";
let found = ifaces[this._healthcheckInterface]?.find(
(i) => i.family === "IPv4",
);
if (found !== undefined) {
ip = found.address;
this.startup(
`Found IP (${ip}) for interface ${this._healthcheckInterface}`,
);
this.startup(
`Will listen on interface ${this._healthcheckInterface} (IP: ${ip})`,
);
}
if (ip.length === 0) {
throw new Error(
`${this._healthcheckInterface} is not an interface on this server`,
);
}
this.startup(
`Attempting to listen on (http://${ip}:${this._healthcheckPort})`,
);
this._healthcheckServer = http
.createServer((req, res) => this.healthcheckCallback(req, res))
.listen(this._healthcheckPort, ip);
this._healthcheckServer.keepAliveTimeout = this._httpKeepAliveTimeout;
this._healthcheckServer.headersTimeout = this._httpHeaderTimeout;
this.startup("Now listening. Healthcheck endpoint enabled!");
}
async startupError(code, testing) {
this.error("Heuston, we have a problem. Shutting down now ...");
if (testing) {
await this.exit(code, false);
return;
}
await this.exit(code);
}
async init(testing = false) {
this.startup("Initialising ...");
this.startup(`App Version (${this._appVersion})`);
this.startup(`NODE_ENV (${NODE_ENV})`);
for (let ext of this._exts) {
this.startup(`Attempting to start extension ${ext.name} ...`);
await ext.start().catch(async (e) => {
this.error(e);
await this.startupError(-1, testing);
});
}
this.startup("Attempting to start the application ...");
await this.start().catch(async (e) => {
this.error(e);
await this.startupError(-1, testing);
});
await this.setupHealthcheck();
this.startup("Setting up event handler for SIGINT and SIGTERM");
process.on("SIGINT", async () => await this.exit(0));
process.on("SIGTERM", async () => await this.exit(0));
this.startup("Ready to Rock and Roll baby!");
}
async exit(code, hard = true) {
this.startup("Exiting ...");
if (this._healthcheckServer !== undefined) {
this.startup("Closing healthcheck endpoint port now ...");
this._healthcheckServer.close();
this.startup("Port closed");
}
this.startup("Attempting to stop the application ...");
await this.stop().catch((e) => {
this.error(e);
});
for (let ext of this._exts.reverse()) {
this.startup(`Attempting to stop extension ${ext.name} ...`);
await ext.stop().catch((e) => {
this.error(e);
});
}
for (let origin in this._httpReqPools) {
this._httpReqPools[origin].destroy();
}
this.startup("So long and thanks for all the fish!");
this._logger.stop();
if (hard) {
process.exit(code);
}
}
getConfigStr(config, passedOptions = {}, appOrExtName = LOGGER_APP_NAME) {
let options = {
...DEFAULT_CONFIG_OPTIONS,
...passedOptions,
};
return this._configMan.get(
config,
config_man_js_1.ConfigTypes.String,
options,
appOrExtName,
this._logger,
);
}
getConfigBool(config, passedOptions = {}, appOrExtName = LOGGER_APP_NAME) {
let options = {
...DEFAULT_CONFIG_OPTIONS,
...passedOptions,
};
return this._configMan.get(
config,
config_man_js_1.ConfigTypes.Boolean,
options,
appOrExtName,
this._logger,
);
}
getConfigNum(config, passedOptions = {}, appOrExtName = LOGGER_APP_NAME) {
let options = {
...DEFAULT_CONFIG_OPTIONS,
...passedOptions,
};
return this._configMan.get(
config,
config_man_js_1.ConfigTypes.Number,
options,
appOrExtName,
this._logger,
);
}
async healthcheckCallback(req, res) {
if (
req.method?.toLowerCase() !== "get" ||
req.url !== this._healthCheckPath
) {
res.statusCode = 404;
res.end();
return;
}
let healthy = await this.healthCheck().catch((e) => {
this.error(e);
});
if (healthy) {
res.statusCode = this._healthCheckGoodResCode;
} else {
res.statusCode = this._healthCheckBadResCode;
}
res.end();
}
fatal(...args) {
this._logger.fatal(LOGGER_APP_NAME, ...args);
}
error(...args) {
this._logger.error(LOGGER_APP_NAME, ...args);
}
warn(...args) {
this._logger.warn(LOGGER_APP_NAME, ...args);
}
info(...args) {
this._logger.info(LOGGER_APP_NAME, ...args);
}
startup(...args) {
this._logger.startup(LOGGER_APP_NAME, ...args);
}
debug(...args) {
this._logger.debug(LOGGER_APP_NAME, ...args);
}
trace(...args) {
this._logger.trace(LOGGER_APP_NAME, ...args);
}
force(...args) {
this._logger.force(LOGGER_APP_NAME, ...args);
}
addExt(ext) {
this.startup(`Adding extension ${ext.name}`);
this._exts.push(ext);
}
async sleep(durationInSeconds) {
let ms = Math.round(durationInSeconds * 1000);
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async question(ask, passedOptions) {
let input = process.stdin;
let output = process.stdout;
let options = {
...DEFAULT_QUESTION_OPTIONS,
...passedOptions,
};
return new Promise((resolve) => {
let rl = readline.createInterface({
input,
output,
});
if (options.muteAnswer) {
input.on("keypress", () => {
var len = rl.line.length;
if (options.muteChar.length === 0) {
readline.moveCursor(output, -1, 0);
readline.clearLine(output, 1);
} else {
readline.moveCursor(output, -len, 0);
readline.clearLine(output, 1);
for (var i = 0; i < len; i++) {
output.write(options.muteChar[0]);
}
}
});
}
rl.question(ask, (answer) => {
resolve(answer);
rl.close();
});
});
}
createHttpReqPool(origin, passedOptions) {
let options = {
...DEFAULT_HTTP_REQ_POOL_OPTIONS,
...passedOptions,
};
this.trace(
"Creating new HTTP pool for (%s) with options (%j)",
origin,
passedOptions,
);
if (this._httpReqPools[origin] !== undefined) {
throw new Error(`A HTTP pool already exists for ${origin}`);
}
this._httpReqPools[origin] = new undici_1.Pool(origin, options);
}
async httpReq(origin, path, passedOptions) {
let options = {
...DEFAULT_HTTP_REQ_OPTIONS,
...passedOptions,
};
this.trace("httpReq for origin (%s) path (%s)", origin, path);
let pool = this._httpReqPools[origin];
if (pool === undefined) {
this.createHttpReqPool(origin);
pool = this._httpReqPools[origin];
}
let headers = options.headers === undefined ? {} : options.headers;
if (options.bearerToken !== undefined) {
headers.Authorization = `Bearer ${options.bearerToken}`;
}
if (options.auth !== undefined) {
let token = Buffer.from(
`${options.auth.username}:${options.auth.password}`,
).toString("base64");
headers.Authorization = `Basic ${token}`;
}
let body;
if (options.body !== undefined && options.method !== "GET") {
if (
options.headers?.["content-type"] === undefined ||
typeof options.body === "object"
) {
headers["content-type"] = "application/json; charset=utf-8";
body = JSON.stringify(options.body);
} else {
body = options.body;
}
}
let results = await pool.request({
origin,
path,
method: options.method,
headers,
query: options.searchParams,
body,
});
let resData;
let contentExists = false;
if (
results.headers["content-length"] !== undefined &&
results.headers["content-length"] !== "0"
) {
contentExists = true;
}
if (
contentExists &&
results.headers["content-type"]?.startsWith("application/json") === true
) {
resData = await results.body.json();
} else {
resData = await results.body.text();
if (
resData.length &&
results.headers["content-type"]?.startsWith("application/json") === true
) {
resData = JSON.parse(resData);
}
}
let res = {
statusCode: results.statusCode,
headers: results.headers,
trailers: results.trailers,
body: resData,
};
return res;
}
}
exports.Shell = Shell;