diff --git a/src/apptask.js b/src/apptask.js index dfeffcabb..e15f1ac08 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -74,13 +74,13 @@ function updateApp(app, values, callback) { debugApp(app, 'updating app with values: %j', values); appdb.update(app.id, values, function (error) { - if (error) return callback(error); + if (error) return callback(new BoxError(BoxError.INTERNAL_ERROR, error)); for (var value in values) { app[value] = values[value]; } - return callback(null); + callback(null); }); } @@ -88,16 +88,16 @@ function reserveHttpPort(app, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof callback, 'function'); - var server = net.createServer(); + let server = net.createServer(); server.listen(0, function () { - var port = server.address().port; - updateApp(app, { httpPort: port }, function (error) { - if (error) { - server.close(); - return callback(error); - } + let port = server.address().port; - server.close(callback); + updateApp(app, { httpPort: port }, function (error) { + server.close(function (/* closeError */) { + if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Failed to allocate http port ${port}: ${error.message}`)); + + callback(null); + }); }); }); } @@ -106,14 +106,22 @@ function configureReverseProxy(app, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof callback, 'function'); - reverseProxy.configureApp(app, { userId: null, username: 'apptask' }, callback); + reverseProxy.configureApp(app, { userId: null, username: 'apptask' }, function (error) { + if (error) return callback(new BoxError(BoxError.REVERSEPROXY_ERROR, `Error configuring nginx: ${error.message}`)); + + callback(null); + }); } function unconfigureReverseProxy(app, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof callback, 'function'); - reverseProxy.unconfigureApp(app, callback); + reverseProxy.unconfigureApp(app, function (error) { + if (error) return callback(new BoxError(BoxError.REVERSEPROXY_ERROR, `Error unconfiguring nginx: ${error.message}`)); + + callback(null); + }); } function createContainer(app, callback) { @@ -124,7 +132,7 @@ function createContainer(app, callback) { debugApp(app, 'creating container'); docker.createContainer(app, function (error, container) { - if (error) return callback(new Error('Error creating container: ' + error)); + if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, `Error creating container: ${error.message}`)); updateApp(app, { containerId: container.id }, callback); }); @@ -138,7 +146,7 @@ function deleteContainers(app, options, callback) { debugApp(app, 'deleting app containers (app, scheduler)'); docker.deleteContainers(app.id, options, function (error) { - if (error) return callback(new Error('Error deleting container: ' + error)); + if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, `Error deleting container: ${error.message}`)); updateApp(app, { containerId: null }, callback); }); @@ -148,7 +156,12 @@ function createAppDir(app, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof callback, 'function'); - mkdirp(path.join(paths.APPS_DATA_DIR, app.id), callback); + const appDir = path.join(paths.APPS_DATA_DIR, app.id); + mkdirp(appDir, function (error) { + if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error creating directory: ${error.message}`, { appDir })); + + callback(null); + }); } function deleteAppDir(app, options, callback) { @@ -166,7 +179,7 @@ function deleteAppDir(app, options, callback) { if (safe.fs.existsSync(resolvedAppDataDir)) { const entries = safe.fs.readdirSync(resolvedAppDataDir); - if (!entries) return callback(`Error listing ${resolvedAppDataDir}: ${safe.error.message}`); + 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 @@ -179,9 +192,13 @@ function deleteAppDir(app, options, callback) { // if this fails, it's probably because the localstorage/redis addons have not cleaned up properly if (options.removeDirectory) { if (stat.isSymbolicLink()) { - if (!safe.fs.unlinkSync(appDataDir)) return callback(safe.error.code === 'ENOENT' ? null : safe.error); + 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 { - if (!safe.fs.rmdirSync(appDataDir)) return callback(safe.error.code === 'ENOENT' ? null : safe.error); + if (!safe.fs.rmdirSync(appDataDir)) { + if (safe.error.code !== 'ENOENT') return callback(new BoxError(BoxError.FS_ERROR, `Error removing dir ${appDataDir} : ${safe.error.message}`)); + } } } @@ -194,8 +211,13 @@ function addCollectdProfile(app, callback) { var collectdConf = ejs.render(COLLECTD_CONFIG_EJS, { appId: app.id, containerId: app.containerId, appDataDir: apps.getDataDir(app, app.dataDir) }); fs.writeFile(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), collectdConf, function (error) { - if (error) return callback(error); - shell.sudo('addCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'add', app.id ], {}, callback); + if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error writing collectd config: ${error.message}`)); + + shell.sudo('addCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'add', app.id ], {}, function (error) { + if (error) return callback(new BoxError(BoxError.COLLECTD_ERROR, 'Culd not add collectd config')); + + callback(null); + }); }); } @@ -205,7 +227,11 @@ function removeCollectdProfile(app, callback) { fs.unlink(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), function (error) { if (error && error.code !== 'ENOENT') debugApp(app, 'Error removing collectd profile', error); - shell.sudo('removeCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'remove', app.id ], {}, callback); + shell.sudo('removeCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'remove', app.id ], {}, function (error) { + if (error) return callback(new BoxError(BoxError.COLLECTD_ERROR, 'Culd not remove collectd config')); + + callback(null); + }); }); } @@ -214,17 +240,22 @@ function addLogrotateConfig(app, callback) { assert.strictEqual(typeof callback, 'function'); docker.inspect(app.containerId, function (error, result) { - if (error) return callback(error); + if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, `Error inspecting app container: ${error.message}`, { containerId: app.containerId })); var runVolume = result.Mounts.find(function (mount) { return mount.Destination === '/run'; }); - if (!runVolume) return callback(new Error('App does not have /run mounted')); + if (!runVolume) return callback(new BoxError(BoxError.DOCKER_ERROR, 'App does not have /run mounted')); // 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) { - if (error) return callback(error); - shell.sudo('addLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'add', app.id, tmpFilePath ], {}, callback); + 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); + }); }); }); } @@ -233,7 +264,23 @@ function removeLogrotateConfig(app, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof callback, 'function'); - shell.sudo('removeLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'remove', app.id ], {}, callback); + 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) { @@ -266,16 +313,32 @@ function downloadIcon(app, callback) { .buffer(true) .timeout(30 * 1000) .end(function (error, res) { - if (error && !error.response) return retryCallback(new Error('Network error downloading icon : ' + error.message)); + 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 - if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, app.id + '.png'), res.body)) return retryCallback(new Error('Error saving icon:' + safe.error.message)); + 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); }); }, callback); } +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) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof overwrite, 'boolean'); @@ -348,33 +411,6 @@ function unregisterSubdomains(app, allDomains, callback) { }); } -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 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 waitForDnsPropagation(app, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof callback, 'function'); @@ -385,14 +421,18 @@ function waitForDnsPropagation(app, callback) { } sysinfo.getPublicIp(function (error, ip) { - if (error) return callback(error); + if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Error getting public IP: ${error.message}`)); domains.waitForDnsRecord(app.location, app.domain, 'A', ip, { interval: 5000, times: 240 }, function (error) { - if (error) return callback(error); + 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 })); // now wait for alternateDomains, if any async.eachSeries(app.alternateDomains, function (domain, iteratorCallback) { - domains.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { interval: 5000, times: 240 }, iteratorCallback); + domains.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { interval: 5000, times: 240 }, function (error) { + 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); }); }); @@ -408,7 +448,11 @@ function migrateDataDir(app, sourceDir, callback) { debug(`migrateDataDir: migrating data from ${resolvedSourceDir} to ${resolvedTargetDir}`); - shell.sudo('migrateDataDir', [ MV_VOLUME_CMD, resolvedSourceDir, resolvedTargetDir ], {}, callback); + shell.sudo('migrateDataDir', [ MV_VOLUME_CMD, resolvedSourceDir, resolvedTargetDir ], {}, function (error) { + if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Error migrating data directory: ${error.message}`)); + + callback(null); + }); } function downloadImage(manifest, callback) { @@ -416,14 +460,18 @@ function downloadImage(manifest, callback) { assert.strictEqual(typeof callback, 'function'); docker.info(function (error, info) { - if (error) return callback(error); + if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, `Error getting docker info: ${error.message}`)); const dfAsync = util.callbackify(df.file); dfAsync(info.DockerRootDir, function (error, diskUsage) { - if (error) return callback(error); - if (diskUsage.available < (1024*1024*1024)) return callback(new Error('Not enough disk space to pull docker image. See https://cloudron.io/documentation/storage/#docker-image-location')); + 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 })); - docker.downloadImage(manifest, callback); + docker.downloadImage(manifest, function (error) { + if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, `Error downloading image: ${error.message}`, { image: manifest.dockerImage })); + + callback(null); + }); }); }); } @@ -703,7 +751,7 @@ function update(app, updateConfig, progressCallback, callback) { const newUdpPorts = updateConfig.manifest.udpPorts || {}; async.each(Object.keys(currentPorts), function (portName, callback) { - if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(); // port still in use + if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(null); // port still in use appdb.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP, function (error) { if (error && error.reason === DatabaseError.NOT_FOUND) console.error('Portbinding does not exist in database.'); @@ -712,7 +760,7 @@ function update(app, updateConfig, progressCallback, callback) { // also delete from app object for further processing (the db is updated in the next step) delete app.portBindings[portName]; - callback(); + callback(null); }); }, next); }, @@ -746,7 +794,7 @@ function update(app, updateConfig, progressCallback, callback) { debugApp(app, 'Error updating app: %s', error); updateApp(app, { installationState: apps.ISTATE_ERROR, error: error.toPlainObject ? error.toPlainObject() : error.message, updateTime: new Date() }, callback.bind(null, error)); } else { - if (updateConfig.skipNotification) return callback(); + if (updateConfig.skipNotification) return callback(null); eventlog.add(eventlog.ACTION_APP_UPDATE_FINISH, auditsource.APP_TASK, { app: app, success: true }, callback); } diff --git a/src/boxerror.js b/src/boxerror.js index 5df46af86..f76561919 100644 --- a/src/boxerror.js +++ b/src/boxerror.js @@ -33,10 +33,18 @@ util.inherits(BoxError, Error); BoxError.ACCESS_DENIED = 'Access Denied'; BoxError.ALREADY_EXISTS = 'Already Exists'; BoxError.BAD_FIELD = 'Bad Field'; +BoxError.COLLECTD_ERROR = 'Collectd Error'; BoxError.CONFLICT = 'Conflict'; +BoxError.DATABASE_ERROR = 'Database Error'; +BoxError.DNS_ERROR = 'DNS Error'; +BoxError.DOCKER_ERROR = 'Docker Error'; BoxError.EXTERNAL_ERROR = 'External Error'; +BoxError.FS_ERROR = 'FileSystem Error'; BoxError.INTERNAL_ERROR = 'Internal Error'; +BoxError.LOGROTATE_ERROR = 'Logrotate Error'; +BoxError.NETWORK_ERROR = 'Network Error'; BoxError.NOT_FOUND = 'Not found'; +BoxError.REVERSEPROXY_ERROR = 'ReverseProxy Error'; BoxError.prototype.toPlainObject = function () { return _.extend({}, { message: this.message, reason: this.reason }, this.details);