Files
cloudron-box/src/apptask.js

1086 lines
47 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
'use strict';
exports = module.exports = {
run,
2019-08-26 15:55:57 -07:00
// exported for testing
_configureReverseProxy: configureReverseProxy,
_unconfigureReverseProxy: unconfigureReverseProxy,
_createAppDir: createAppDir,
_deleteAppDir: deleteAppDir,
_verifyManifest: verifyManifest,
_registerSubdomains: registerSubdomains,
_unregisterSubdomains: unregisterSubdomains,
_waitForDnsPropagation: waitForDnsPropagation
};
const appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
2019-10-30 11:02:21 -07:00
auditSource = require('./auditsource.js'),
backups = require('./backups.js'),
2019-09-05 17:13:07 -07:00
BoxError = require('./boxerror.js'),
2020-01-31 13:33:19 -08:00
collectd = require('./collectd.js'),
constants = require('./constants.js'),
debug = require('debug')('box:apptask'),
df = require('@sindresorhus/df'),
2015-10-19 14:09:20 -07:00
docker = require('./docker.js'),
domains = require('./domains.js'),
ejs = require('ejs'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
iputils = require('./iputils.js'),
manifestFormat = require('cloudron-manifestformat'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
2018-06-04 11:45:22 +02:00
rimraf = require('rimraf'),
safe = require('safetydance'),
services = require('./services.js'),
settings = require('./settings.js'),
shell = require('./shell.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
util = require('util'),
_ = require('underscore');
2020-01-31 13:37:07 -08:00
const COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd/app.ejs', { encoding: 'utf8' }),
MV_VOLUME_CMD = path.join(__dirname, 'scripts/mvvolume.sh'),
LOGROTATE_CONFIG_EJS = fs.readFileSync(__dirname + '/logrotate.ejs', { encoding: 'utf8' }),
CONFIGURE_LOGROTATE_CMD = path.join(__dirname, 'scripts/configurelogrotate.sh');
2016-01-05 12:14:39 +01:00
function debugApp(app) {
assert.strictEqual(typeof app, 'object');
2018-02-08 15:07:49 +01:00
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
2019-09-24 10:28:50 -07:00
function makeTaskError(error, app) {
2019-12-05 09:32:45 -08:00
assert.strictEqual(typeof error, 'object');
assert.strictEqual(typeof app, 'object');
2019-09-24 10:28:50 -07:00
// track a few variables which helps 'repair' restart the task (see also scheduleTask in apps.js)
2019-11-22 14:27:41 -08:00
error.details.taskId = app.taskId;
error.details.installationState = app.installationState;
return error.toPlainObject();
}
2017-12-21 01:04:38 -08:00
// updates the app object and the database
function updateApp(app, values, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof values, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'updating app with values: %j', values);
appdb.update(app.id, values, function (error) {
2019-10-24 20:31:45 -07:00
if (error) return callback(error);
2017-12-21 01:04:38 -08:00
for (var value in values) {
app[value] = values[value];
}
2019-09-06 11:19:58 -07:00
callback(null);
2017-12-21 01:04:38 -08:00
});
}
function allocateContainerIp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
async.retry({ times: 10 }, function (retryCallback) {
const iprange = iputils.intFromIp('172.18.20.255') - iputils.intFromIp('172.18.16.1');
let rnd = Math.floor(Math.random() * iprange);
const containerIp = iputils.ipFromInt(iputils.intFromIp('172.18.16.1') + rnd);
updateApp(app, { containerIp }, retryCallback);
}, callback);
}
function configureReverseProxy(app, callback) {
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
2019-09-06 11:19:58 -07:00
reverseProxy.configureApp(app, { userId: null, username: 'apptask' }, function (error) {
2019-10-24 10:31:56 -07:00
if (error) return callback(error);
2019-09-06 11:19:58 -07:00
callback(null);
});
}
function unconfigureReverseProxy(app, callback) {
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
2019-09-06 11:19:58 -07:00
reverseProxy.unconfigureApp(app, function (error) {
2019-10-24 10:31:56 -07:00
if (error) return callback(error);
2019-09-06 11:19:58 -07:00
callback(null);
});
}
function createContainer(app, callback) {
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
2015-10-19 16:22:35 -07:00
assert(!app.containerId); // otherwise, it will trigger volumeFrom
2015-10-19 18:48:56 -07:00
debugApp(app, 'creating container');
2015-10-19 21:33:53 -07:00
docker.createContainer(app, function (error, container) {
2019-10-23 09:24:51 -07:00
if (error) return callback(error);
updateApp(app, { containerId: container.id }, function (error) {
if (error) return callback(error);
// re-generate configs that rely on container id
async.series([
addLogrotateConfig.bind(null, app),
addCollectdProfile.bind(null, app)
], callback);
});
});
}
function deleteContainers(app, options, callback) {
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'deleting app containers (app, scheduler)');
2015-10-19 18:48:56 -07:00
async.series([
// remove configs that rely on container id
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
docker.stopContainers.bind(null, app.id),
docker.deleteContainers.bind(null, app.id, options),
updateApp.bind(null, app, { containerId: null })
], callback);
}
function createAppDir(app, callback) {
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
2019-09-06 11:19:58 -07:00
const appDir = path.join(paths.APPS_DATA_DIR, app.id);
fs.mkdir(appDir, { recursive: true }, function (error) {
2019-09-06 11:19:58 -07:00
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error creating directory: ${error.message}`, { appDir }));
callback(null);
});
}
function deleteAppDir(app, options, callback) {
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof callback, 'function');
const appDataDir = path.join(paths.APPS_DATA_DIR, app.id);
// resolve any symlinked data dir
const stat = safe.fs.lstatSync(appDataDir);
if (!stat) return callback(null);
2018-09-14 14:37:20 +02:00
const resolvedAppDataDir = stat.isSymbolicLink() ? safe.fs.readlinkSync(appDataDir) : appDataDir;
if (safe.fs.existsSync(resolvedAppDataDir)) {
const entries = safe.fs.readdirSync(resolvedAppDataDir);
2019-09-06 11:19:58 -07:00
if (!entries) return callback(new BoxError(BoxError.FS_ERROR, `Error listing ${resolvedAppDataDir}: ${safe.error.message}`));
// remove only files. directories inside app dir are currently volumes managed by the addons
// we cannot delete those dirs anyway because of perms
entries.forEach(function (entry) {
let stat = safe.fs.statSync(path.join(resolvedAppDataDir, entry));
if (stat && !stat.isDirectory()) safe.fs.unlinkSync(path.join(resolvedAppDataDir, entry));
});
}
// if this fails, it's probably because the localstorage/redis addons have not cleaned up properly
if (options.removeDirectory) {
if (stat.isSymbolicLink()) {
2019-09-06 11:19:58 -07:00
if (!safe.fs.unlinkSync(appDataDir)) {
if (safe.error.code !== 'ENOENT') return callback(new BoxError(BoxError.FS_ERROR, `Error unlinking dir ${appDataDir} : ${safe.error.message}`));
}
} else {
2019-09-06 11:19:58 -07:00
if (!safe.fs.rmdirSync(appDataDir)) {
if (safe.error.code !== 'ENOENT') return callback(new BoxError(BoxError.FS_ERROR, `Error removing dir ${appDataDir} : ${safe.error.message}`));
}
}
}
callback(null);
}
function addCollectdProfile(app, callback) {
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
2019-08-17 03:37:22 -07:00
var collectdConf = ejs.render(COLLECTD_CONFIG_EJS, { appId: app.id, containerId: app.containerId, appDataDir: apps.getDataDir(app, app.dataDir) });
2020-01-31 13:33:19 -08:00
collectd.addProfile(app.id, collectdConf, callback);
}
function removeCollectdProfile(app, callback) {
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
2020-01-31 13:33:19 -08:00
collectd.removeProfile(app.id, callback);
}
function addLogrotateConfig(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
docker.inspect(app.containerId, function (error, result) {
2019-10-23 09:24:51 -07:00
if (error) return callback(error);
var runVolume = result.Mounts.find(function (mount) { return mount.Destination === '/run'; });
2019-09-06 11:19:58 -07:00
if (!runVolume) return callback(new BoxError(BoxError.DOCKER_ERROR, 'App does not have /run mounted'));
2017-09-12 21:45:42 -07:00
// logrotate configs can have arbitrary commands, so the config files must be owned by root
var logrotateConf = ejs.render(LOGROTATE_CONFIG_EJS, { volumePath: runVolume.Source, appId: app.id });
var tmpFilePath = path.join(os.tmpdir(), app.id + '.logrotate');
fs.writeFile(tmpFilePath, logrotateConf, function (error) {
2019-09-06 11:19:58 -07:00
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error writing logrotate config: ${error.message}`));
shell.sudo('addLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'add', app.id, tmpFilePath ], {}, function (error) {
if (error) return callback(new BoxError(BoxError.LOGROTATE_ERROR, `Error adding logrotate config: ${error.message}`));
callback(null);
});
});
});
}
function removeLogrotateConfig(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
2019-09-06 11:19:58 -07:00
shell.sudo('removeLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'remove', app.id ], {}, function (error) {
if (error) return callback(new BoxError(BoxError.LOGROTATE_ERROR, `Error removing logrotate config: ${error.message}`));
callback(null);
});
}
function cleanupLogs(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
// note that redis container logs are cleaned up by the addon
rimraf(path.join(paths.LOG_DIR, app.id), function (error) {
if (error) debugApp(app, 'cannot cleanup logs: %s', error);
callback(null);
});
}
function verifyManifest(manifest, callback) {
assert.strictEqual(typeof manifest, 'object');
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof callback, 'function');
var error = manifestFormat.parse(manifest);
2019-09-05 17:13:07 -07:00
if (error) return callback(new BoxError(BoxError.BAD_FIELD, `Manifest error: ${error.message}`, { field: 'manifest' }));
error = apps.checkManifestConstraints(manifest);
2019-09-05 17:13:07 -07:00
if (error) return callback(new BoxError(BoxError.CONFLICT, `Manifest constraint check failed: ${error.message}`, { field: 'manifest' }));
2019-09-05 17:13:07 -07:00
callback(null);
}
function downloadIcon(app, callback) {
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
// nothing to download if we dont have an appStoreId
if (!app.appStoreId) return callback(null);
debugApp(app, 'Downloading icon of %s@%s', app.appStoreId, app.manifest.version);
var iconUrl = settings.apiServerOrigin() + '/api/v1/apps/' + app.appStoreId + '/versions/' + app.manifest.version + '/icon';
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
superagent
.get(iconUrl)
.buffer(true)
.timeout(30 * 1000)
.end(function (error, res) {
2019-09-06 11:19:58 -07:00
if (error && !error.response) return retryCallback(new BoxError(BoxError.NETWORK_ERROR, `Network error downloading icon : ${error.message}`));
if (res.statusCode !== 200) return retryCallback(null); // ignore error. this can also happen for apps installed with cloudron-cli
2019-09-06 11:19:58 -07:00
const iconPath = path.join(paths.APP_ICONS_DIR, app.id + '.png');
if (!safe.fs.writeFileSync(iconPath, res.body)) return retryCallback(new BoxError(BoxError.FS_ERROR, `Error saving icon to ${iconPath}: ${safe.error.message}`));
retryCallback(null);
2017-09-17 21:25:07 -07:00
});
}, callback);
}
2019-09-06 11:19:58 -07:00
function removeIcon(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (!safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, app.id + '.png'))) {
if (safe.error.code !== 'ENOENT') debugApp(app, 'cannot remove icon : %s', safe.error);
}
if (!safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, app.id + '.user.png'))) {
if (safe.error.code !== 'ENOENT') debugApp(app, 'cannot remove user icon : %s', safe.error);
}
callback(null);
}
function registerSubdomains(app, overwrite, callback) {
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof app, 'object');
2017-02-02 10:40:10 -08:00
assert.strictEqual(typeof overwrite, 'boolean');
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof callback, 'function');
2019-10-29 15:46:33 -07:00
sysinfo.getServerIp(function (error, ip) {
2016-01-05 12:16:39 +01:00
if (error) return callback(error);
2021-01-18 17:26:26 -08:00
const allDomains = [ { subdomain: app.location, domain: app.domain }].concat(app.alternateDomains).concat(app.aliasDomains);
2020-11-20 11:17:24 -08:00
debugApp(app, `registerSubdomain: Will register ${JSON.stringify(allDomains)}`);
2019-09-16 09:22:43 -07:00
async.eachSeries(allDomains, function (domain, iteratorDone) {
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering subdomain: %s%s', domain.subdomain ? (domain.subdomain + '.') : '', domain.domain);
// get the current record before updating it
domains.getDnsRecords(domain.subdomain, domain.domain, 'A', function (error, values) {
2019-10-23 10:02:04 -07:00
if (error && error.reason === BoxError.EXTERNAL_ERROR) return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain })); // try again
if (error && error.reason === BoxError.ACCESS_DENIED) return retryCallback(null, new BoxError(BoxError.ACCESS_DENIED, error.message, { domain }));
if (error && error.reason === BoxError.NOT_FOUND) return retryCallback(null, new BoxError(BoxError.NOT_FOUND, error.message, { domain }));
2019-09-23 10:49:50 -07:00
if (error) return retryCallback(null, new BoxError(BoxError.EXTERNAL_ERROR, error.message, domain)); // give up for other errors
2019-09-10 15:23:47 -07:00
if (values.length !== 0 && values[0] === ip) return retryCallback(null); // up-to-date
// refuse to update any existing DNS record for custom domains that we did not create
2019-09-19 22:45:44 -07:00
if (values.length !== 0 && !overwrite) return retryCallback(null, new BoxError(BoxError.ALREADY_EXISTS, 'DNS Record already exists', { domain }));
2018-06-29 22:25:34 +02:00
domains.upsertDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
2019-10-23 10:02:04 -07:00
if (error && (error.reason === BoxError.BUSY || error.reason === BoxError.EXTERNAL_ERROR)) {
2020-11-20 11:17:24 -08:00
debugApp(app, 'registerSubdomains: Upsert error. Will retry.', error.message);
2019-09-19 22:45:44 -07:00
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain })); // try again
}
2019-09-05 17:13:07 -07:00
retryCallback(null, error ? new BoxError(BoxError.EXTERNAL_ERROR, error.message, domain) : null);
});
});
}, function (error, result) {
2019-09-05 17:13:07 -07:00
if (error || result) return iteratorDone(error || result);
iteratorDone(null);
});
}, callback);
});
}
function unregisterSubdomains(app, allDomains, callback) {
assert.strictEqual(typeof app, 'object');
assert(Array.isArray(allDomains));
assert.strictEqual(typeof callback, 'function');
2019-10-29 15:46:33 -07:00
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
async.eachSeries(allDomains, function (domain, iteratorDone) {
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s%s', domain.subdomain ? (domain.subdomain + '.') : '', domain.domain);
domains.removeDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
2019-10-23 10:02:04 -07:00
if (error && error.reason === BoxError.NOT_FOUND) return retryCallback(null, null);
if (error && (error.reason === BoxError.SBUSY || error.reason === BoxError.EXTERNAL_ERROR)) {
2020-11-20 11:17:24 -08:00
debugApp(app, 'registerSubdomains: Remove error. Will retry.', error.message);
2019-09-19 22:45:44 -07:00
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain })); // try again
2019-09-05 17:13:07 -07:00
}
2019-09-19 22:45:44 -07:00
retryCallback(null, error ? new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain }) : null);
});
}, function (error, result) {
2019-09-05 17:13:07 -07:00
if (error || result) return iteratorDone(error || result);
iteratorDone();
});
}, callback);
});
}
function waitForDnsPropagation(app, callback) {
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (!constants.CLOUDRON) {
debugApp(app, 'Skipping dns propagation check for development');
return callback(null);
}
2019-10-29 15:46:33 -07:00
sysinfo.getServerIp(function (error, ip) {
2019-09-06 11:19:58 -07:00
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Error getting public IP: ${error.message}`));
domains.waitForDnsRecord(app.location, app.domain, 'A', ip, { times: 240 }, function (error) {
2019-09-06 11:19:58 -07:00
if (error) return callback(new BoxError(BoxError.DNS_ERROR, `DNS Record is not synced yet: ${error.message}`, { ip: ip, subdomain: app.location, domain: app.domain }));
2021-01-18 17:26:26 -08:00
// now wait for alternateDomains and aliasDomains, if any
async.eachSeries(app.alternateDomains.concat(app.aliasDomains), function (domain, iteratorCallback) {
domains.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { times: 240 }, function (error) {
2019-09-06 11:19:58 -07:00
if (error) return callback(new BoxError(BoxError.DNS_ERROR, `DNS Record is not synced yet: ${error.message}`, { ip: ip, subdomain: domain.subdomain, domain: domain.domain }));
iteratorCallback();
});
}, callback);
});
});
}
function moveDataDir(app, targetDir, callback) {
assert.strictEqual(typeof app, 'object');
assert(targetDir === null || typeof targetDir === 'string');
assert.strictEqual(typeof callback, 'function');
let resolvedSourceDir = apps.getDataDir(app, app.dataDir);
let resolvedTargetDir = apps.getDataDir(app, targetDir);
2020-11-20 11:17:24 -08:00
debugApp(app, `moveDataDir: migrating data from ${resolvedSourceDir} to ${resolvedTargetDir}`);
if (resolvedSourceDir === resolvedTargetDir) return callback();
2019-09-08 16:57:08 -07:00
shell.sudo('moveDataDir', [ MV_VOLUME_CMD, resolvedSourceDir, resolvedTargetDir ], {}, function (error) {
2019-09-06 11:19:58 -07:00
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Error migrating data directory: ${error.message}`));
callback(null);
});
}
function downloadImage(manifest, callback) {
assert.strictEqual(typeof manifest, 'object');
assert.strictEqual(typeof callback, 'function');
docker.info(function (error, info) {
2019-10-23 09:24:51 -07:00
if (error) return callback(error);
const dfAsync = util.callbackify(df.file);
dfAsync(info.DockerRootDir, function (error, diskUsage) {
2019-09-06 11:19:58 -07:00
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error getting file system info: ${error.message}`));
if (diskUsage.available < (1024*1024*1024)) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Not enough disk space to pull docker image', { diskUsage: diskUsage, dockerRootDir: info.DockerRootDir }));
2019-09-06 11:19:58 -07:00
docker.downloadImage(manifest, function (error) {
2019-10-23 09:24:51 -07:00
if (error) return callback(error);
2019-09-06 11:19:58 -07:00
callback(null);
});
});
});
}
function startApp(app, callback){
2020-11-20 11:17:24 -08:00
debugApp(app, 'startApp: starting container');
if (app.runState === apps.RSTATE_STOPPED) return callback();
docker.startContainer(app.id, callback);
}
function install(app, args, progressCallback, callback) {
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof args, 'object');
2019-08-26 20:43:15 -07:00
assert.strictEqual(typeof progressCallback, 'function');
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof callback, 'function');
const restoreConfig = args.restoreConfig; // has to be set when restoring
const overwriteDns = args.overwriteDns;
const skipDnsSetup = args.skipDnsSetup;
2019-12-06 09:42:05 -08:00
const oldManifest = args.oldManifest;
2017-04-11 12:49:21 -07:00
async.series([
// this protects against the theoretical possibility of an app being marked for install/restore from
// a previous version of box code
verifyManifest.bind(null, app.manifest),
// teardown for re-installs
2019-08-26 20:43:15 -07:00
progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }),
unconfigureReverseProxy.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
function teardownAddons(next) {
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
2019-09-30 09:58:13 -07:00
let addonsToRemove;
2019-12-06 09:42:05 -08:00
if (oldManifest) {
addonsToRemove = _.omit(oldManifest.addons, Object.keys(app.manifest.addons));
2019-09-30 09:58:13 -07:00
} else {
addonsToRemove = app.manifest.addons;
}
services.teardownAddons(app, addonsToRemove, next);
},
function deleteAppDirIfNeeded(done) {
if (restoreConfig && !restoreConfig.backupId) return done(); // in-place import should not delete data dir
deleteAppDir(app, { removeDirectory: false }, done); // do not remove any symlinked appdata dir
},
function deleteImageIfChanged(done) {
2019-12-06 09:42:05 -08:00
if (!oldManifest || oldManifest.dockerImage === app.manifest.dockerImage) return done();
2019-12-06 09:42:05 -08:00
docker.deleteImage(oldManifest, done);
},
// allocating container ip here, lets the users "repair" an app if allocation fails at appdb.add time
allocateContainerIp.bind(null, app),
2019-08-26 20:43:15 -07:00
progressCallback.bind(null, { percent: 20, message: 'Downloading icon' }),
downloadIcon.bind(null, app),
function setupDnsIfNeeded(done) {
if (skipDnsSetup) return done();
async.series([
progressCallback.bind(null, { percent: 30, message: 'Registering subdomains' }),
registerSubdomains.bind(null, app, overwriteDns)
], done);
},
2018-09-22 10:09:46 -07:00
2019-08-26 20:43:15 -07:00
progressCallback.bind(null, { percent: 40, message: 'Downloading image' }),
downloadImage.bind(null, app.manifest),
2019-08-26 20:43:15 -07:00
progressCallback.bind(null, { percent: 50, message: 'Creating app data directory' }),
createAppDir.bind(null, app),
2017-04-11 12:49:21 -07:00
function restoreFromBackup(next) {
2019-09-30 09:58:13 -07:00
if (!restoreConfig) {
async.series([
2019-08-26 20:43:15 -07:00
progressCallback.bind(null, { percent: 60, message: 'Setting up addons' }),
services.setupAddons.bind(null, app, app.manifest.addons),
], next);
} else if (!restoreConfig.backupId) { // in-place import
async.series([
progressCallback.bind(null, { percent: 60, message: 'Importing addons in-place' }),
services.setupAddons.bind(null, app, app.manifest.addons),
services.clearAddons.bind(null, app, _.omit(app.manifest.addons, 'localstorage')),
services.restoreAddons.bind(null, app, app.manifest.addons),
], next);
} else {
async.series([
2019-08-26 20:43:15 -07:00
progressCallback.bind(null, { percent: 65, message: 'Download backup and restoring addons' }),
services.setupAddons.bind(null, app, app.manifest.addons),
services.clearAddons.bind(null, app, app.manifest.addons),
backups.downloadApp.bind(null, app, restoreConfig, (progress) => {
progressCallback({ percent: 65, message: progress.message });
}),
services.restoreAddons.bind(null, app, app.manifest.addons)
], next);
}
2017-04-11 12:49:21 -07:00
},
2019-08-26 20:43:15 -07:00
progressCallback.bind(null, { percent: 70, message: 'Creating container' }),
createContainer.bind(null, app),
startApp.bind(null, app),
2019-08-26 20:43:15 -07:00
progressCallback.bind(null, { percent: 85, message: 'Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
progressCallback.bind(null, { percent: 95, message: 'Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
2019-08-26 20:43:15 -07:00
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
if (error) {
debugApp(app, 'error installing app: %s', error);
2019-09-24 10:28:50 -07:00
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
}
callback(null);
});
}
function backup(app, args, progressCallback, callback) {
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof args, 'object');
2019-08-26 22:11:27 -07:00
assert.strictEqual(typeof progressCallback, 'function');
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof callback, 'function');
async.series([
2019-08-26 22:11:27 -07:00
progressCallback.bind(null, { percent: 10, message: 'Backing up' }),
backups.backupApp.bind(null, app, { snapshotOnly: !!args.snapshotOnly }, (progress) => {
2019-08-26 22:11:27 -07:00
progressCallback({ percent: 30, message: progress.message });
}),
2019-08-26 22:11:27 -07:00
progressCallback.bind(null, { percent: 100, message: 'Done' }),
2019-08-30 13:12:49 -07:00
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null })
], function seriesDone(error) {
if (error) {
debugApp(app, 'error backing up app: %s', error);
// return to installed state intentionally. the error is stashed only in the task and not the app (the UI shows error state otherwise)
return updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null }, callback.bind(null, makeTaskError(error, app)));
}
callback(null);
});
}
function create(app, args, progressCallback, callback) {
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof args, 'object');
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
async.series([
progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }),
deleteContainers.bind(null, app, { managedOnly: true }),
2019-09-09 15:58:58 -07:00
// FIXME: re-setup addons only because sendmail addon to re-inject env vars on mailboxName change
progressCallback.bind(null, { percent: 30, message: 'Setting up addons' }),
services.setupAddons.bind(null, app, app.manifest.addons),
2019-09-09 15:58:58 -07:00
2019-09-08 16:57:08 -07:00
progressCallback.bind(null, { percent: 60, message: 'Creating container' }),
createContainer.bind(null, app),
startApp.bind(null, app),
2019-09-08 16:57:08 -07:00
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
if (error) {
debugApp(app, 'error creating : %s', error);
2019-09-24 10:28:50 -07:00
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
2019-09-08 16:57:08 -07:00
}
callback(null);
});
}
2019-09-10 15:23:47 -07:00
function changeLocation(app, args, progressCallback, callback) {
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof app, 'object');
2019-09-10 15:23:47 -07:00
assert.strictEqual(typeof args, 'object');
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
2019-09-10 15:23:47 -07:00
const oldConfig = args.oldConfig;
2019-09-08 16:57:08 -07:00
const locationChanged = oldConfig.fqdn !== app.fqdn;
const skipDnsSetup = args.skipDnsSetup;
const overwriteDns = args.overwriteDns;
2019-09-08 16:57:08 -07:00
async.series([
progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }),
unconfigureReverseProxy.bind(null, app),
2019-09-08 16:57:08 -07:00
deleteContainers.bind(null, app, { managedOnly: true }),
function (next) {
let obsoleteDomains = oldConfig.alternateDomains.filter(function (o) {
return !app.alternateDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; });
});
2021-01-18 17:26:26 -08:00
if (oldConfig.aliasDomains) {
obsoleteDomains = obsoleteDomains.concat(oldConfig.aliasDomains.filter(function (o) {
return !app.aliasDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; });
}));
}
2019-09-08 16:57:08 -07:00
if (locationChanged) obsoleteDomains.push({ subdomain: oldConfig.location, domain: oldConfig.domain });
if (obsoleteDomains.length === 0) return next();
unregisterSubdomains(app, obsoleteDomains, next);
},
function setupDnsIfNeeded(done) {
if (skipDnsSetup) return done();
async.series([
progressCallback.bind(null, { percent: 30, message: 'Registering subdomains' }),
registerSubdomains.bind(null, app, overwriteDns)
], done);
},
2019-09-08 16:57:08 -07:00
// re-setup addons since they rely on the app's fqdn (e.g oauth)
progressCallback.bind(null, { percent: 50, message: 'Setting up addons' }),
services.setupAddons.bind(null, app, app.manifest.addons),
2019-09-08 16:57:08 -07:00
progressCallback.bind(null, { percent: 60, message: 'Creating container' }),
createContainer.bind(null, app),
startApp.bind(null, app),
2019-09-08 16:57:08 -07:00
progressCallback.bind(null, { percent: 80, message: 'Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
2019-09-08 16:57:08 -07:00
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
if (error) {
2019-12-05 16:27:00 -08:00
debugApp(app, 'error changing location : %s', error);
2019-09-24 10:28:50 -07:00
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
2019-09-08 16:57:08 -07:00
}
callback(null);
});
}
function migrateDataDir(app, args, progressCallback, callback) {
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof args, 'object');
2019-09-08 16:57:08 -07:00
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
let newDataDir = args.newDataDir;
assert(newDataDir === null || typeof newDataDir === 'string');
2019-09-08 16:57:08 -07:00
async.series([
progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }),
deleteContainers.bind(null, app, { managedOnly: true }),
progressCallback.bind(null, { percent: 45, message: 'Ensuring app data directory' }),
createAppDir.bind(null, app),
2019-09-09 16:37:59 -07:00
// re-setup addons since this creates the localStorage volume
progressCallback.bind(null, { percent: 50, message: 'Setting up addons' }),
services.setupAddons.bind(null, _.extend({}, app, { dataDir: newDataDir }), app.manifest.addons),
2019-09-09 16:37:59 -07:00
progressCallback.bind(null, { percent: 60, message: 'Moving data dir' }),
moveDataDir.bind(null, app, newDataDir),
2019-09-08 16:57:08 -07:00
progressCallback.bind(null, { percent: 90, message: 'Creating container' }),
2019-09-08 16:57:08 -07:00
createContainer.bind(null, app),
startApp.bind(null, app),
2019-09-08 16:57:08 -07:00
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null, dataDir: newDataDir })
2019-09-08 16:57:08 -07:00
], function seriesDone(error) {
if (error) {
2019-12-05 16:27:00 -08:00
debugApp(app, 'error migrating data dir : %s', error);
2019-09-24 10:28:50 -07:00
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
2019-09-08 16:57:08 -07:00
}
2020-08-19 18:23:44 +02:00
callback();
2019-09-08 16:57:08 -07:00
});
}
// configure is called for an infra update and repair to re-create container, reverseproxy config. it's all "local"
function configure(app, args, progressCallback, callback) {
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof args, 'object');
2019-08-26 21:36:18 -07:00
assert.strictEqual(typeof progressCallback, 'function');
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof callback, 'function');
async.series([
2019-08-26 21:36:18 -07:00
progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }),
unconfigureReverseProxy.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
2019-08-26 21:36:18 -07:00
progressCallback.bind(null, { percent: 20, message: 'Downloading icon' }),
2017-03-15 20:24:03 -07:00
downloadIcon.bind(null, app),
2019-08-26 21:36:18 -07:00
progressCallback.bind(null, { percent: 40, message: 'Downloading image' }),
downloadImage.bind(null, app.manifest),
2019-08-26 21:36:18 -07:00
progressCallback.bind(null, { percent: 45, message: 'Ensuring app data directory' }),
createAppDir.bind(null, app),
// re-setup addons since they rely on the app's fqdn (e.g oauth)
2019-08-26 21:36:18 -07:00
progressCallback.bind(null, { percent: 50, message: 'Setting up addons' }),
services.setupAddons.bind(null, app, app.manifest.addons),
2019-08-26 21:36:18 -07:00
progressCallback.bind(null, { percent: 60, message: 'Creating container' }),
createContainer.bind(null, app),
startApp.bind(null, app),
progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
2019-08-26 21:36:18 -07:00
progressCallback.bind(null, { percent: 100, message: 'Done' }),
2019-08-30 13:12:49 -07:00
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
if (error) {
debugApp(app, 'error reconfiguring : %s', error);
2019-09-24 10:28:50 -07:00
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
}
callback();
});
}
function update(app, args, progressCallback, callback) {
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof args, 'object');
2019-08-26 22:08:40 -07:00
assert.strictEqual(typeof progressCallback, 'function');
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof callback, 'function');
const updateConfig = args.updateConfig;
debugApp(app, `Updating to ${updateConfig.manifest.version}`);
// app does not want these addons anymore
// FIXME: this does not handle option changes (like multipleDatabases)
const unusedAddons = _.omit(app.manifest.addons, Object.keys(updateConfig.manifest.addons));
const httpPathsChanged = app.manifest.httpPaths !== updateConfig.manifest.httpPaths;
const httpPortChanged = app.manifest.httpPort !== updateConfig.manifest.httpPort;
const proxyAuthChanged = !_.isEqual(safe.query(app.manifest, 'addons.proxyAuth'), safe.query(updateConfig.manifest, 'addons.proxyAuth'));
async.series([
// this protects against the theoretical possibility of an app being marked for update from
// a previous version of box code
2019-12-18 09:33:44 -08:00
progressCallback.bind(null, { percent: 5, message: 'Verify manifest' }),
verifyManifest.bind(null, updateConfig.manifest),
function (next) {
2019-08-29 10:59:05 -07:00
if (updateConfig.skipBackup) return next(null);
async.series([
2019-08-26 22:08:40 -07:00
progressCallback.bind(null, { percent: 15, message: 'Backing up app' }),
// preserve update backups for 3 weeks
2019-08-26 22:11:27 -07:00
backups.backupApp.bind(null, app, { preserveSecs: 3*7*24*60*60 }, (progress) => {
progressCallback({ percent: 15, message: `Backup - ${progress.message}` });
})
], function (error) {
if (error) error.backupError = true;
next(error);
});
},
// download new image before app is stopped. this is so we can reduce downtime
// and also not remove the 'common' layers when the old image is deleted
2019-08-26 22:08:40 -07:00
progressCallback.bind(null, { percent: 25, message: 'Downloading image' }),
downloadImage.bind(null, updateConfig.manifest),
// note: we cleanup first and then backup. this is done so that the app is not running should backup fail
// we cannot easily 'recover' from backup failures because we have to revert manfest and portBindings
2019-08-26 22:08:40 -07:00
progressCallback.bind(null, { percent: 35, message: 'Cleaning up old install' }),
deleteContainers.bind(null, app, { managedOnly: true }),
function deleteImageIfChanged(done) {
if (app.manifest.dockerImage === updateConfig.manifest.dockerImage) return done();
docker.deleteImage(app.manifest, done);
2015-07-20 10:09:02 -07:00
},
2016-06-13 23:07:41 -07:00
// only delete unused addons after backup
services.teardownAddons.bind(null, app, unusedAddons),
2016-06-13 23:07:41 -07:00
// free unused ports
function (next) {
const currentPorts = app.portBindings || {};
const newTcpPorts = updateConfig.manifest.tcpPorts || {};
const newUdpPorts = updateConfig.manifest.udpPorts || {};
async.each(Object.keys(currentPorts), function (portName, callback) {
2019-09-06 11:19:58 -07:00
if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(null); // port still in use
appdb.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP, function (error) {
2020-11-20 11:17:24 -08:00
if (error && error.reason === BoxError.NOT_FOUND) debugApp(app, 'update: portbinding does not exist in database', error);
else if (error) return next(error);
// also delete from app object for further processing (the db is updated in the next step)
delete app.portBindings[portName];
2019-09-06 11:19:58 -07:00
callback(null);
});
}, next);
},
2019-08-29 20:47:38 -07:00
updateApp.bind(null, app, _.pick(updateConfig, 'manifest', 'appStoreId', 'memoryLimit')), // switch over to the new config
2019-08-26 22:08:40 -07:00
progressCallback.bind(null, { percent: 45, message: 'Downloading icon' }),
downloadIcon.bind(null, app),
progressCallback.bind(null, { percent: 60, message: 'Updating addons' }),
services.setupAddons.bind(null, app, updateConfig.manifest.addons),
progressCallback.bind(null, { percent: 70, message: 'Creating container' }),
createContainer.bind(null, app),
startApp.bind(null, app),
progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }),
function (next) {
if (!httpPathsChanged && !proxyAuthChanged && !httpPortChanged) return next();
configureReverseProxy(app, next);
},
2019-08-26 22:08:40 -07:00
progressCallback.bind(null, { percent: 100, message: 'Done' }),
2019-08-30 13:12:49 -07:00
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null, updateTime: new Date() })
], function seriesDone(error) {
if (error && error.backupError) {
debugApp(app, 'update aborted because backup failed', error);
2019-08-30 13:12:49 -07:00
updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }, callback.bind(null, error));
} else if (error) {
debugApp(app, 'Error updating app: %s', error);
2019-09-24 10:28:50 -07:00
updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
} else {
2019-10-30 11:02:21 -07:00
eventlog.add(eventlog.ACTION_APP_UPDATE_FINISH, auditSource.APP_TASK, { app: app, success: true }, () => callback()); // ignore error
}
});
}
function start(app, args, progressCallback, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof args, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
async.series([
progressCallback.bind(null, { percent: 10, message: 'Starting app services' }),
services.startAppServices.bind(null, app),
progressCallback.bind(null, { percent: 35, message: 'Starting container' }),
docker.startContainer.bind(null, app.id),
progressCallback.bind(null, { percent: 60, message: 'Adding collectd profile' }),
addCollectdProfile.bind(null, app),
// stopped apps do not renew certs. currently, we don't do DNS to not overwrite existing user settings
progressCallback.bind(null, { percent: 80, message: 'Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
if (error) {
debugApp(app, 'error starting app: %s', error);
2019-09-24 10:28:50 -07:00
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
}
callback(null);
});
}
function stop(app, args, progressCallback, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof args, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
async.series([
progressCallback.bind(null, { percent: 20, message: 'Stopping container' }),
docker.stopContainers.bind(null, app.id),
progressCallback.bind(null, { percent: 50, message: 'Stopping app services' }),
services.stopAppServices.bind(null, app),
progressCallback.bind(null, { percent: 80, message: 'Removing collectd profile' }),
removeCollectdProfile.bind(null, app),
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
if (error) {
debugApp(app, 'error starting app: %s', error);
2019-09-24 10:28:50 -07:00
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
}
callback(null);
});
}
2019-12-20 10:29:29 -08:00
function restart(app, args, progressCallback, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof args, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
async.series([
progressCallback.bind(null, { percent: 20, message: 'Restarting container' }),
docker.restartContainer.bind(null, app.id),
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
if (error) {
debugApp(app, 'error starting app: %s', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
}
callback(null);
});
}
function uninstall(app, args, progressCallback, callback) {
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof args, 'object');
2019-08-26 21:56:55 -07:00
assert.strictEqual(typeof progressCallback, 'function');
2016-01-05 12:14:39 +01:00
assert.strictEqual(typeof callback, 'function');
async.series([
2019-08-26 21:56:55 -07:00
progressCallback.bind(null, { percent: 20, message: 'Deleting container' }),
unconfigureReverseProxy.bind(null, app),
deleteContainers.bind(null, app, {}),
2019-08-26 21:56:55 -07:00
progressCallback.bind(null, { percent: 30, message: 'Teardown addons' }),
services.teardownAddons.bind(null, app, app.manifest.addons),
progressCallback.bind(null, { percent: 40, message: 'Cleanup file manager' }),
progressCallback.bind(null, { percent: 50, message: 'Deleting app data directory' }),
deleteAppDir.bind(null, app, { removeDirectory: true }),
progressCallback.bind(null, { percent: 60, message: 'Deleting image' }),
2015-10-19 15:39:26 -07:00
docker.deleteImage.bind(null, app.manifest),
progressCallback.bind(null, { percent: 70, message: 'Unregistering domains' }),
2021-01-18 17:26:26 -08:00
unregisterSubdomains.bind(null, app, [ { subdomain: app.location, domain: app.domain } ].concat(app.alternateDomains).concat(app.aliasDomains)),
progressCallback.bind(null, { percent: 80, message: 'Cleanup icon' }),
removeIcon.bind(null, app),
2019-08-26 21:56:55 -07:00
progressCallback.bind(null, { percent: 90, message: 'Cleanup logs' }),
2018-06-04 11:45:22 +02:00
cleanupLogs.bind(null, app),
2019-08-26 21:56:55 -07:00
progressCallback.bind(null, { percent: 95, message: 'Remove app from database' }),
appdb.del.bind(null, app.id)
], function seriesDone(error) {
if (error) {
debugApp(app, 'error uninstalling app: %s', error);
2019-09-24 10:28:50 -07:00
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
}
callback(null);
});
}
function run(appId, args, progressCallback, callback) {
2019-08-26 15:55:57 -07:00
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof args, 'object');
2019-08-26 15:55:57 -07:00
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
// determine what to do
apps.get(appId, function (error, app) {
if (error) return callback(error);
debugApp(app, 'startTask installationState: %s runState: %s', app.installationState, app.runState);
switch (app.installationState) {
case apps.ISTATE_PENDING_INSTALL:
case apps.ISTATE_PENDING_CLONE:
case apps.ISTATE_PENDING_RESTORE:
return install(app, args, progressCallback, callback);
case apps.ISTATE_PENDING_CONFIGURE:
return configure(app, args, progressCallback, callback);
case apps.ISTATE_PENDING_RECREATE_CONTAINER:
case apps.ISTATE_PENDING_RESIZE:
case apps.ISTATE_PENDING_DEBUG:
return create(app, args, progressCallback, callback);
case apps.ISTATE_PENDING_LOCATION_CHANGE:
return changeLocation(app, args, progressCallback, callback);
case apps.ISTATE_PENDING_DATA_DIR_MIGRATION:
return migrateDataDir(app, args, progressCallback, callback);
case apps.ISTATE_PENDING_UNINSTALL:
return uninstall(app, args, progressCallback, callback);
case apps.ISTATE_PENDING_UPDATE:
return update(app, args, progressCallback, callback);
case apps.ISTATE_PENDING_BACKUP:
return backup(app, args, progressCallback, callback);
case apps.ISTATE_PENDING_START:
return start(app, args, progressCallback, callback);
case apps.ISTATE_PENDING_STOP:
return stop(app, args, progressCallback, callback);
2019-12-20 10:29:29 -08:00
case apps.ISTATE_PENDING_RESTART:
return restart(app, args, progressCallback, callback);
case apps.ISTATE_INSTALLED: // can only happen when we have a bug in our code while testing/development
return updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }, callback);
default:
debugApp(app, 'apptask launched with invalid command');
return callback(new BoxError(BoxError.INTERNAL_ERROR, 'Unknown install command in apptask:' + app.installationState));
}
});
}