Make addons code remove a BoxError

This commit is contained in:
Girish Ramakrishnan
2019-12-04 13:17:58 -08:00
parent c992853cca
commit 53e39f571c
8 changed files with 154 additions and 177 deletions
+71 -83
View File
@@ -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}` });
+17 -30
View File
@@ -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);
});
});
}
+1
View File
@@ -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';
+52 -55
View File
@@ -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);
+7 -5
View File
@@ -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) {
+1 -1
View File
@@ -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();
});
+3 -1
View File
@@ -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';
+2 -2
View File
@@ -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({