@mountainpass/hooked-cli
Version:
A tool for runnable scripts
234 lines (233 loc) • 11.8 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { CronJob } from 'cron';
import express from 'express';
import fs from 'fs';
import fsPromise from 'fs/promises';
import common from '../common/invoke.js';
import loaders from '../common/loaders.js';
import { findScript } from '../config.js';
import defaults from '../defaults.js';
import { StdinChoicesResolver } from '../scriptExecutors/resolvers/StdinChoicesResolver.js';
import { isDefined, isStdinScript, sortCaseInsensitive } from '../types.js';
import logger from '../utils/logger.js';
import { globalErrorHandler, hasRole } from './globalErrorHandler.js';
const getLastModifiedTimeMs = (filepath) => __awaiter(void 0, void 0, void 0, function* () {
return (yield fsPromise.stat(filepath)).mtimeMs;
});
export class HttpError extends Error {
constructor(statusCode, message) {
super(message);
this.name = 'AuthError';
this.statusCode = statusCode;
}
}
/** Rebuilds the cron jobs. */
const rebuildCronJobs = (systemProcessEnvs, options, config, previousJobs) => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b, _c, _d;
// stop existing jobs (what happens if running?)
for (const job of previousJobs) {
logger.debug(`Stopping cron job - ${job.context.name}`);
job.stop();
}
// create new jobs
const newJobs = [];
if (isDefined((_a = config.server) === null || _a === void 0 ? void 0 : _a.triggers)) {
for (const [name, cronJob] of Object.entries((_b = config.server) === null || _b === void 0 ? void 0 : _b.triggers)) {
const { $cron: cronTime, $script } = cronJob;
const scriptPath = $script.split(' ');
// resolve script path
const job = CronJob.from({
context: { name },
cronTime,
onTick: function () {
return __awaiter(this, void 0, void 0, function* () {
var _a;
logger.debug(`Running cron job - "${name}" - nextRun=${(_a = job.nextDate().toISO()) !== null && _a !== void 0 ? _a : '<unknown>'}`);
yield common.invoke(null, systemProcessEnvs, options, config, ['default'], scriptPath, {}, true, false)
.catch((err) => { logger.error(`Error occurred running cron job - ${err.message}`); });
});
},
start: true,
timeZone: (_c = options.timezone) !== null && _c !== void 0 ? _c : 'UTC'
});
logger.debug(`Created cron job - "${name}" - nextRun=${(_d = job.nextDate().toISO()) !== null && _d !== void 0 ? _d : '<unknown>'}`);
newJobs.push(job);
}
}
if (newJobs.length === 0) {
logger.debug('No cron jobs.');
}
return newJobs;
});
const router = (systemProcessEnvs, options) => __awaiter(void 0, void 0, void 0, function* () {
const app = express.Router();
const filepath = defaults.getDefaults().HOOKED_FILE;
let config = {};
let lastModified = -1;
let cronJobs = [];
// initial setup.
config = yield loaders.loadConfiguration(systemProcessEnvs, options);
lastModified = yield getLastModifiedTimeMs(filepath);
cronJobs = yield rebuildCronJobs(systemProcessEnvs, options, config, cronJobs);
// watcher for file configuration changes
const fileChangeListener = (curr, prev) => {
checkIfConfigurationHasChanged(curr)
.catch((err) => { logger.error(`Error occurred checking config change - ${err.message}`); });
};
// watch for changes
fs.watchFile(filepath, { interval: 3000 }, fileChangeListener);
process.on('SIGTERM', () => { fs.unwatchFile(filepath, fileChangeListener); });
/** Force reloads of all configuration. */
const reloadConfiguration = () => __awaiter(void 0, void 0, void 0, function* () {
config = yield loaders.loadConfiguration(systemProcessEnvs, options);
// and reconfigure cron jobs
cronJobs = yield rebuildCronJobs(systemProcessEnvs, options, config, cronJobs);
});
/** Checks whether the root config file modification time is newer, and reloads the configuration. (Max once per second) */
const checkIfConfigurationHasChanged = (curr) => __awaiter(void 0, void 0, void 0, function* () {
// TODO extend to all files?
if (curr.mtimeMs > lastModified) {
// file has been modified, reload...
logger.debug(`Configuration changed, reloading '${filepath}'`);
lastModified = curr.mtimeMs;
yield reloadConfiguration();
}
});
app.get('/me', globalErrorHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
res.json(req.user);
})));
/**
* Reloads all configuration from disk.
*/
app.get('/reload', hasRole('admin'), globalErrorHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
yield reloadConfiguration();
res.json({ message: 'Successfully reloaded configuration.' });
})));
/**
* Prints the different environments available (and their environment variable names).
*/
app.get('/env', hasRole('admin'), globalErrorHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
const result = Object.entries((_a = config.env) !== null && _a !== void 0 ? _a : {}).reduce((prev, curr) => {
const [key, env] = curr;
prev[key] = Object.keys(env).sort(sortCaseInsensitive);
return prev;
}, {});
res.json(result);
})));
// app.get('/imports', globalErrorHandler(async (req, res) => {
// res.json(config.imports ?? [])
// }))
app.get('/dashboard/list', globalErrorHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
const user = req.user;
if (isDefined(config.server)) {
const dashboardsList = (_a = config.server.dashboards) !== null && _a !== void 0 ? _a : [];
const dashboards = dashboardsList
.filter((d) => {
var _a;
// has access
return ((_a = d.accessRoles) !== null && _a !== void 0 ? _a : ['admin']).some((r) => user.accessRoles.includes(r));
})
.map((d) => {
return { title: d.title };
});
if (dashboards.length === 0)
console.debug(`No dashboards for user '${req.user.username}' with accessRoles '${req.user.accessRoles.join(',')}'`);
res.json(dashboards);
}
})));
app.get('/dashboard/get/:dashboard', globalErrorHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b;
const fetchDashboard = decodeURIComponent(req.params.dashboard);
const user = req.user;
const dashboard = ((_b = (_a = config.server) === null || _a === void 0 ? void 0 : _a.dashboards) !== null && _b !== void 0 ? _b : [])
.find((d) => { var _a; return d.title === fetchDashboard && ((_a = d.accessRoles) !== null && _a !== void 0 ? _a : ['admin']).some((r) => user.accessRoles.includes(r)); });
if (isDefined(dashboard)) {
res.json(dashboard);
}
else {
res.status(403).json({ "message": "Dashboard not found or user does not have access." }).end();
}
})));
app.get('/triggers', hasRole('admin'), globalErrorHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b;
res.json((_b = (_a = config.server) === null || _a === void 0 ? void 0 : _a.triggers) !== null && _b !== void 0 ? _b : []);
})));
// app.get('/plugins', globalErrorHandler(async (req, res) => {
// res.json(config.plugins ?? {})
// }))
/**
* Get all script configs.
*/
app.get('/scripts', hasRole('admin'), globalErrorHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
res.json((_a = config.scripts) !== null && _a !== void 0 ? _a : {});
})));
/**
* Get a specific script config.
*/
app.get('/scripts/:scriptPath', globalErrorHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
const scriptPath = decodeURIComponent(req.params.scriptPath).split(' ');
const [script] = yield findScript(config, scriptPath, options);
// TODO check access roles!
res.json(script); // { script, paths }
})));
/**
* Fetch a single, script's, environment value config (with resolved choices, by env name)
*/
app.get('/resolveEnvValue/:env/script/:scriptPath/env/:envKeyName', globalErrorHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
const envKeyName = decodeURIComponent(req.params.envKeyName);
const scriptPath = decodeURIComponent(req.params.scriptPath).split(' ');
// setup
const { env, envVars } = yield loaders.initialiseEnvironment(systemProcessEnvs, options, config);
// find the script to execute...
const rootScriptAndPaths = yield findScript(config, scriptPath, options);
const script = rootScriptAndPaths[0];
if (isDefined(script.$env)) {
const envValueScript = script.$env[envKeyName];
if (isStdinScript(envValueScript)) {
const choices = yield StdinChoicesResolver(envKeyName, envValueScript, {
config,
env,
envVars,
options,
stdin: (_a = req.body) !== null && _a !== void 0 ? _a : {}
});
return res.json(Object.assign(Object.assign({}, envValueScript), { $choices: choices }));
}
else if (isDefined(envValueScript)) {
return res.json(Object.assign({}, envValueScript));
}
}
return res.json({ message: 'Invalid script or environment key name.' }).status(400).end();
})));
/**
* Runs the given script.
*/
app.get('/run/:env/:scriptPath', globalErrorHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
const providedEnvNames = decodeURIComponent(req.params.env).split(',');
const scriptPath = decodeURIComponent(req.params.scriptPath).split(' ');
res.json(yield common.invoke(req.user, systemProcessEnvs, options, config, providedEnvNames, scriptPath, {}, false, false));
})));
/**
* Runs the given script with environment variables.
*/
app.post('/run/:env/:scriptPath', globalErrorHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
const providedEnvNames = decodeURIComponent(req.params.env).split(',');
const scriptPath = decodeURIComponent(req.params.scriptPath).split(' ');
res.json(yield common.invoke(req.user, systemProcessEnvs, options, config, providedEnvNames, scriptPath, (_a = req.body) !== null && _a !== void 0 ? _a : {}, false, false));
})));
return app;
});
export default { router };