apps: add crontab
crontab is a text field, so we can have comments part of #793
This commit is contained in:
66
src/apps.js
66
src/apps.js
@@ -26,6 +26,7 @@ exports = module.exports = {
|
||||
|
||||
setAccessRestriction,
|
||||
setOperators,
|
||||
setCrontab,
|
||||
setLabel,
|
||||
setIcon,
|
||||
setTags,
|
||||
@@ -80,6 +81,7 @@ exports = module.exports = {
|
||||
getIcon,
|
||||
getMemoryLimit,
|
||||
getLimits,
|
||||
getSchedulerConfig,
|
||||
|
||||
listEventlog,
|
||||
|
||||
@@ -131,6 +133,7 @@ exports = module.exports = {
|
||||
_validatePortBindings: validatePortBindings,
|
||||
_validateAccessRestriction: validateAccessRestriction,
|
||||
_translatePortBindings: translatePortBindings,
|
||||
_parseCrontab: parseCrontab,
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
@@ -140,6 +143,7 @@ const appstore = require('./appstore.js'),
|
||||
backups = require('./backups.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
constants = require('./constants.js'),
|
||||
CronJob = require('cron').CronJob,
|
||||
database = require('./database.js'),
|
||||
debug = require('debug')('box:apps'),
|
||||
dns = require('./dns.js'),
|
||||
@@ -173,7 +177,7 @@ const appstore = require('./appstore.js'),
|
||||
const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState',
|
||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares',
|
||||
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson', 'apps.operatorsJson',
|
||||
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup', 'apps.proxyAuth', 'apps.containerIp',
|
||||
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup', 'apps.proxyAuth', 'apps.containerIp', 'apps.crontab',
|
||||
'apps.creationTime', 'apps.updateTime', 'apps.enableMailbox', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate',
|
||||
'apps.dataDir', 'apps.ts', 'apps.healthTime', '(apps.icon IS NOT NULL) AS hasIcon', '(apps.appStoreIcon IS NOT NULL) AS hasAppStoreIcon' ].join(',');
|
||||
|
||||
@@ -261,6 +265,50 @@ function translatePortBindings(portBindings, manifest) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseCrontab(crontab) {
|
||||
assert(crontab === null || typeof crontab === 'string');
|
||||
|
||||
const result = [];
|
||||
if (!crontab) return result;
|
||||
|
||||
const lines = crontab.split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line || line.startsWith('#')) continue;
|
||||
const parts = /(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*)/.exec(line);
|
||||
if (!parts) throw new BoxError(BoxError.BAD_FIELD, `Invalid cron configuration at line ${i+1}`);
|
||||
const schedule = parts.slice(1, 6).join(' ');
|
||||
const command = parts[6];
|
||||
|
||||
try {
|
||||
new CronJob('00 ' + schedule, function() {}); // second is disallowed
|
||||
} catch (ex) {
|
||||
throw new BoxError(BoxError.BAD_FIELD, `Invalid cron pattern at line ${i+1}`);
|
||||
}
|
||||
|
||||
if (command.length === 0) throw new BoxError(BoxError.BAD_FIELD, `Invalid cron pattern. Command must not be empty at line ${i+1}`);
|
||||
|
||||
result.push({ schedule, command });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getSchedulerConfig(app) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
|
||||
let schedulerConfig = app.manifest.addons?.scheduler || null;
|
||||
|
||||
const crontab = parseCrontab(app.crontab);
|
||||
if (crontab.length === 0) return schedulerConfig;
|
||||
|
||||
schedulerConfig = schedulerConfig || {};
|
||||
// put a '.' because it is not a valid name for schedule name in manifestformat
|
||||
crontab.forEach((c, idx) => schedulerConfig[`crontab.${idx}`] = c);
|
||||
|
||||
return schedulerConfig;
|
||||
}
|
||||
|
||||
// also validates operators
|
||||
function validateAccessRestriction(accessRestriction) {
|
||||
assert.strictEqual(typeof accessRestriction, 'object');
|
||||
@@ -444,7 +492,7 @@ function getDataDir(app, dataDir) {
|
||||
function removeInternalFields(app) {
|
||||
return _.pick(app,
|
||||
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId',
|
||||
'location', 'domain', 'fqdn', 'mailboxName', 'mailboxDomain',
|
||||
'location', 'domain', 'fqdn', 'mailboxName', 'mailboxDomain', 'crontab',
|
||||
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares', 'operators',
|
||||
'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
|
||||
'label', 'alternateDomains', 'aliasDomains', 'env', 'enableAutomaticUpdate', 'dataDir', 'mounts', 'enableMailbox');
|
||||
@@ -1226,13 +1274,25 @@ async function setOperators(app, operators, auditSource) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
const appId = app.id;
|
||||
let error = validateAccessRestriction(operators); // not a typo. same structure for operators and accessRestriction
|
||||
const error = validateAccessRestriction(operators); // not a typo. same structure for operators and accessRestriction
|
||||
if (error) throw error;
|
||||
|
||||
await update(appId, { operators });
|
||||
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, operators });
|
||||
}
|
||||
|
||||
async function setCrontab(app, crontab, auditSource) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(crontab === null || typeof crontab === 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
const appId = app.id;
|
||||
parseCrontab(crontab);
|
||||
|
||||
await update(appId, { crontab });
|
||||
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, crontab });
|
||||
}
|
||||
|
||||
async function setLabel(app, label, auditSource) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof label, 'string');
|
||||
|
||||
Reference in New Issue
Block a user