Migrate codebase from CommonJS to ES Modules

- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
  (dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
  declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
  loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing

Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Girish Ramakrishnan
2026-02-14 09:53:14 +01:00
parent e0e9f14a5e
commit 96dc79cfe6
277 changed files with 4789 additions and 3811 deletions

View File

@@ -1,6 +1,75 @@
'use strict';
import * as appTaskManager from './apptaskmanager.js';
import * as appstore from './appstore.js';
import * as archives from './archives.js';
import assert from 'node:assert';
import backups from './backups.js';
import * as backupSites from './backupsites.js';
import BoxError from './boxerror.js';
import constants from './constants.js';
import crypto from 'node:crypto';
import { CronTime } from 'cron';
import * as dashboard from './dashboard.js';
import * as database from './database.js';
import debugModule from 'debug';
import * as dns from './dns.js';
import * as docker from './docker.js';
import * as domains from './domains.js';
import eventlog from './eventlog.js';
import fs from 'node:fs';
import Location from './location.js';
import locks from './locks.js';
import * as logs from './logs.js';
import * as mail from './mail.js';
import manifestFormat from '@cloudron/manifest-format';
import mysql from 'mysql2';
import * as notifications from './notifications.js';
import once from './once.js';
import path from 'node:path';
import paths from './paths.js';
import { PassThrough } from 'node:stream';
import * as reverseProxy from './reverseproxy.js';
import safe from 'safetydance';
import semver from 'semver';
import services from './services.js';
import shellModule from './shell.js';
import tasks from './tasks.js';
import { Transform as TransformStream } from 'node:stream';
import * as users from './users.js';
import util from 'node:util';
import * as volumes from './volumes.js';
import * as _ from './underscore.js';
exports = module.exports = {
const debug = debugModule('box:apps');
const shell = shellModule('apps');
const PORT_TYPE_TCP = 'tcp';
const PORT_TYPE_UDP = 'udp';
const ISTATE_PENDING_INSTALL = 'pending_install';
const ISTATE_PENDING_CLONE = 'pending_clone';
const ISTATE_PENDING_CONFIGURE = 'pending_configure';
const ISTATE_PENDING_RECREATE_CONTAINER = 'pending_recreate_container';
const ISTATE_PENDING_LOCATION_CHANGE = 'pending_location_change';
const ISTATE_PENDING_SERVICES_CHANGE = 'pending_services_change';
const ISTATE_PENDING_DATA_DIR_MIGRATION = 'pending_data_dir_migration';
const ISTATE_PENDING_RESIZE = 'pending_resize';
const ISTATE_PENDING_DEBUG = 'pending_debug';
const ISTATE_PENDING_UNINSTALL = 'pending_uninstall';
const ISTATE_PENDING_RESTORE = 'pending_restore';
const ISTATE_PENDING_IMPORT = 'pending_import';
const ISTATE_PENDING_UPDATE = 'pending_update';
const ISTATE_PENDING_START = 'pending_start';
const ISTATE_PENDING_STOP = 'pending_stop';
const ISTATE_PENDING_RESTART = 'pending_restart';
const ISTATE_ERROR = 'error';
const ISTATE_INSTALLED = 'installed';
const RSTATE_RUNNING = 'running';
const RSTATE_STOPPED = 'stopped';
const ACCESS_LEVEL_ADMIN = 'admin';
const ACCESS_LEVEL_OPERATOR = 'operator';
const ACCESS_LEVEL_USER = 'user';
const ACCESS_LEVEL_NONE = '';
export default {
canAccess,
isOperator,
accessLevel,
@@ -99,32 +168,32 @@ exports = module.exports = {
canBackupApp,
PORT_TYPE_TCP: 'tcp',
PORT_TYPE_UDP: 'udp',
PORT_TYPE_TCP,
PORT_TYPE_UDP,
// task codes - the installation state is now a misnomer (keep in sync in UI)
ISTATE_PENDING_INSTALL: 'pending_install', // installs and fresh reinstalls
ISTATE_PENDING_CLONE: 'pending_clone', // clone
ISTATE_PENDING_CONFIGURE: 'pending_configure', // infra update
ISTATE_PENDING_RECREATE_CONTAINER: 'pending_recreate_container', // env change or addon change
ISTATE_PENDING_LOCATION_CHANGE: 'pending_location_change',
ISTATE_PENDING_SERVICES_CHANGE: 'pending_services_change',
ISTATE_PENDING_DATA_DIR_MIGRATION: 'pending_data_dir_migration',
ISTATE_PENDING_RESIZE: 'pending_resize',
ISTATE_PENDING_DEBUG: 'pending_debug',
ISTATE_PENDING_UNINSTALL: 'pending_uninstall', // uninstallation
ISTATE_PENDING_RESTORE: 'pending_restore', // restore to previous backup or on upgrade
ISTATE_PENDING_IMPORT: 'pending_import', // import from external backup
ISTATE_PENDING_UPDATE: 'pending_update', // update from installed state preserving data
ISTATE_PENDING_START: 'pending_start',
ISTATE_PENDING_STOP: 'pending_stop',
ISTATE_PENDING_RESTART: 'pending_restart',
ISTATE_ERROR: 'error', // error executing last pending_* command
ISTATE_INSTALLED: 'installed', // app is installed
ISTATE_PENDING_INSTALL,
ISTATE_PENDING_CLONE,
ISTATE_PENDING_CONFIGURE,
ISTATE_PENDING_RECREATE_CONTAINER,
ISTATE_PENDING_LOCATION_CHANGE,
ISTATE_PENDING_SERVICES_CHANGE,
ISTATE_PENDING_DATA_DIR_MIGRATION,
ISTATE_PENDING_RESIZE,
ISTATE_PENDING_DEBUG,
ISTATE_PENDING_UNINSTALL,
ISTATE_PENDING_RESTORE,
ISTATE_PENDING_IMPORT,
ISTATE_PENDING_UPDATE,
ISTATE_PENDING_START,
ISTATE_PENDING_STOP,
ISTATE_PENDING_RESTART,
ISTATE_ERROR,
ISTATE_INSTALLED,
// run states
RSTATE_RUNNING: 'running',
RSTATE_STOPPED: 'stopped', // app stopped by us
RSTATE_RUNNING,
RSTATE_STOPPED,
// health states (keep in sync in UI)
HEALTH_HEALTHY: 'healthy',
@@ -133,10 +202,10 @@ exports = module.exports = {
HEALTH_DEAD: 'dead',
// app access levels
ACCESS_LEVEL_ADMIN: 'admin',
ACCESS_LEVEL_OPERATOR: 'operator',
ACCESS_LEVEL_USER: 'user',
ACCESS_LEVEL_NONE: '',
ACCESS_LEVEL_ADMIN,
ACCESS_LEVEL_OPERATOR,
ACCESS_LEVEL_USER,
ACCESS_LEVEL_NONE,
// exported for testing
_checkForPortBindingConflict: checkForPortBindingConflict,
@@ -148,47 +217,6 @@ exports = module.exports = {
_clear: clear
};
const appTaskManager = require('./apptaskmanager.js'),
appstore = require('./appstore.js'),
archives = require('./archives.js'),
assert = require('node:assert'),
backups = require('./backups.js'),
backupSites = require('./backupsites.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
crypto = require('node:crypto'),
{ CronTime } = require('cron'),
dashboard = require('./dashboard.js'),
database = require('./database.js'),
debug = require('debug')('box:apps'),
dns = require('./dns.js'),
docker = require('./docker.js'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
fs = require('node:fs'),
Location = require('./location.js'),
locks = require('./locks.js'),
logs = require('./logs.js'),
mail = require('./mail.js'),
manifestFormat = require('@cloudron/manifest-format'),
mysql = require('mysql2'),
notifications = require('./notifications.js'),
once = require('./once.js'),
path = require('node:path'),
paths = require('./paths.js'),
PassThrough = require('node:stream').PassThrough,
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
services = require('./services.js'),
shell = require('./shell.js')('apps'),
tasks = require('./tasks.js'),
TransformStream = require('node:stream').Transform,
users = require('./users.js'),
util = require('node:util'),
volumes = require('./volumes.js'),
_ = require('./underscore.js');
// NOTE: when adding fields here, update the clone and unarchive logic as well
const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.versionsUrl', 'apps.installationState', 'apps.errorJson', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuQuota',
@@ -201,7 +229,7 @@ const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.versionsUrl',
// const PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId', 'count' ].join(',');
const LOCATION_FIELDS = [ 'appId', 'subdomain', 'domain', 'type', 'certificateJson' ];
const CHECKVOLUME_CMD = path.join(__dirname, 'scripts/checkvolume.sh');
const CHECKVOLUME_CMD = path.join(import.meta.dirname, 'scripts/checkvolume.sh');
// ports is a map of envvar -> hostPort
function validatePorts(ports, manifest) {
@@ -280,7 +308,7 @@ function translateToPortBindings(ports, manifest) {
const tcpPorts = manifest.tcpPorts || {};
for (const portName in ports) {
const portType = portName in tcpPorts ? exports.PORT_TYPE_TCP : exports.PORT_TYPE_UDP;
const portType = portName in tcpPorts ? PORT_TYPE_TCP : PORT_TYPE_UDP;
const portCount = portName in tcpPorts ? tcpPorts[portName].portCount : manifest.udpPorts[portName].portCount; // since count is optional, this can be undefined
portBindings[portName] = { hostPort: ports[portName], type: portType, count: portCount || 1 };
}
@@ -587,10 +615,10 @@ function pickFields(app, accessLevel) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof accessLevel, 'string');
if (accessLevel === exports.ACCESS_LEVEL_NONE) return null; // cannot happen!
if (accessLevel === ACCESS_LEVEL_NONE) return null; // cannot happen!
let result;
if (accessLevel === exports.ACCESS_LEVEL_USER) {
if (accessLevel === ACCESS_LEVEL_USER) {
result = _.pick(app, [
'id', 'appStoreId', 'versionsUrl', 'installationState', 'error', 'runState', 'health', 'taskId', 'accessRestriction',
'secondaryDomains', 'redirectDomains', 'aliasDomains', 'sso', 'subdomain', 'domain', 'fqdn', 'certificate',
@@ -784,7 +812,7 @@ function canAutoupdateAppSync(app, updateInfo) {
return { code: false, reason: 'Major package version requires review of breaking changes' }; // major changes are blocking
}
if (app.runState === exports.RSTATE_STOPPED) return { code: false, reason: 'Stopped apps cannot run migration scripts' };
if (app.runState === RSTATE_STOPPED) return { code: false, reason: 'Stopped apps cannot run migration scripts' };
const newTcpPorts = manifest.tcpPorts || {};
const newUdpPorts = manifest.udpPorts || {};
@@ -849,9 +877,9 @@ function canAccess(app, user) {
}
function accessLevel(app, user) {
if (isAdmin(user)) return exports.ACCESS_LEVEL_ADMIN;
if (isOperator(app, user)) return exports.ACCESS_LEVEL_OPERATOR;
return canAccess(app, user) ? exports.ACCESS_LEVEL_USER : exports.ACCESS_LEVEL_NONE;
if (isAdmin(user)) return ACCESS_LEVEL_ADMIN;
if (isOperator(app, user)) return ACCESS_LEVEL_OPERATOR;
return canAccess(app, user) ? ACCESS_LEVEL_USER : ACCESS_LEVEL_NONE;
}
async function checkForPortBindingConflict(portBindings, options) {
@@ -1243,10 +1271,10 @@ async function onTaskFinished(error, appId, installationState, taskId, auditSour
if (!app || !task) return;
switch (installationState) {
case exports.ISTATE_PENDING_DATA_DIR_MIGRATION:
case ISTATE_PENDING_DATA_DIR_MIGRATION:
if (success) await safe(services.rebuildService('sftp', auditSource), { debug });
break;
case exports.ISTATE_PENDING_UPDATE: {
case ISTATE_PENDING_UPDATE: {
const fromManifest = success ? task.args[1].updateConfig.manifest : app.manifest;
const toManifest = success ? app.manifest : task.args[1].updateConfig.manifest;
@@ -1264,11 +1292,11 @@ async function scheduleTask(appId, installationState, taskId, auditSource) {
assert.strictEqual(typeof auditSource, 'object');
let memoryLimit = 400;
if (installationState === exports.ISTATE_PENDING_CLONE || installationState === exports.ISTATE_PENDING_RESTORE
|| installationState === exports.ISTATE_PENDING_IMPORT || installationState === exports.ISTATE_PENDING_UPDATE) {
if (installationState === ISTATE_PENDING_CLONE || installationState === ISTATE_PENDING_RESTORE
|| installationState === ISTATE_PENDING_IMPORT || installationState === ISTATE_PENDING_UPDATE) {
const sites = await backupSites.listByContentForUpdates(appId);
memoryLimit = sites.reduce((acc, cur) => cur.limits?.memoryLimit ? Math.max(cur.limits.memoryLimit/1024/1024, acc) : acc, 400);
} else if (installationState === exports.ISTATE_PENDING_DATA_DIR_MIGRATION) {
} else if (installationState === ISTATE_PENDING_DATA_DIR_MIGRATION) {
memoryLimit = 1024; // cp takes more memory than we think
}
@@ -1286,8 +1314,8 @@ async function scheduleTask(appId, installationState, taskId, auditSource) {
taskId,
installationState
};
await safe(update(appId, { installationState: exports.ISTATE_ERROR, error: appError, taskId: null }), { debug });
} else if (!(installationState === exports.ISTATE_PENDING_UNINSTALL && !error)) { // clear out taskId except for successful uninstall
await safe(update(appId, { installationState: ISTATE_ERROR, error: appError, taskId: null }), { debug });
} else if (!(installationState === ISTATE_PENDING_UNINSTALL && !error)) { // clear out taskId except for successful uninstall
await safe(update(appId, { taskId: null }), { debug });
}
@@ -1303,7 +1331,7 @@ async function addTask(appId, installationState, task, auditSource) {
const { args, values } = task;
// TODO: match the SQL logic to match checkAppState. this means checking the error.installationState and installationState. Unfortunately, former is JSON right now
const requiredState = null; // 'requiredState' in task ? task.requiredState : exports.ISTATE_INSTALLED;
const requiredState = null; // 'requiredState' in task ? task.requiredState : ISTATE_INSTALLED;
const scheduleNow = 'scheduleNow' in task ? task.scheduleNow : true;
const requireNullTaskId = 'requireNullTaskId' in task ? task.requireNullTaskId : true;
@@ -1324,17 +1352,17 @@ function checkAppState(app, state) {
if (app.taskId) return new BoxError(BoxError.BAD_STATE, `Locked by task ${app.taskId} : ${app.installationState} / ${app.runState}`);
if (app.installationState === exports.ISTATE_ERROR) {
if (app.installationState === ISTATE_ERROR) {
// allow task to be called again if that was the errored task
if (app.error.installationState === state) return null;
// allow uninstall from any state
if (state !== exports.ISTATE_PENDING_UNINSTALL && state !== exports.ISTATE_PENDING_RESTORE && state !== exports.ISTATE_PENDING_IMPORT) return new BoxError(BoxError.BAD_STATE, 'Not allowed in error state');
if (state !== ISTATE_PENDING_UNINSTALL && state !== ISTATE_PENDING_RESTORE && state !== ISTATE_PENDING_IMPORT) return new BoxError(BoxError.BAD_STATE, 'Not allowed in error state');
}
if (app.runState === exports.RSTATE_STOPPED) {
if (app.runState === RSTATE_STOPPED) {
// can't backup or restore since app addons are down. can't update because migration scripts won't run
if (state === exports.ISTATE_PENDING_UPDATE || state === exports.ISTATE_PENDING_RESTORE || state === exports.ISTATE_PENDING_IMPORT) return new BoxError(BoxError.BAD_STATE, 'Not allowed in stopped state');
if (state === ISTATE_PENDING_UPDATE || state === ISTATE_PENDING_RESTORE || state === ISTATE_PENDING_IMPORT) return new BoxError(BoxError.BAD_STATE, 'Not allowed in stopped state');
}
return null;
@@ -1521,8 +1549,8 @@ async function install(data, auditSource) {
enableRedis,
notes,
crontab,
runState: exports.RSTATE_RUNNING,
installationState: exports.ISTATE_PENDING_INSTALL
runState: RSTATE_RUNNING,
installationState: ISTATE_PENDING_INSTALL
};
const [addError] = await safe(add(appId, appStoreId, versionsUrl, manifest, subdomain, domain, portBindings, app));
@@ -1681,7 +1709,7 @@ async function setMemoryLimit(app, memoryLimit, auditSource) {
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_RESIZE);
let error = checkAppState(app, ISTATE_PENDING_RESIZE);
if (error) throw error;
error = validateMemoryLimit(app.manifest, memoryLimit);
@@ -1691,7 +1719,7 @@ async function setMemoryLimit(app, memoryLimit, auditSource) {
args: {},
values: { memoryLimit }
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_RESIZE, task, auditSource);
const taskId = await addTask(appId, ISTATE_PENDING_RESIZE, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, memoryLimit, taskId });
@@ -1704,7 +1732,7 @@ async function setCpuQuota(app, cpuQuota, auditSource) {
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_RESIZE);
let error = checkAppState(app, ISTATE_PENDING_RESIZE);
if (error) throw error;
error = validateCpuQuota(cpuQuota);
@@ -1714,7 +1742,7 @@ async function setCpuQuota(app, cpuQuota, auditSource) {
args: {},
values: { cpuQuota }
};
const taskId = await safe(addTask(appId, exports.ISTATE_PENDING_RESIZE, task, auditSource));
const taskId = await safe(addTask(appId, ISTATE_PENDING_RESIZE, task, auditSource));
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, cpuQuota, taskId });
@@ -1727,14 +1755,14 @@ async function setMounts(app, mounts, auditSource) {
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
const error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER);
const error = checkAppState(app, ISTATE_PENDING_RECREATE_CONTAINER);
if (error) throw error;
const task = {
args: {},
values: { mounts }
};
const [taskError, taskId] = await safe(addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, auditSource));
const [taskError, taskId] = await safe(addTask(appId, ISTATE_PENDING_RECREATE_CONTAINER, task, auditSource));
if (taskError && taskError.reason === BoxError.ALREADY_EXISTS) throw new BoxError(BoxError.CONFLICT, 'Duplicate mount points');
if (taskError) throw taskError;
@@ -1749,7 +1777,7 @@ async function setDevices(app, devices, auditSource) {
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER);
let error = checkAppState(app, ISTATE_PENDING_RECREATE_CONTAINER);
if (error) throw error;
error = validateDevices(devices);
@@ -1759,7 +1787,7 @@ async function setDevices(app, devices, auditSource) {
args: {},
values: { devices }
};
const [taskError, taskId] = await safe(addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, auditSource));
const [taskError, taskId] = await safe(addTask(appId, ISTATE_PENDING_RECREATE_CONTAINER, task, auditSource));
if (taskError) throw taskError;
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, devices, taskId });
@@ -1773,7 +1801,7 @@ async function setEnvironment(app, env, auditSource) {
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER);
let error = checkAppState(app, ISTATE_PENDING_RECREATE_CONTAINER);
if (error) throw error;
error = validateEnv(env);
@@ -1783,7 +1811,7 @@ async function setEnvironment(app, env, auditSource) {
args: {},
values: { env }
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, auditSource);
const taskId = await addTask(appId, ISTATE_PENDING_RECREATE_CONTAINER, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, env, taskId });
@@ -1796,7 +1824,7 @@ async function setDebugMode(app, debugMode, auditSource) {
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_DEBUG);
let error = checkAppState(app, ISTATE_PENDING_DEBUG);
if (error) throw error;
error = validateDebugMode(debugMode);
@@ -1806,7 +1834,7 @@ async function setDebugMode(app, debugMode, auditSource) {
args: {},
values: { debugMode }
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_DEBUG, task, auditSource);
const taskId = await addTask(appId, ISTATE_PENDING_DEBUG, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, debugMode, taskId });
@@ -1823,7 +1851,7 @@ async function setMailbox(app, data, auditSource) {
const enableMailbox = data.enable;
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_SERVICES_CHANGE);
let error = checkAppState(app, ISTATE_PENDING_SERVICES_CHANGE);
if (error) throw error;
if (!app.manifest.addons?.sendmail) throw new BoxError(BoxError.BAD_FIELD, 'App does not use sendmail');
@@ -1854,7 +1882,7 @@ async function setMailbox(app, data, auditSource) {
args: {},
values: { enableMailbox, mailboxName, mailboxDomain, mailboxDisplayName }
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_SERVICES_CHANGE, task, auditSource);
const taskId = await addTask(appId, ISTATE_PENDING_SERVICES_CHANGE, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, mailboxName, mailboxDomain, mailboxDisplayName, taskId });
@@ -1870,7 +1898,7 @@ async function setInbox(app, data, auditSource) {
const enableInbox = data.enable;
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_SERVICES_CHANGE);
let error = checkAppState(app, ISTATE_PENDING_SERVICES_CHANGE);
if (error) throw error;
if (!app.manifest.addons?.recvmail) throw new BoxError(BoxError.BAD_FIELD, 'App does not use recvmail addon');
@@ -1889,7 +1917,7 @@ async function setInbox(app, data, auditSource) {
args: {},
values: { enableInbox, inboxName, inboxDomain }
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_SERVICES_CHANGE, task, auditSource);
const taskId = await addTask(appId, ISTATE_PENDING_SERVICES_CHANGE, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, enableInbox, inboxName, inboxDomain, taskId });
@@ -1902,7 +1930,7 @@ async function setTurn(app, enableTurn, auditSource) {
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
const error = checkAppState(app, exports.ISTATE_PENDING_SERVICES_CHANGE);
const error = checkAppState(app, ISTATE_PENDING_SERVICES_CHANGE);
if (error) throw error;
if (!app.manifest.addons?.turn) throw new BoxError(BoxError.BAD_FIELD, 'App does not use turn addon');
@@ -1912,7 +1940,7 @@ async function setTurn(app, enableTurn, auditSource) {
args: {},
values: { enableTurn }
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_SERVICES_CHANGE, task, auditSource);
const taskId = await addTask(appId, ISTATE_PENDING_SERVICES_CHANGE, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, enableTurn, taskId });
@@ -1925,7 +1953,7 @@ async function setRedis(app, enableRedis, auditSource) {
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
const error = checkAppState(app, exports.ISTATE_PENDING_SERVICES_CHANGE);
const error = checkAppState(app, ISTATE_PENDING_SERVICES_CHANGE);
if (error) throw error;
if (!app.manifest.addons?.redis) throw new BoxError(BoxError.BAD_FIELD, 'App does not use redis addon');
@@ -1935,7 +1963,7 @@ async function setRedis(app, enableRedis, auditSource) {
args: {},
values: { enableRedis }
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_SERVICES_CHANGE, task, auditSource);
const taskId = await addTask(appId, ISTATE_PENDING_SERVICES_CHANGE, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, enableRedis, taskId });
@@ -2020,7 +2048,7 @@ async function setLocation(app, data, auditSource) {
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_LOCATION_CHANGE);
let error = checkAppState(app, ISTATE_PENDING_LOCATION_CHANGE);
if (error) throw error;
const values = {
@@ -2073,7 +2101,7 @@ async function setLocation(app, data, auditSource) {
},
values
};
const [taskError, taskId] = await safe(addTask(appId, exports.ISTATE_PENDING_LOCATION_CHANGE, task, auditSource));
const [taskError, taskId] = await safe(addTask(appId, ISTATE_PENDING_LOCATION_CHANGE, task, auditSource));
if (taskError && taskError.reason !== BoxError.ALREADY_EXISTS) throw taskError;
if (taskError && taskError.reason === BoxError.ALREADY_EXISTS) throw getDuplicateErrorDetails(taskError.message, locations, values.portBindings);
@@ -2094,7 +2122,7 @@ async function setStorage(app, volumeId, volumePrefix, auditSource) {
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
const error = checkAppState(app, exports.ISTATE_PENDING_DATA_DIR_MIGRATION);
const error = checkAppState(app, ISTATE_PENDING_DATA_DIR_MIGRATION);
if (error) throw error;
if (volumeId) {
@@ -2107,7 +2135,7 @@ async function setStorage(app, volumeId, volumePrefix, auditSource) {
args: { newStorageVolumeId: volumeId, newStorageVolumePrefix: volumePrefix },
values: {}
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_DATA_DIR_MIGRATION, task, auditSource);
const taskId = await addTask(appId, ISTATE_PENDING_DATA_DIR_MIGRATION, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, volumeId, volumePrefix, taskId });
@@ -2123,9 +2151,9 @@ async function updateApp(app, data, auditSource) {
const skipBackup = !!data.skipBackup, appId = app.id, manifest = data.manifest;
const values = { updateInfo: null }; // clear update indicator immediately
if (app.runState === exports.RSTATE_STOPPED) throw new BoxError(BoxError.BAD_STATE, 'Stopped apps cannot be updated');
if (app.runState === RSTATE_STOPPED) throw new BoxError(BoxError.BAD_STATE, 'Stopped apps cannot be updated');
let error = checkAppState(app, exports.ISTATE_PENDING_UPDATE);
let error = checkAppState(app, ISTATE_PENDING_UPDATE);
if (error) throw error;
error = manifestFormat.parse(manifest);
@@ -2192,7 +2220,7 @@ async function updateApp(app, data, auditSource) {
args: { updateConfig },
values
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_UPDATE, task, auditSource);
const taskId = await addTask(appId, ISTATE_PENDING_UPDATE, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId, app, skipBackup, toManifest: manifest, fromManifest: app.manifest, force: data.force, taskId });
@@ -2268,7 +2296,7 @@ async function repair(app, data, auditSource) {
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
let errorState = (app.error && app.error.installationState) || exports.ISTATE_PENDING_CONFIGURE;
let errorState = (app.error && app.error.installationState) || ISTATE_PENDING_CONFIGURE;
const task = {
args: {},
@@ -2277,7 +2305,7 @@ async function repair(app, data, auditSource) {
};
// maybe split this into a separate route like reinstall?
if (errorState === exports.ISTATE_PENDING_INSTALL || errorState === exports.ISTATE_PENDING_CLONE) {
if (errorState === ISTATE_PENDING_INSTALL || errorState === ISTATE_PENDING_CLONE) {
task.args = { skipDnsSetup: false, overwriteDns: true };
if (data.manifest) {
let error = manifestFormat.parse(data.manifest);
@@ -2297,7 +2325,7 @@ async function repair(app, data, auditSource) {
task.args.oldManifest = app.manifest;
}
} else {
errorState = exports.ISTATE_PENDING_CONFIGURE;
errorState = ISTATE_PENDING_CONFIGURE;
if (data.dockerImage) {
const newManifest = Object.assign({}, app.manifest, { dockerImage: data.dockerImage });
task.values.manifest = newManifest;
@@ -2318,7 +2346,7 @@ async function restore(app, backupId, auditSource) {
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_RESTORE);
let error = checkAppState(app, ISTATE_PENDING_RESTORE);
if (error) throw error;
// for empty or null backupId, use existing manifest to mimic a reinstall
@@ -2358,7 +2386,7 @@ async function restore(app, backupId, auditSource) {
values
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_RESTORE, task, auditSource);
const taskId = await addTask(appId, ISTATE_PENDING_RESTORE, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app, backupId: backup.id, remotePath: backup.remotePath, fromManifest: app.manifest, toManifest: manifest, taskId });
@@ -2372,7 +2400,7 @@ async function importApp(app, data, auditSource) {
const appId = app.id;
const error = checkAppState(app, exports.ISTATE_PENDING_IMPORT);
const error = checkAppState(app, ISTATE_PENDING_IMPORT);
if (error) throw error;
let restoreConfig;
@@ -2401,7 +2429,7 @@ async function importApp(app, data, auditSource) {
},
values: {}
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_IMPORT, task, auditSource);
const taskId = await addTask(appId, ISTATE_PENDING_IMPORT, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_IMPORT, auditSource, { app, remotePath: data.remotePath, inPlace: data.inPlace, taskId });
@@ -2479,8 +2507,8 @@ async function clone(app, data, user, auditSource) {
if (!manifest.addons?.recvmail) dolly.inboxDomain = null; // needed because we are cloning _current_ app settings with old manifest
const obj = Object.assign(dolly, {
installationState: exports.ISTATE_PENDING_CLONE,
runState: exports.RSTATE_RUNNING,
installationState: ISTATE_PENDING_CLONE,
runState: RSTATE_RUNNING,
mailboxName,
mailboxDomain,
secondaryDomains,
@@ -2497,9 +2525,9 @@ async function clone(app, data, user, auditSource) {
const task = {
args: { restoreConfig, overwriteDns, skipDnsSetup, oldManifest: null },
values: {},
requiredState: exports.ISTATE_PENDING_CLONE
requiredState: ISTATE_PENDING_CLONE
};
const taskId = await addTask(newAppId, exports.ISTATE_PENDING_CLONE, task, auditSource);
const taskId = await addTask(newAppId, ISTATE_PENDING_CLONE, task, auditSource);
const newApp = Object.assign({}, _.omit(obj, ['icon']), { appStoreId, versionsUrl, manifest, subdomain, domain, portBindings });
newApp.fqdn = dns.fqdn(newApp.subdomain, newApp.domain);
@@ -2558,8 +2586,8 @@ async function unarchive(archive, data, auditSource) {
redirectDomains: [],
aliasDomains: [],
mailboxDomain: data.domain, // archive's mailboxDomain may not exist
runState: exports.RSTATE_RUNNING,
installationState: exports.ISTATE_PENDING_INSTALL,
runState: RSTATE_RUNNING,
installationState: ISTATE_PENDING_INSTALL,
sso: backup.appConfig ? backup.appConfig.sso : true // when no appConfig take a blind guess
});
obj.icon = (await archives.getIcons(archive.id))?.icon;
@@ -2592,7 +2620,7 @@ async function uninstall(app, auditSource) {
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
const error = checkAppState(app, exports.ISTATE_PENDING_UNINSTALL);
const error = checkAppState(app, ISTATE_PENDING_UNINSTALL);
if (error) throw error;
const task = {
@@ -2600,7 +2628,7 @@ async function uninstall(app, auditSource) {
values: {},
requiredState: null // can run in any state, as long as no task is active
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_UNINSTALL, task, auditSource);
const taskId = await addTask(appId, ISTATE_PENDING_UNINSTALL, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId, app, taskId });
return { taskId };
@@ -2628,14 +2656,14 @@ async function start(app, auditSource) {
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
const error = checkAppState(app, exports.ISTATE_PENDING_START);
const error = checkAppState(app, ISTATE_PENDING_START);
if (error) throw error;
const task = {
args: {},
values: { runState: exports.RSTATE_RUNNING }
values: { runState: RSTATE_RUNNING }
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_START, task, auditSource);
const taskId = await addTask(appId, ISTATE_PENDING_START, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_START, auditSource, { appId, app, taskId });
return { taskId };
}
@@ -2645,14 +2673,14 @@ async function stop(app, auditSource) {
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
const error = checkAppState(app, exports.ISTATE_PENDING_STOP);
const error = checkAppState(app, ISTATE_PENDING_STOP);
if (error) throw error;
const task = {
args: {},
values: { runState: exports.RSTATE_STOPPED }
values: { runState: RSTATE_STOPPED }
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_STOP, task, auditSource);
const taskId = await addTask(appId, ISTATE_PENDING_STOP, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_STOP, auditSource, { appId, app, taskId });
@@ -2664,14 +2692,14 @@ async function restart(app, auditSource) {
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
const error = checkAppState(app, exports.ISTATE_PENDING_RESTART);
const error = checkAppState(app, ISTATE_PENDING_RESTART);
if (error) throw error;
const task = {
args: {},
values: { runState: exports.RSTATE_RUNNING }
values: { runState: RSTATE_RUNNING }
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_RESTART, task, auditSource);
const taskId = await addTask(appId, ISTATE_PENDING_RESTART, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_RESTART, auditSource, { appId, app, taskId });
@@ -2706,7 +2734,7 @@ async function createExec(app, options) {
const cmd = options.cmd || [ '/bin/bash' ];
assert(Array.isArray(cmd) && cmd.length > 0);
if (app.installationState !== exports.ISTATE_INSTALLED || app.runState !== exports.RSTATE_RUNNING) {
if (app.installationState !== ISTATE_INSTALLED || app.runState !== RSTATE_RUNNING) {
throw new BoxError(BoxError.BAD_STATE, 'App not installed or running');
}
@@ -2735,7 +2763,7 @@ async function startExec(app, execId, options) {
assert.strictEqual(typeof execId, 'string');
assert(options && typeof options === 'object');
if (app.installationState !== exports.ISTATE_INSTALLED || app.runState !== exports.RSTATE_RUNNING) {
if (app.installationState !== ISTATE_INSTALLED || app.runState !== RSTATE_RUNNING) {
throw new BoxError(BoxError.BAD_STATE, 'App not installed or running');
}
@@ -2778,13 +2806,13 @@ function canBackupApp(app) {
// only backup apps that are installed or specific pending states
// stopped apps cannot be backed up because addons might be down (redis)
if (app.runState === exports.RSTATE_STOPPED) return false;
if (app.runState === RSTATE_STOPPED) return false;
// we used to check the health here but that doesn't work for stopped apps. it's better to just fail
// and inform the user if the backup fails and the app addons have not been setup yet.
return app.installationState === exports.ISTATE_INSTALLED ||
app.installationState === exports.ISTATE_PENDING_CONFIGURE ||
app.installationState === exports.ISTATE_PENDING_UPDATE; // called from apptask
return app.installationState === ISTATE_INSTALLED ||
app.installationState === ISTATE_PENDING_CONFIGURE ||
app.installationState === ISTATE_PENDING_UPDATE; // called from apptask
}
async function backup(app, backupSiteId, auditSource) {
@@ -2872,18 +2900,18 @@ async function restoreApps(apps, options, auditSource) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
apps = apps.filter(app => app.installationState !== exports.ISTATE_ERROR); // remove errored apps. let them be 'repaired' by hand
apps = apps.filter(app => app.installationState !== exports.ISTATE_PENDING_RESTORE); // safeguard against tasks being created non-stop if we crash on startup
apps = apps.filter(app => app.installationState !== ISTATE_ERROR); // remove errored apps. let them be 'repaired' by hand
apps = apps.filter(app => app.installationState !== ISTATE_PENDING_RESTORE); // safeguard against tasks being created non-stop if we crash on startup
for (const app of apps) {
const [error, results] = await safe(backups.getByIdentifierAndStatePaged(app.id, backups.BACKUP_STATE_NORMAL, 1, 1));
let installationState, restoreConfig;
if (!error && results.length) {
installationState = exports.ISTATE_PENDING_RESTORE;
installationState = ISTATE_PENDING_RESTORE;
// intentionally ignore any backupSite provided during restore by the user because the site may not have all the apps
restoreConfig = { backupId: results[0].id };
} else {
installationState = exports.ISTATE_PENDING_INSTALL;
installationState = ISTATE_PENDING_INSTALL;
restoreConfig = null;
}
@@ -2907,8 +2935,8 @@ async function configureApps(apps, options, auditSource) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
apps = apps.filter(app => app.installationState !== exports.ISTATE_ERROR); // remove errored apps. let them be 'repaired' by hand
apps = apps.filter(app => app.installationState !== exports.ISTATE_PENDING_CONFIGURE); // safeguard against tasks being created non-stop if we crash on startup
apps = apps.filter(app => app.installationState !== ISTATE_ERROR); // remove errored apps. let them be 'repaired' by hand
apps = apps.filter(app => app.installationState !== ISTATE_PENDING_CONFIGURE); // safeguard against tasks being created non-stop if we crash on startup
const scheduleNow = !!options.scheduleNow;
@@ -2922,7 +2950,7 @@ async function configureApps(apps, options, auditSource) {
requireNullTaskId: false // ignore existing stale taskId
};
const [addTaskError, taskId] = await safe(addTask(app.id, exports.ISTATE_PENDING_CONFIGURE, task, auditSource));
const [addTaskError, taskId] = await safe(addTask(app.id, ISTATE_PENDING_CONFIGURE, task, auditSource));
if (addTaskError) debug(`configureApps: error marking ${app.fqdn} for configure: ${JSON.stringify(addTaskError)}`);
else debug(`configureApps: marked ${app.id} for re-configure with taskId ${taskId}`);
}
@@ -2935,23 +2963,23 @@ async function restartAppsUsingAddons(changedAddons, auditSource) {
let apps = await list();
// TODO: This ends up restarting apps that have optional redis
apps = apps.filter(app => app.manifest.addons && _.intersection(Object.keys(app.manifest.addons), changedAddons).length !== 0);
apps = apps.filter(app => app.installationState !== exports.ISTATE_ERROR); // remove errored apps. let them be 'repaired' by hand
apps = apps.filter(app => app.installationState !== exports.ISTATE_PENDING_RESTART); // safeguard against tasks being created non-stop restart if we crash on startup
apps = apps.filter(app => app.runState !== exports.RSTATE_STOPPED); // don't start stopped apps
apps = apps.filter(app => app.installationState !== ISTATE_ERROR); // remove errored apps. let them be 'repaired' by hand
apps = apps.filter(app => app.installationState !== ISTATE_PENDING_RESTART); // safeguard against tasks being created non-stop restart if we crash on startup
apps = apps.filter(app => app.runState !== RSTATE_STOPPED); // don't start stopped apps
for (const app of apps) {
debug(`restartAppsUsingAddons: marking ${app.fqdn} for restart`);
const task = {
args: {},
values: { runState: exports.RSTATE_RUNNING }
values: { runState: RSTATE_RUNNING }
};
// stop apps before updating the databases because postgres will "lock" them preventing import
const [stopError] = await safe(docker.stopContainers(app.id));
if (stopError) debug(`restartAppsUsingAddons: error stopping ${app.fqdn}`, stopError);
const [addTaskError, taskId] = await safe(addTask(app.id, exports.ISTATE_PENDING_RESTART, task, auditSource));
const [addTaskError, taskId] = await safe(addTask(app.id, ISTATE_PENDING_RESTART, task, auditSource));
if (addTaskError) debug(`restartAppsUsingAddons: error marking ${app.fqdn} for restart: ${JSON.stringify(addTaskError)}`);
else debug(`restartAppsUsingAddons: marked ${app.id} for restart with taskId ${taskId}`);
}