diff --git a/src/addons.js b/src/addons.js index 943371e0d..4df163d5b 100644 --- a/src/addons.js +++ b/src/addons.js @@ -40,7 +40,6 @@ var accesscontrol = require('./accesscontrol.js'), crypto = require('crypto'), debug = require('debug')('box:addons'), docker = require('./docker.js'), - dockerConnection = docker.connection, fs = require('fs'), graphs = require('./graphs.js'), hat = require('./hat.js'), @@ -232,33 +231,10 @@ function dumpPath(addon, appId) { } } -function restartContainer(serviceName, callback) { - assert.strictEqual(typeof serviceName, 'string'); - assert.strictEqual(typeof callback, 'function'); - - assert(KNOWN_SERVICES[serviceName], `Unknown service ${serviceName}`); - - docker.stopContainer(serviceName, function (error) { - if (error) return callback(error); - - docker.startContainer(serviceName, function (error) { - if (error && error.reason === BoxError.NOT_FOUND) { - callback(null); // callback early since rebuilding takes long - return rebuildService(serviceName, function (error) { if (error) console.error(`Unable to rebuild service ${serviceName}`, error); }); - } - if (error) return callback(error); - - callback(null); - }); - }); -} - function rebuildService(serviceName, callback) { assert.strictEqual(typeof serviceName, 'string'); assert.strictEqual(typeof callback, 'function'); - assert(KNOWN_SERVICES[serviceName], `Unknown service ${serviceName}`); - // this attempts to recreate the service docker container if they don't exist but platform infra version is unchanged // passing an infra version of 'none' will not attempt to purge existing data, not sure if this is good or bad if (serviceName === 'mongodb') return startMongodb({ version: 'none' }, callback); @@ -271,6 +247,24 @@ function rebuildService(serviceName, callback) { callback(); } +function restartContainer(serviceName, callback) { + assert.strictEqual(typeof serviceName, 'string'); + assert.strictEqual(typeof callback, 'function'); + + docker.stopContainer(serviceName, function (error) { + if (error) return callback(error); + + docker.startContainer(serviceName, function (error) { + if (error && error.reason === BoxError.NOT_FOUND) { + callback(null); // callback early since rebuilding takes long + return rebuildService(serviceName, function (error) { if (error) console.error(`Unable to rebuild service ${serviceName}`, error); }); + } + + callback(error); + }); + }); +} + function getServiceDetails(containerName, tokenEnvName, callback) { assert.strictEqual(typeof containerName, 'string'); assert.strictEqual(typeof tokenEnvName, 'string'); @@ -483,8 +477,8 @@ function waitForService(containerName, tokenEnvName, callback) { async.retry({ times: 10, interval: 15000 }, function (retryCallback) { request.get(`https://${result.ip}:3000/healthcheck?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { - if (error) return retryCallback(new Error(`Error waiting for ${containerName}: ${error.message}`)); - if (response.statusCode !== 200 || !response.body.status) return retryCallback(new Error(`Error waiting for ${containerName}. Status code: ${response.statusCode} message: ${response.body.message}`)); + if (error) return retryCallback(new BoxError(BoxError.ADDONS_ERROR, `Network error waiting for ${containerName}: ${error.message}`)); + if (response.statusCode !== 200 || !response.body.status) return retryCallback(new BoxError(BoxError.ADDONS_ERROR, `Error waiting for ${containerName}. Status code: ${response.statusCode} message: ${response.body.message}`)); retryCallback(null); }); @@ -502,7 +496,7 @@ function setupAddons(app, addons, callback) { debugApp(app, 'setupAddons: Setting up %j', Object.keys(addons)); async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) { - if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon)); + if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`)); debugApp(app, 'Setting up addon %s with options %j', addon, addons[addon]); @@ -520,7 +514,7 @@ function teardownAddons(app, addons, callback) { debugApp(app, 'teardownAddons: Tearing down %j', Object.keys(addons)); async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) { - if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon)); + if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`)); debugApp(app, 'Tearing down addon %s with options %j', addon, addons[addon]); @@ -540,7 +534,7 @@ function backupAddons(app, addons, callback) { debugApp(app, 'backupAddons: Backing up %j', Object.keys(addons)); async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) { - if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon)); + if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`)); KNOWN_ADDONS[addon].backup(app, addons[addon], iteratorCallback); }, callback); @@ -558,7 +552,7 @@ function clearAddons(app, addons, callback) { debugApp(app, 'clearAddons: clearing %j', Object.keys(addons)); async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) { - if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon)); + if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`)); KNOWN_ADDONS[addon].clear(app, addons[addon], iteratorCallback); }, callback); @@ -576,7 +570,7 @@ function restoreAddons(app, addons, callback) { debugApp(app, 'restoreAddons: restoring %j', Object.keys(addons)); async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) { - if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon)); + if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`)); KNOWN_ADDONS[addon].restore(app, addons[addon], iteratorCallback); }, callback); @@ -587,7 +581,7 @@ function importAppDatabase(app, addon, callback) { assert.strictEqual(typeof addon, 'string'); assert.strictEqual(typeof callback, 'function'); - if (!(addon in KNOWN_ADDONS)) return callback(new Error(`No such addon: ${addon}`)); + if (!(addon in KNOWN_ADDONS)) return callback(new BoxError(BoxError.NOT_FOUND, `No such addon: ${addon}`)); async.series([ KNOWN_ADDONS[addon].setup.bind(null, app, app.manifest.addons[addon]), @@ -1052,8 +1046,8 @@ function setupMySql(app, options, callback) { if (error) return callback(error); request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) { - if (error) return callback(new Error('Error setting up mysql: ' + error)); - if (response.statusCode !== 201) return callback(new Error(`Error setting up mysql. Status code: ${response.statusCode} message: ${response.body.message}`)); + if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error setting up mysql: ${error.message}`)); + if (response.statusCode !== 201) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error setting up mysql. Status code: ${response.statusCode} message: ${response.body.message}`)); const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_'; @@ -1090,9 +1084,10 @@ function clearMySql(app, options, callback) { getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) { if (error) return callback(error); - request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/clear?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) { - if (error) return callback(new Error('Error clearing mysql: ' + error)); - if (response.statusCode !== 200) return callback(new Error(`Error clearing mysql. Status code: ${response.statusCode} message: ${response.body.message}`)); + request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { + if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error clearing mysql: ${error.message}`)); + if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error clearing mysql. Status code: ${response.statusCode} message: ${response.body.message}`)); + callback(); }); }); @@ -1109,9 +1104,9 @@ function teardownMySql(app, options, callback) { getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) { if (error) return callback(error); - request.delete(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}?access_token=${result.token}&username=${username}`, { rejectUnauthorized: false }, function (error, response) { - if (error) return callback(new Error('Error clearing mysql: ' + error)); - if (response.statusCode !== 200) return callback(new Error(`Error clearing mysql. Status code: ${response.statusCode} message: ${response.body.message}`)); + request.delete(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) { + if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mysql: ${error.message}`)); + if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mysql. Status code: ${response.statusCode} message: ${response.body.message}`)); appdb.unsetAddonConfig(app.id, 'mysql', callback); }); @@ -1137,7 +1132,7 @@ function pipeRequestToFile(url, filename, callback) { const req = request.post(url, { rejectUnauthorized: false }); req.on('error', done); // network error, dns error, request errored in middle etc req.on('response', function (response) { - if (response.statusCode !== 200) return done(new Error(`Unexpected response code: ${response.statusCode} message: ${response.statusMessage} filename: ${filename}`)); + if (response.statusCode !== 200) return done(new BoxError(BoxError.ADDONS_ERROR, `Unexpected response code when piping ${url}: ${response.statusCode} message: ${response.statusMessage} filename: ${filename}`)); response.pipe(writeStream).on('finish', done); // this is hit after data written to disk }); @@ -1178,9 +1173,9 @@ function restoreMySql(app, options, callback) { var input = fs.createReadStream(dumpPath('mysql', app.id)); input.on('error', callback); - const restoreReq = request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/restore?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) { - if (error) return callback(error); - if (response.statusCode !== 200) return callback(new Error(`Unexpected response from mysql addon ${response.statusCode} message: ${response.body.message}`)); + const restoreReq = request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/restore?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { + if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring mysql: ${error.message}`)); + if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring mysql. Status code: ${response.statusCode} message: ${response.body.message}`)); callback(null); }); @@ -1265,8 +1260,8 @@ function setupPostgreSql(app, options, callback) { if (error) return callback(error); request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) { - if (error) return callback(new Error('Error setting up postgresql: ' + error)); - if (response.statusCode !== 201) return callback(new Error(`Error setting up postgresql. Status code: ${response.statusCode} message: ${response.body.message}`)); + if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error setting up postgresql: ${error.message}`)); + if (response.statusCode !== 201) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error setting up postgresql. Status code: ${response.statusCode} message: ${response.body.message}`)); const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_'; @@ -1298,9 +1293,9 @@ function clearPostgreSql(app, options, callback) { getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) { if (error) return callback(error); - request.post(`https://${result.ip}:3000/databases/${database}/clear?access_token=${result.token}&username=${username}`, { rejectUnauthorized: false }, function (error, response) { - if (error) return callback(new Error('Error clearing postgresql: ' + error)); - if (response.statusCode !== 200) return callback(new Error(`Error clearing postgresql. Status code: ${response.statusCode} message: ${response.body.message}`)); + request.post(`https://${result.ip}:3000/databases/${database}/clear?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) { + if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error clearing postgresql: ${error.message}`)); + if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error clearing postgresql. Status code: ${response.statusCode} message: ${response.body.message}`)); callback(null); }); @@ -1317,9 +1312,9 @@ function teardownPostgreSql(app, options, callback) { getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) { if (error) return callback(error); - request.delete(`https://${result.ip}:3000/databases/${database}?access_token=${result.token}&username=${username}`, { rejectUnauthorized: false }, function (error, response) { - if (error) return callback(new Error('Error tearing down postgresql: ' + error)); - if (response.statusCode !== 200) return callback(new Error(`Error tearing down postgresql. Status code: ${response.statusCode} message: ${response.body.message}`)); + request.delete(`https://${result.ip}:3000/databases/${database}?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) { + if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error tearing down postgresql: ${error.message}`)); + if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down postgresql. Status code: ${response.statusCode} message: ${response.body.message}`)); appdb.unsetAddonConfig(app.id, 'postgresql', callback); }); @@ -1360,9 +1355,9 @@ function restorePostgreSql(app, options, callback) { var input = fs.createReadStream(dumpPath('postgresql', app.id)); input.on('error', callback); - const restoreReq = request.post(`https://${result.ip}:3000/databases/${database}/restore?access_token=${result.token}&username=${username}`, { rejectUnauthorized: false }, function (error, response) { - if (error) return callback(error); - if (response.statusCode !== 200) return callback(new Error(`Unexpected response from postgresql addon ${response.statusCode} message: ${response.body.message}`)); + const restoreReq = request.post(`https://${result.ip}:3000/databases/${database}/restore?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) { + if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring postgresql: ${error.message}`)); + if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring postgresql. Status code: ${response.statusCode} message: ${response.body.message}`)); callback(null); }); @@ -1441,8 +1436,8 @@ function setupMongoDb(app, options, callback) { if (error) return callback(error); request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) { - if (error) return callback(new Error('Error setting up mongodb: ' + error)); - if (response.statusCode !== 201) return callback(new Error(`Error setting up mongodb. Status code: ${response.statusCode}`)); + if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error setting up mongodb: ${error.message}`)); + if (response.statusCode !== 201) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error setting up mongodb. Status code: ${response.statusCode} message: ${response.body.message}`)); const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_'; @@ -1476,9 +1471,9 @@ function clearMongodb(app, options, callback) { getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) { if (error) return callback(error); - request.post(`https://${result.ip}:3000/databases/${app.id}/clear?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) { - if (error) return callback(new Error('Error clearing mongodb: ' + error)); - if (response.statusCode !== 200) return callback(new Error(`Error clearing mongodb. Status code: ${response.statusCode} message: ${response.body.message}`)); + request.post(`https://${result.ip}:3000/databases/${app.id}/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { + if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error clearing mongodb: ${error.message}`)); + if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error clearing mongodb. Status code: ${response.statusCode} message: ${response.body.message}`)); callback(); }); @@ -1495,9 +1490,9 @@ function teardownMongoDb(app, options, callback) { getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) { if (error) return callback(error); - request.delete(`https://${result.ip}:3000/databases/${app.id}?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) { - if (error) return callback(new Error('Error tearing down mongodb: ' + error)); - if (response.statusCode !== 200) return callback(new Error(`Error tearing down mongodb. Status code: ${response.statusCode} message: ${response.body.message}`)); + request.delete(`https://${result.ip}:3000/databases/${app.id}?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { + if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mongodb: ${error.message}`)); + if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mongodb. Status code: ${response.statusCode} message: ${response.body.message}`)); appdb.unsetAddonConfig(app.id, 'mongodb', callback); }); @@ -1534,9 +1529,9 @@ function restoreMongoDb(app, options, callback) { const readStream = fs.createReadStream(dumpPath('mongodb', app.id)); readStream.on('error', callback); - const restoreReq = request.post(`https://${result.ip}:3000/databases/${app.id}/restore?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) { - if (error) return callback(error); - if (response.statusCode !== 200) return callback(new Error(`Unexpected response from mongodb addon ${response.statusCode} message: ${response.body.message}`)); + const restoreReq = request.post(`https://${result.ip}:3000/databases/${app.id}/restore?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { + if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring mongodb: ${error.message}`)); + if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring mongodb. Status code: ${response.statusCode} message: ${response.body.message}`)); callback(null); }); @@ -1654,9 +1649,9 @@ function clearRedis(app, options, callback) { getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) { if (error) return callback(error); - request.post(`https://${result.ip}:3000/clear?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) { - if (error) return callback(new Error('Error clearing redis: ' + error)); - if (response.statusCode !== 200) return callback(new Error(`Error clearing redis. Status code: ${response.statusCode} message: ${response.body.message}`)); + request.post(`https://${result.ip}:3000/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { + if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error clearing redis: ${error.message}`)); + if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error clearing redis. Status code: ${response.statusCode} message: ${response.body.message}`)); callback(null); }); @@ -1668,18 +1663,11 @@ function teardownRedis(app, options, callback) { assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); - var container = dockerConnection.getContainer('redis-' + app.id); - - var removeOptions = { - force: true, // kill container if it's running - v: true // removes volumes associated with the container - }; - - container.remove(removeOptions, function (error) { - if (error && error.statusCode !== 404) return callback(new Error('Error removing container:' + error)); + docker.deleteContainer(`redis-${app.id}`, function (error) { + if (error) return callback(error); shell.sudo('removeVolume', [ RMADDONDIR_CMD, 'redis', app.id ], {}, function (error) { - if (error) return callback(new Error('Error removing redis data:' + error)); + if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error removing redis data: ${error.message}`)); rimraf(path.join(paths.LOG_DIR, `redis-${app.id}`), function (error) { if (error) debugApp(app, 'cannot cleanup logs: %s', error); @@ -1724,9 +1712,9 @@ function restoreRedis(app, options, callback) { } input.on('error', callback); - const restoreReq = request.post(`https://${result.ip}:3000/restore?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) { - if (error) return callback(error); - if (response.statusCode !== 200) return callback(new Error(`Unexpected response from redis addon: ${response.statusCode} message: ${response.body.message}`)); + const restoreReq = request.post(`https://${result.ip}:3000/restore?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { + if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring redis: ${error.message}`)); + if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error restoring redis. Status code: ${response.statusCode} message: ${response.body.message}`)); callback(null); }); @@ -1811,7 +1799,7 @@ function statusGraphite(callback) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED }); if (error) return callback(error); - request.get('http://127.0.0.1:8417/graphite-web/dashboard', { timeout: 3000 }, function (error, response) { + request.get('http://127.0.0.1:8417/graphite-web/dashboard', { json: true, timeout: 3000 }, function (error, response) { if (error) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for graphite: ${error.message}` }); if (response.statusCode !== 200) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for graphite. Status code: ${response.statusCode} message: ${response.body.message}` }); diff --git a/src/apps.js b/src/apps.js index 38e65afae..53d1ead7f 100644 --- a/src/apps.js +++ b/src/apps.js @@ -1693,8 +1693,6 @@ function exec(appId, options, callback) { return callback(new BoxError(BoxError.BAD_STATE, 'App not installed or running')); } - var container = docker.connection.getContainer(app.containerId); - var execOptions = { AttachStdin: true, AttachStdout: true, @@ -1707,37 +1705,26 @@ function exec(appId, options, callback) { Cmd: cmd }; - container.exec(execOptions, function (error, exec) { + var startOptions = { + Detach: false, + Tty: options.tty, + // hijacking upgrades the docker connection from http to tcp. because of this upgrade, + // we can work with half-close connections (not defined in http). this way, the client + // can properly signal that stdin is EOF by closing it's side of the socket. In http, + // the whole connection will be dropped when stdin get EOF. + // https://github.com/apocas/dockerode/commit/b4ae8a03707fad5de893f302e4972c1e758592fe + hijack: true, + stream: true, + stdin: true, + stdout: true, + stderr: true + }; + + docker.execContainer(app.containerId, { execOptions, startOptions, rows: options.rows, columns: options.columns }, function (error, stream) { if (error && error.statusCode === 409) return callback(new BoxError(BoxError.BAD_STATE, error.message)); // container restarting/not running if (error) return callback(error); - var startOptions = { - Detach: false, - Tty: options.tty, - // hijacking upgrades the docker connection from http to tcp. because of this upgrade, - // we can work with half-close connections (not defined in http). this way, the client - // can properly signal that stdin is EOF by closing it's side of the socket. In http, - // the whole connection will be dropped when stdin get EOF. - // https://github.com/apocas/dockerode/commit/b4ae8a03707fad5de893f302e4972c1e758592fe - hijack: true, - stream: true, - stdin: true, - stdout: true, - stderr: true - }; - exec.start(startOptions, function(error, stream /* in hijacked mode, this is a net.socket */) { - if (error) return callback(error); - - if (options.rows && options.columns) { - // there is a race where resizing too early results in a 404 "no such exec" - // https://git.cloudron.io/cloudron/box/issues/549 - setTimeout(function () { - exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); }); - }, 2000); - } - - return callback(null, stream); - }); + callback(null, stream); }); }); } diff --git a/src/boxerror.js b/src/boxerror.js index 3b589bd16..2b4e754f4 100644 --- a/src/boxerror.js +++ b/src/boxerror.js @@ -33,6 +33,7 @@ function BoxError(reason, errorOrMessage, details) { } util.inherits(BoxError, Error); BoxError.ACCESS_DENIED = 'Access Denied'; +BoxError.ADDONS_ERROR = 'Addons Error'; BoxError.ALREADY_EXISTS = 'Already Exists'; BoxError.BAD_FIELD = 'Bad Field'; BoxError.BAD_STATE = 'Bad State'; diff --git a/src/docker.js b/src/docker.js index 4c255870c..3c112a572 100644 --- a/src/docker.js +++ b/src/docker.js @@ -1,8 +1,6 @@ 'use strict'; exports = module.exports = { - connection: connectionInstance(), - testRegistryConfig: testRegistryConfig, setRegistryConfig: setRegistryConfig, injectPrivateFields: injectPrivateFields, @@ -27,6 +25,7 @@ exports = module.exports = { getContainerIdByIp: getContainerIdByIp, inspect: inspect, inspectByName: inspect, + execContainer: execContainer, getEvents: getEvents, memoryUsage: memoryUsage, createVolume: createVolume, @@ -34,20 +33,14 @@ exports = module.exports = { clearVolume: clearVolume }; -// timeout is optional -function connectionInstance(timeout) { - var Docker = require('dockerode'); - var docker = new Docker({ socketPath: '/var/run/docker.sock', timeout: timeout }); - return docker; -} - var addons = require('./addons.js'), async = require('async'), assert = require('assert'), BoxError = require('./boxerror.js'), child_process = require('child_process'), constants = require('./constants.js'), - debug = require('debug')('box:docker.js'), + debug = require('debug')('box:gConnection.js'), + Docker = require('dockerode'), path = require('path'), settings = require('./settings.js'), shell = require('./shell.js'), @@ -58,6 +51,8 @@ var addons = require('./addons.js'), const CLEARVOLUME_CMD = path.join(__dirname, 'scripts/clearvolume.sh'), MKDIRVOLUME_CMD = path.join(__dirname, 'scripts/mkdirvolume.sh'); +const gConnection = new Docker({ socketPath: '/var/run/docker.sock' }); + function debugApp(app) { assert(typeof app === 'object'); @@ -68,8 +63,7 @@ function testRegistryConfig(auth, callback) { assert.strictEqual(typeof auth, 'object'); assert.strictEqual(typeof callback, 'function'); - let docker = exports.connection; - docker.checkAuth(auth, function (error /*, data */) { // this returns a 500 even for auth errors + gConnection.checkAuth(auth, function (error /*, data */) { // this returns a 500 even for auth errors if (error) return callback(new BoxError(BoxError.BAD_FIELD, error, { field: 'serverAddress' })); callback(); @@ -108,9 +102,9 @@ function ping(callback) { assert.strictEqual(typeof callback, 'function'); // do not let the request linger - var docker = connectionInstance(1000); + const connection = new Docker({ socketPath: '/var/run/gConnection.sock', timeout: 1000 }); - docker.ping(function (error, result) { + connection.ping(function (error, result) { if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); if (result !== 'OK') return callback(new BoxError(BoxError.DOCKER_ERROR, 'Unable to ping the docker daemon')); @@ -140,14 +134,12 @@ function getRegistryConfig(image, callback) { } function pullImage(manifest, callback) { - var docker = exports.connection; - getRegistryConfig(manifest.dockerImage, function (error, authConfig) { if (error) return callback(error); debug(`pullImage: will pull ${manifest.dockerImage}. auth: ${authConfig ? 'yes' : 'no'}`); - docker.pull(manifest.dockerImage, { authconfig: authConfig }, function (error, stream) { + gConnection.pull(manifest.dockerImage, { authconfig: authConfig }, function (error, stream) { if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Unable to pull image. Please check the network or if the image needs authentication. statusCode: ' + error.statusCode)); // https://github.com/dotcloud/docker/issues/1074 says each status message @@ -203,8 +195,7 @@ function createSubcontainer(app, name, cmd, options, callback) { assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); - var docker = exports.connection, - isAppContainer = !cmd; // non app-containers are like scheduler and exec (terminal) containers + let isAppContainer = !cmd; // non app-containers are like scheduler and exec (terminal) containers var manifest = app.manifest; var exposedPorts = {}, dockerPortBindings = { }; @@ -330,7 +321,7 @@ function createSubcontainer(app, name, cmd, options, callback) { debugApp(app, 'Creating container for %s', app.manifest.dockerImage); - docker.createContainer(containerOptions, function (error, container) { + gConnection.createContainer(containerOptions, function (error, container) { if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); callback(null, container); @@ -346,9 +337,7 @@ function startContainer(containerId, callback) { assert.strictEqual(typeof containerId, 'string'); assert.strictEqual(typeof callback, 'function'); - var docker = exports.connection; - - var container = docker.getContainer(containerId); + var container = gConnection.getContainer(containerId); debug('Starting container %s', containerId); container.start(function (error) { @@ -369,8 +358,7 @@ function stopContainer(containerId, callback) { return callback(); } - var docker = exports.connection; - var container = docker.getContainer(containerId); + var container = gConnection.getContainer(containerId); debug('Stopping container %s', containerId); var options = { @@ -400,8 +388,7 @@ function deleteContainer(containerId, callback) { if (containerId === null) return callback(null); - var docker = exports.connection; - var container = docker.getContainer(containerId); + var container = gConnection.getContainer(containerId); var removeOptions = { force: true, // kill container if it's running @@ -425,14 +412,12 @@ function deleteContainers(appId, options, callback) { assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); - var docker = exports.connection; - debug('deleting containers of %s', appId); let labels = [ 'appId=' + appId ]; if (options.managedOnly) labels.push('isCloudronManaged=true'); - docker.listContainers({ all: 1, filters: JSON.stringify({ label: labels }) }, function (error, containers) { + gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: labels }) }, function (error, containers) { if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); async.eachSeries(containers, function (container, iteratorDone) { @@ -445,11 +430,9 @@ function stopContainers(appId, callback) { assert.strictEqual(typeof appId, 'string'); assert.strictEqual(typeof callback, 'function'); - var docker = exports.connection; - debug('stopping containers of %s', appId); - docker.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) { + gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) { if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); async.eachSeries(containers, function (container, iteratorDone) { @@ -465,8 +448,6 @@ function deleteImage(manifest, callback) { var dockerImage = manifest ? manifest.dockerImage : null; if (!dockerImage) return callback(null); - var docker = exports.connection; - var removeOptions = { force: false, // might be shared with another instance of this app noprune: false // delete untagged parents @@ -475,7 +456,7 @@ function deleteImage(manifest, callback) { // registry v1 used to pull down all *tags*. this meant that deleting image by tag was not enough (since that // just removes the tag). we used to remove the image by id. this is not required anymore because aliases are // not created anymore after https://github.com/docker/docker/pull/10571 - docker.getImage(dockerImage).remove(removeOptions, function (error) { + gConnection.getImage(dockerImage).remove(removeOptions, function (error) { if (error && error.statusCode === 400) return callback(null); // invalid image format. this can happen if user installed with a bad --docker-image if (error && error.statusCode === 404) return callback(null); // not found if (error && error.statusCode === 409) return callback(null); // another container using the image @@ -493,9 +474,7 @@ function getContainerIdByIp(ip, callback) { assert.strictEqual(typeof ip, 'string'); assert.strictEqual(typeof callback, 'function'); - var docker = exports.connection; - - docker.getNetwork('cloudron').inspect(function (error, bridge) { + gConnection.getNetwork('cloudron').inspect(function (error, bridge) { if (error && error.statusCode === 404) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Unable to find the cloudron network')); if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); @@ -516,7 +495,7 @@ function inspect(containerId, callback) { assert.strictEqual(typeof containerId, 'string'); assert.strictEqual(typeof callback, 'function'); - var container = exports.connection.getContainer(containerId); + var container = gConnection.getContainer(containerId); container.inspect(function (error, result) { if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND)); @@ -526,13 +505,38 @@ function inspect(containerId, callback) { }); } +function execContainer(containerId, options, callback) { + assert.strictEqual(typeof containerId, 'string'); + assert.strictEqual(typeof options, 'object'); + assert.strictEqual(typeof callback, 'function'); + + var container = gConnection.getContainer(containerId); + + container.exec(options.execOptions, function (error, exec) { + if (error && error.statusCode === 409) return callback(new BoxError(BoxError.BAD_STATE, error.message)); // container restarting/not running + if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); + + exec.start(options.startOptions, function(error, stream /* in hijacked mode, this is a net.socket */) { + if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); + + if (options.rows && options.columns) { + // there is a race where resizing too early results in a 404 "no such exec" + // https://git.cloudron.io/cloudron/box/issues/549 + setTimeout(function () { + exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); }); + }, 2000); + } + + callback(null, stream); + }); + }); +} + function getEvents(options, callback) { assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); - let docker = exports.connection; - - docker.getEvents(options, function (error, stream) { + gConnection.getEvents(options, function (error, stream) { if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); callback(null, stream); @@ -543,7 +547,7 @@ function memoryUsage(containerId, callback) { assert.strictEqual(typeof containerId, 'string'); assert.strictEqual(typeof callback, 'function'); - var container = exports.connection.getContainer(containerId); + var container = gConnection.getContainer(containerId); container.stats({ stream: false }, function (error, result) { if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND)); @@ -559,8 +563,6 @@ function createVolume(app, name, volumeDataDir, callback) { assert.strictEqual(typeof volumeDataDir, 'string'); assert.strictEqual(typeof callback, 'function'); - let docker = exports.connection; - const volumeOptions = { Name: name, Driver: 'local', @@ -579,7 +581,7 @@ function createVolume(app, name, volumeDataDir, callback) { shell.sudo('createVolume', [ MKDIRVOLUME_CMD, volumeDataDir ], {}, function (error) { if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error creating app data dir: ${error.message}`)); - docker.createVolume(volumeOptions, function (error) { + gConnection.createVolume(volumeOptions, function (error) { if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); callback(); @@ -593,8 +595,7 @@ function clearVolume(app, name, options, callback) { assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); - let docker = exports.connection; - let volume = docker.getVolume(name); + let volume = gConnection.getVolume(name); volume.inspect(function (error, v) { if (error && error.statusCode === 404) return callback(); if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); @@ -614,9 +615,7 @@ function removeVolume(app, name, callback) { assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof callback, 'function'); - let docker = exports.connection; - - let volume = docker.getVolume(name); + let volume = gConnection.getVolume(name); volume.remove(function (error) { if (error && error.statusCode !== 404) return callback(new BoxError(BoxError.DOCKER_ERROR, `removeVolume: Error removing volume of ${app.id} ${error.message}`)); @@ -627,9 +626,7 @@ function removeVolume(app, name, callback) { function info(callback) { assert.strictEqual(typeof callback, 'function'); - let docker = exports.connection; - - docker.info(function (error, result) { + gConnection.info(function (error, result) { if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error connecting to docker')); callback(null, result); diff --git a/src/janitor.js b/src/janitor.js index 281e18dec..2c98d780c 100644 --- a/src/janitor.js +++ b/src/janitor.js @@ -5,7 +5,7 @@ var assert = require('assert'), authcodedb = require('./authcodedb.js'), BoxError = require('./boxerror.js'), debug = require('debug')('box:janitor'), - docker = require('./docker.js').connection, + Docker = require('dockerode'), tokendb = require('./tokendb.js'); exports = module.exports = { @@ -13,7 +13,9 @@ exports = module.exports = { cleanupDockerVolumes: cleanupDockerVolumes }; -var NOOP_CALLBACK = function () { }; +const NOOP_CALLBACK = function () { }; + +const gConnection = new Docker({ socketPath: '/var/run/docker.sock' }); function ignoreError(func) { return function (callback) { @@ -68,7 +70,7 @@ function cleanupTmpVolume(containerInfo, callback) { debug('cleanupTmpVolume %j', containerInfo.Names); - docker.getContainer(containerInfo.Id).exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true, Tty: false }, function (error, execContainer) { + gConnection.getContainer(containerInfo.Id).exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true, Tty: false }, function (error, execContainer) { if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, `Failed to exec container: ${error.message}`)); execContainer.start({ hijack: true }, function (error, stream) { @@ -77,7 +79,7 @@ function cleanupTmpVolume(containerInfo, callback) { stream.on('error', callback); stream.on('end', callback); - docker.modem.demuxStream(stream, process.stdout, process.stderr); + gConnection.modem.demuxStream(stream, process.stdout, process.stderr); }); }); } @@ -89,7 +91,7 @@ function cleanupDockerVolumes(callback) { debug('Cleaning up docker volumes'); - docker.listContainers({ all: 0 }, function (error, containers) { + gConnection.listContainers({ all: 0 }, function (error, containers) { if (error) return callback(error); async.eachSeries(containers, function (container, iteratorDone) { diff --git a/src/reverseproxy.js b/src/reverseproxy.js index 4cd52049d..4a6cb17d3 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -171,7 +171,7 @@ function reload(callback) { if (process.env.BOX_ENV === 'test') return callback(); shell.sudo('reload', [ RELOAD_NGINX_CMD ], {}, function (error) { - if (error) return callback(new BoxError(BoxError.NGINX_ERROR, error)); + if (error) return callback(new BoxError(BoxError.NGINX_ERROR, `Error reloading nginx: ${error.message}`)); callback(); }); diff --git a/src/routes/test/apps-test.js b/src/routes/test/apps-test.js index 09e7e96c0..45b544a9f 100644 --- a/src/routes/test/apps-test.js +++ b/src/routes/test/apps-test.js @@ -11,7 +11,7 @@ let apps = require('../../apps.js'), constants = require('../../constants.js'), crypto = require('crypto'), database = require('../../database.js'), - docker = require('../../docker.js').connection, + Docker = require('dockerode'), expect = require('expect.js'), fs = require('fs'), hat = require('../../hat.js'), @@ -33,6 +33,8 @@ let apps = require('../../apps.js'), var SERVER_URL = 'http://localhost:' + constants.PORT; +const docker = new Docker({ socketPath: '/var/run/docker.sock' }); + // Test image information var TEST_IMAGE_REPO = 'cloudron/test'; var TEST_IMAGE_TAG = '25.19.0'; diff --git a/src/updatechecker.js b/src/updatechecker.js index 66372f7b0..2b469442b 100644 --- a/src/updatechecker.js +++ b/src/updatechecker.js @@ -105,8 +105,8 @@ function checkAppUpdates(callback) { return iteratorDone(); } - const updateIsBlocked = apps.canAutoupdateApp(app, updateInfo.manifest); - if (autoupdatesEnabled && !updateIsBlocked) return iteratorDone(); + const canAutoupdateApp = apps.canAutoupdateApp(app, updateInfo.manifest); + if (autoupdatesEnabled && canAutoupdateApp) return iteratorDone(); debug('Notifying of app update for %s from %s to %s', app.id, app.manifest.version, updateInfo.manifest.version); notificationPending.push({