apps: add crontab

crontab is a text field, so we can have comments

part of #793
This commit is contained in:
Girish Ramakrishnan
2021-09-27 14:21:42 -07:00
parent 04ff8dab1b
commit b86d5ea0ea
6 changed files with 89 additions and 5 deletions

View File

@@ -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');