diff --git a/src/cloudron.js b/src/cloudron.js index d8917304d..aad4c6e4f 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -9,7 +9,6 @@ exports = module.exports = { getDisks: getDisks, getLogs: getLogs, - updateToLatest: updateToLatest, reboot: reboot, onActivated: onActivated, @@ -19,14 +18,10 @@ exports = module.exports = { var assert = require('assert'), async = require('async'), - backups = require('./backups.js'), - caas = require('./caas.js'), config = require('./config.js'), cron = require('./cron.js'), debug = require('debug')('box:cloudron'), df = require('@sindresorhus/df'), - eventlog = require('./eventlog.js'), - locker = require('./locker.js'), mailer = require('./mailer.js'), os = require('os'), path = require('path'), @@ -39,13 +34,10 @@ var assert = require('assert'), shell = require('./shell.js'), spawn = require('child_process').spawn, split = require('split'), - updateChecker = require('./updatechecker.js'), users = require('./users.js'), - util = require('util'), - _ = require('underscore'); + util = require('util'); -var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'), - UPDATE_CMD = path.join(__dirname, 'scripts/update.sh'); +var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'); var NOOP_CALLBACK = function (error) { if (error) debug(error); }; @@ -164,102 +156,6 @@ function reboot(callback) { shell.sudo('reboot', [ REBOOT_CMD ], callback); } -function update(boxUpdateInfo, auditSource, callback) { - assert.strictEqual(typeof boxUpdateInfo, 'object'); - assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); - - if (!boxUpdateInfo) return callback(null); - - var error = locker.lock(locker.OP_BOX_UPDATE); - if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message)); - - eventlog.add(eventlog.ACTION_UPDATE, auditSource, { boxUpdateInfo: boxUpdateInfo }); - - // ensure tools can 'wait' on progress - progress.set(progress.UPDATE, 0, 'Starting'); - - // initiate the update/upgrade but do not wait for it - if (boxUpdateInfo.upgrade) { - debug('Starting upgrade'); - caas.upgrade(boxUpdateInfo, function (error) { - if (error) { - debug('Upgrade failed with error:', error); - locker.unlock(locker.OP_BOX_UPDATE); - } - }); - } else { - debug('Starting update'); - doUpdate(boxUpdateInfo, function (error) { - if (error) { - debug('Update failed with error:', error); - locker.unlock(locker.OP_BOX_UPDATE); - } - }); - } - - callback(null); -} - -function updateToLatest(auditSource, callback) { - assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); - - var boxUpdateInfo = updateChecker.getUpdateInfo().box; - if (!boxUpdateInfo) return callback(new CloudronError(CloudronError.ALREADY_UPTODATE, 'No update available')); - if (!boxUpdateInfo.sourceTarballUrl) return callback(new CloudronError(CloudronError.BAD_STATE, 'No automatic update available')); - - if (boxUpdateInfo.upgrade && config.provider() !== 'caas') return callback(new CloudronError(CloudronError.SELF_UPGRADE_NOT_SUPPORTED)); - - update(boxUpdateInfo, auditSource, callback); -} - -function doUpdate(boxUpdateInfo, callback) { - assert(boxUpdateInfo && typeof boxUpdateInfo === 'object'); - - function updateError(e) { - progress.set(progress.UPDATE, -1, e.message); - callback(e); - } - - progress.set(progress.UPDATE, 5, 'Backing up for update'); - - backups.backupBoxAndApps({ userId: null, username: 'updater' }, function (error) { - if (error) return updateError(error); - - // NOTE: this data is opaque and will be passed through the installer.sh - var data= { - provider: config.provider(), - apiServerOrigin: config.apiServerOrigin(), - webServerOrigin: config.webServerOrigin(), - adminDomain: config.adminDomain(), - adminFqdn: config.adminFqdn(), - adminLocation: config.adminLocation(), - isDemo: config.isDemo(), - - appstore: { - apiServerOrigin: config.apiServerOrigin() - }, - caas: { - apiServerOrigin: config.apiServerOrigin(), - webServerOrigin: config.webServerOrigin() - }, - - version: boxUpdateInfo.version - }; - - debug('updating box %s %j', boxUpdateInfo.sourceTarballUrl, _.omit(data, 'tlsCert', 'tlsKey', 'token', 'appstore', 'caas')); - - progress.set(progress.UPDATE, 5, 'Downloading and installing new version'); - - shell.sudo('update', [ UPDATE_CMD, boxUpdateInfo.sourceTarballUrl, JSON.stringify(data) ], function (error) { - if (error) return updateError(error); - - // Do not add any code here. The installer script will stop the box code any instant - }); - }); -} - function checkDiskSpace(callback) { callback = callback || NOOP_CALLBACK; diff --git a/src/cron.js b/src/cron.js index 35c55077c..f64aeb69a 100644 --- a/src/cron.js +++ b/src/cron.js @@ -22,6 +22,7 @@ var apps = require('./apps.js'), reverseProxy = require('./reverseproxy.js'), scheduler = require('./scheduler.js'), settings = require('./settings.js'), + updater = require('./updater.js'), updateChecker = require('./updatechecker.js'); var gJobs = { @@ -207,7 +208,7 @@ function boxAutoupdatePatternChanged(pattern) { var updateInfo = updateChecker.getUpdateInfo(); if (updateInfo.box) { debug('Starting autoupdate to %j', updateInfo.box); - cloudron.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK); + updater.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK); } else { debug('No box auto updates available'); } diff --git a/src/routes/cloudron.js b/src/routes/cloudron.js index 58b295e90..2049c1864 100644 --- a/src/routes/cloudron.js +++ b/src/routes/cloudron.js @@ -22,7 +22,9 @@ var appstore = require('../appstore.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, progress = require('../progress.js'), + updater = require('../updater.js'), updateChecker = require('../updatechecker.js'), + UpdaterError = require('../updater.js').UpdaterError, _ = require('underscore'); function auditSource(req) { @@ -58,10 +60,10 @@ function getDisks(req, res, next) { function update(req, res, next) { // this only initiates the update, progress can be checked via the progress route - cloudron.updateToLatest(auditSource(req), function (error) { - if (error && error.reason === CloudronError.ALREADY_UPTODATE) return next(new HttpError(422, error.message)); - if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message)); - if (error && error.reason === CloudronError.SELF_UPGRADE_NOT_SUPPORTED) return next(new HttpError(412, error.message)); + updater.updateToLatest(auditSource(req), function (error) { + if (error && error.reason === UpdaterError.ALREADY_UPTODATE) return next(new HttpError(422, error.message)); + if (error && error.reason === UpdaterError.BAD_STATE) return next(new HttpError(409, error.message)); + if (error && error.reason === UpdaterError.SELF_UPGRADE_NOT_SUPPORTED) return next(new HttpError(412, error.message)); if (error) return next(new HttpError(500, error)); next(new HttpSuccess(202, {})); diff --git a/src/routes/sysadmin.js b/src/routes/sysadmin.js index 7ab692404..4eb80939e 100644 --- a/src/routes/sysadmin.js +++ b/src/routes/sysadmin.js @@ -9,10 +9,11 @@ exports = module.exports = { var backups = require('../backups.js'), BackupsError = require('../backups.js').BackupsError, cloudron = require('../cloudron.js'), - CloudronError = require('../cloudron.js').CloudronError, debug = require('debug')('box:routes/sysadmin'), HttpError = require('connect-lastmile').HttpError, - HttpSuccess = require('connect-lastmile').HttpSuccess; + HttpSuccess = require('connect-lastmile').HttpSuccess, + updater = require('../updater.js'), + UpdaterError = require('../updater.js').UpdaterError; function backup(req, res, next) { debug('triggering backup'); @@ -33,10 +34,10 @@ function update(req, res, next) { // this only initiates the update, progress can be checked via the progress route var auditSource = { userId: null, username: 'sysadmin' }; - cloudron.updateToLatest(auditSource, function (error) { - if (error && error.reason === CloudronError.ALREADY_UPTODATE) return next(new HttpError(422, error.message)); - if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message)); - if (error && error.reason === CloudronError.SELF_UPGRADE_NOT_SUPPORTED) return next(new HttpError(412, error.message)); + updater.updateToLatest(auditSource, function (error) { + if (error && error.reason === UpdaterError.ALREADY_UPTODATE) return next(new HttpError(422, error.message)); + if (error && error.reason === UpdaterError.BAD_STATE) return next(new HttpError(409, error.message)); + if (error && error.reason === UpdaterError.SELF_UPGRADE_NOT_SUPPORTED) return next(new HttpError(412, error.message)); if (error) return next(new HttpError(500, error)); next(new HttpSuccess(202, {})); diff --git a/src/updater.js b/src/updater.js new file mode 100644 index 000000000..49bd36df6 --- /dev/null +++ b/src/updater.js @@ -0,0 +1,145 @@ +'use strict'; + +exports = module.exports = { + updateToLatest: updateToLatest, + + UpdaterError: UpdaterError +}; + +var assert = require('assert'), + backups = require('./backups.js'), + caas = require('./caas.js'), + config = require('./config.js'), + debug = require('debug')('box:updater'), + eventlog = require('./eventlog.js'), + locker = require('./locker.js'), + path = require('path'), + progress = require('./progress.js'), + shell = require('./shell.js'), + updateChecker = require('./updatechecker.js'), + util = require('util'), + _ = require('underscore'); + +const UPDATE_CMD = path.join(__dirname, 'scripts/update.sh'); + +function UpdaterError(reason, errorOrMessage) { + assert.strictEqual(typeof reason, 'string'); + assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined'); + + Error.call(this); + Error.captureStackTrace(this, this.constructor); + + this.name = this.constructor.name; + this.reason = reason; + if (typeof errorOrMessage === 'undefined') { + this.message = reason; + } else if (typeof errorOrMessage === 'string') { + this.message = errorOrMessage; + } else { + this.message = 'Internal error'; + this.nestedError = errorOrMessage; + } +} +util.inherits(UpdaterError, Error); +UpdaterError.INTERNAL_ERROR = 'Internal Error'; +UpdaterError.EXTERNAL_ERROR = 'External Error'; +UpdaterError.BAD_STATE = 'Bad state'; +UpdaterError.ALREADY_UPTODATE = 'No Update Available'; +UpdaterError.NOT_FOUND = 'Not found'; +UpdaterError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported'; + +function update(boxUpdateInfo, auditSource, callback) { + assert.strictEqual(typeof boxUpdateInfo, 'object'); + assert.strictEqual(typeof auditSource, 'object'); + assert.strictEqual(typeof callback, 'function'); + + if (!boxUpdateInfo) return callback(null); + + var error = locker.lock(locker.OP_BOX_UPDATE); + if (error) return callback(new UpdaterError(UpdaterError.BAD_STATE, error.message)); + + eventlog.add(eventlog.ACTION_UPDATE, auditSource, { boxUpdateInfo: boxUpdateInfo }); + + // ensure tools can 'wait' on progress + progress.set(progress.UPDATE, 0, 'Starting'); + + // initiate the update/upgrade but do not wait for it + if (boxUpdateInfo.upgrade) { + debug('Starting upgrade'); + caas.upgrade(boxUpdateInfo, function (error) { + if (error) { + debug('Upgrade failed with error:', error); + locker.unlock(locker.OP_BOX_UPDATE); + } + }); + } else { + debug('Starting update'); + doUpdate(boxUpdateInfo, function (error) { + if (error) { + debug('Update failed with error:', error); + locker.unlock(locker.OP_BOX_UPDATE); + } + }); + } + + callback(null); +} + +function updateToLatest(auditSource, callback) { + assert.strictEqual(typeof auditSource, 'object'); + assert.strictEqual(typeof callback, 'function'); + + var boxUpdateInfo = updateChecker.getUpdateInfo().box; + if (!boxUpdateInfo) return callback(new UpdaterError(UpdaterError.ALREADY_UPTODATE, 'No update available')); + if (!boxUpdateInfo.sourceTarballUrl) return callback(new UpdaterError(UpdaterError.BAD_STATE, 'No automatic update available')); + + if (boxUpdateInfo.upgrade && config.provider() !== 'caas') return callback(new UpdaterError(UpdaterError.SELF_UPGRADE_NOT_SUPPORTED)); + + update(boxUpdateInfo, auditSource, callback); +} + +function doUpdate(boxUpdateInfo, callback) { + assert(boxUpdateInfo && typeof boxUpdateInfo === 'object'); + + function updateError(e) { + progress.set(progress.UPDATE, -1, e.message); + callback(e); + } + + progress.set(progress.UPDATE, 5, 'Backing up for update'); + + backups.backupBoxAndApps({ userId: null, username: 'updater' }, function (error) { + if (error) return updateError(error); + + // NOTE: this data is opaque and will be passed through the installer.sh + var data= { + provider: config.provider(), + apiServerOrigin: config.apiServerOrigin(), + webServerOrigin: config.webServerOrigin(), + adminDomain: config.adminDomain(), + adminFqdn: config.adminFqdn(), + adminLocation: config.adminLocation(), + isDemo: config.isDemo(), + + appstore: { + apiServerOrigin: config.apiServerOrigin() + }, + caas: { + apiServerOrigin: config.apiServerOrigin(), + webServerOrigin: config.webServerOrigin() + }, + + version: boxUpdateInfo.version + }; + + debug('updating box %s %j', boxUpdateInfo.sourceTarballUrl, _.omit(data, 'tlsCert', 'tlsKey', 'token', 'appstore', 'caas')); + + progress.set(progress.UPDATE, 5, 'Downloading and installing new version'); + + shell.sudo('update', [ UPDATE_CMD, boxUpdateInfo.sourceTarballUrl, JSON.stringify(data) ], function (error) { + if (error) return updateError(error); + + // Do not add any code here. The installer script will stop the box code any instant + }); + }); +}