UNPKG

@mountainpass/hooked-cli

Version:
234 lines (233 loc) 11.8 kB
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 };