Fixes to service configuration

restart service does not rebuild automatically, we should add a route
for that. we need to figure where to scale services etc if we randomly
create containers like that.
This commit is contained in:
Girish Ramakrishnan
2021-01-21 12:53:38 -08:00
parent 47a598a494
commit 9f9575f46a
11 changed files with 200 additions and 190 deletions
+129 -141
View File
@@ -4,8 +4,9 @@ exports = module.exports = {
getServiceIds,
getServiceStatus,
getServiceConfig,
configureService,
getServiceLogs,
configureService,
restartService,
rebuildService,
@@ -13,7 +14,6 @@ exports = module.exports = {
stopAppServices,
startServices,
updateServiceConfig,
setupAddons,
teardownAddons,
@@ -42,7 +42,7 @@ var appdb = require('./appdb.js'),
debug = require('debug')('box:addons'),
docker = require('./docker.js'),
fs = require('fs'),
graphs = require('./graphs.js'),
graphite = require('./graphite.js'),
hat = require('./hat.js'),
infra = require('./infra_version.js'),
mail = require('./mail.js'),
@@ -173,27 +173,27 @@ var ADDONS = {
const SERVICES = {
turn: {
status: statusTurn,
restart: restartContainer.bind(null, 'turn'),
restart: docker.restartContainer.bind(null, 'turn'),
defaultMemoryLimit: 256 * 1024 * 1024
},
mail: {
status: containerStatus.bind(null, 'mail', 'CLOUDRON_MAIL_TOKEN'),
restart: mail.restartMail,
defaultMemoryLimit: Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256) * 1024 * 1024
defaultMemoryLimit: mail.DEFAULT_MEMORY_LIMIT
},
mongodb: {
status: containerStatus.bind(null, 'mongodb', 'CLOUDRON_MONGODB_TOKEN'),
restart: restartContainer.bind(null, 'mongodb'),
restart: docker.restartContainer.bind(null, 'mongodb'),
defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024
},
mysql: {
status: containerStatus.bind(null, 'mysql', 'CLOUDRON_MYSQL_TOKEN'),
restart: restartContainer.bind(null, 'mysql'),
restart: docker.restartContainer.bind(null, 'mysql'),
defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024
},
postgresql: {
status: containerStatus.bind(null, 'postgresql', 'CLOUDRON_POSTGRESQL_TOKEN'),
restart: restartContainer.bind(null, 'postgresql'),
restart: docker.restartContainer.bind(null, 'postgresql'),
defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024
},
docker: {
@@ -208,13 +208,13 @@ const SERVICES = {
},
sftp: {
status: statusSftp,
restart: restartContainer.bind(null, 'sftp'),
defaultMemoryLimit: 256 * 1024 * 1024
restart: docker.restartContainer.bind(null, 'sftp'),
defaultMemoryLimit: sftp.DEFAULT_MEMORY_LIMIT
},
graphite: {
status: statusGraphite,
restart: restartContainer.bind(null, 'graphite'),
defaultMemoryLimit: 75 * 1024 * 1024
restart: docker.restartContainer.bind(null, 'graphite'),
defaultMemoryLimit: graphite.DEFAULT_MEMORY_LIMIT
},
nginx: {
status: statusNginx,
@@ -228,7 +228,7 @@ const APP_SERVICES = {
status: (instance, done) => containerStatus(`redis-${instance}`, 'CLOUDRON_REDIS_TOKEN', done),
start: (instance, done) => docker.startContainer(`redis-${instance}`, done),
stop: (instance, done) => docker.stopContainer(`redis-${instance}`, done),
restart: (instance, done) => restartContainer(`redis-${instance}`, done),
restart: (instance, done) => docker.restartContainer(`redis-${instance}`, done),
defaultMemoryLimit: 150 * 1024 * 1024
}
};
@@ -263,38 +263,6 @@ function dumpPath(addon, appId) {
}
}
function rebuildService(serviceName, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert.strictEqual(typeof callback, 'function');
// 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 === 'turn') return startTurn({ version: 'none' }, callback);
if (serviceName === 'mongodb') return startMongodb({ version: 'none' }, callback);
if (serviceName === 'postgresql') return startPostgresql({ version: 'none' }, callback);
if (serviceName === 'mysql') return startMysql({ version: 'none' }, callback);
if (serviceName === 'sftp') return sftp.startSftp({ version: 'none' }, callback);
if (serviceName === 'graphite') return graphs.startGraphite({ version: 'none' }, callback);
// nothing to rebuild for now
callback();
}
function restartContainer(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
docker.restartContainer(name, function (error) {
if (error && error.reason === BoxError.NOT_FOUND) {
callback(null); // callback early since rebuilding takes long
return rebuildService(name, function (error) { if (error) debug(`restartContainer: Unable to rebuild service ${name}`, error); });
}
if (error) return callback(error);
callback(error);
});
}
function getContainerDetails(containerName, tokenEnvName, callback) {
assert.strictEqual(typeof containerName, 'string');
assert.strictEqual(typeof tokenEnvName, 'string');
@@ -367,12 +335,12 @@ function getServiceConfig(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
const [name, instance ] = id.split(':');
const [name, instance] = id.split(':');
if (!instance) {
settings.getServicesConfig(function (error, servicesConfig) {
if (error) return callback(error);
callback(null, SERVICES[name], servicesConfig);
callback(null, servicesConfig[name] || {});
});
return;
@@ -381,7 +349,7 @@ function getServiceConfig(id, callback) {
appdb.get(instance, function (error, app) {
if (error) return callback(error);
callback(null, APP_SERVICES[name], app.servicesConfig);
callback(null, app.servicesConfig[name] || {});
});
}
@@ -390,13 +358,15 @@ function getServiceStatus(id, callback) {
assert.strictEqual(typeof callback, 'function');
const [name, instance ] = id.split(':');
let containerStatusFunc;
let containerStatusFunc, service;
if (instance) {
if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND));
containerStatusFunc = APP_SERVICES[name].status.bind(null, instance);
service = APP_SERVICES[name];
if (!service) return callback(new BoxError(BoxError.NOT_FOUND));
containerStatusFunc = service.status.bind(null, instance);
} else if (SERVICES[name]) {
containerStatusFunc = SERVICES[name].status;
service = SERVICES[name];
containerStatusFunc = service.status;
} else {
return callback(new BoxError(BoxError.NOT_FOUND));
}
@@ -420,14 +390,13 @@ function getServiceStatus(id, callback) {
tmp.error = result.error || null;
tmp.healthcheck = result.healthcheck || null;
getServiceConfig(id, function (error, service, servicesConfig) {
getServiceConfig(id, function (error, serviceConfig) {
if (error) return callback(error);
const serviceConfig = servicesConfig[name];
tmp.config = Object.assign({}, serviceConfig);
tmp.config = serviceConfig;
if (!tmp.config.memoryLimit && service.defaultMemoryLimit) {
tmp.config.memoryLimit = service.defaultMemoryLimit * 2;
tmp.config.memoryLimit = service.defaultMemoryLimit;
}
callback(null, tmp);
@@ -444,38 +413,34 @@ function configureService(id, data, callback) {
if (instance) {
if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND));
} else if (!SERVICES[name]) {
return callback(new BoxError(BoxError.NOT_FOUND));
}
getServiceConfig(id, function (error, service, servicesConfig) {
if (error) return callback(error);
apps.get(instance, function (error, app) {
if (error) return callback(error);
if (!servicesConfig[name]) servicesConfig[name] = {};
// if not specified we clear the entry and use defaults
if (!data.memoryLimit) {
delete servicesConfig[name].memoryLimit;
} else {
const servicesConfig = app.servicesConfig;
servicesConfig[name] = data;
}
if (instance) {
appdb.update(instance, { servicesConfig }, function (error) {
if (error) return callback(error);
updateAppServiceConfig(name, instance, servicesConfig, callback);
applyServiceConfig(id, data, callback);
});
} else {
});
} else if (SERVICES[name]) {
settings.getServicesConfig(function (error, servicesConfig) {
if (error) return callback(error);
servicesConfig[name] = data;
settings.setServicesConfig(servicesConfig, function (error) {
if (error) return callback(error);
updateServiceConfig(servicesConfig, NOOP_CALLBACK); // this can take a while
callback(null);
applyServiceConfig(id, data, callback);
});
}
});
});
} else {
return callback(new BoxError(BoxError.NOT_FOUND));
}
}
function getServiceLogs(id, options, callback) {
@@ -556,6 +521,29 @@ function getServiceLogs(id, options, callback) {
callback(null, transformStream);
}
function rebuildService(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
// 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
getServiceConfig(id, function (error, serviceConfig) {
if (error) return callback(error);
if (id === 'turn') return startTurn({ version: 'none' }, serviceConfig, callback);
if (id === 'mongodb') return startMongodb({ version: 'none' }, callback);
if (id === 'postgresql') return startPostgresql({ version: 'none' }, callback);
if (id === 'mysql') return startMysql({ version: 'none' }, callback);
if (id === 'sftp') return sftp.start({ version: 'none' }, serviceConfig, callback);
if (id === 'graphite') return graphite.start({ version: 'none' }, serviceConfig, callback);
// nothing to rebuild for now.
// TODO: mongo/postgresql/mysql need to be scaled down.
// TODO: missing redis container is not created
callback();
});
}
function restartService(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -565,9 +553,9 @@ function restartService(id, callback) {
if (instance) {
if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND));
APP_SERVICES[name].restart(instance, callback);
return APP_SERVICES[name].restart(instance, callback);
} else if (SERVICES[name]) {
SERVICES[name].restart(callback);
return SERVICES[name].restart(callback);
} else {
return callback(new BoxError(BoxError.NOT_FOUND));
}
@@ -804,80 +792,77 @@ function exportDatabase(addon, callback) {
});
}
function updateServiceConfig(platformConfig, callback) {
assert.strictEqual(typeof platformConfig, 'object');
function applyServiceConfig(id, serviceConfig, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof serviceConfig, 'object');
assert.strictEqual(typeof callback, 'function');
async.eachSeries([ 'mysql', 'postgresql', 'mail', 'mongodb', 'graphite' ], function iterator(serviceName, iteratorCallback) {
const containerConfig = platformConfig[serviceName];
let memory, memorySwap;
if (containerConfig && containerConfig.memoryLimit) {
memory = system.getMemoryAllocation(containerConfig.memoryLimit);
memorySwap = containerConfig.memoryLimit;
} else {
memory = SERVICES[serviceName].defaultMemoryLimit;
memorySwap = memory * 2;
}
const [name, instance] = id.split(':');
let containerName, memoryLimit;
docker.update(serviceName, memory, memorySwap, iteratorCallback);
}, callback);
}
if (instance) {
if (!APP_SERVICES[name]) return callback(new BoxError(BoxError.NOT_FOUND));
function updateAppServiceConfig(name, instance, servicesConfig, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof instance, 'string');
assert.strictEqual(typeof servicesConfig, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`updateAppServiceConfig: ${name}-${instance} ${JSON.stringify(servicesConfig)}`);
const serviceConfig = servicesConfig[name];
let memory, memorySwap;
if (serviceConfig && serviceConfig.memoryLimit) {
memory = system.getMemoryAllocation(serviceConfig.memoryLimit);
memorySwap = serviceConfig.memoryLimit;
containerName = `${name}-${instance}`;
memoryLimit = serviceConfig && serviceConfig.memoryLimit ? serviceConfig.memoryLimit : APP_SERVICES[name].defaultMemoryLimit;
} else if (SERVICES[name]) {
containerName = name;
memoryLimit = serviceConfig && serviceConfig.memoryLimit ? serviceConfig.memoryLimit : SERVICES[name].defaultMemoryLimit;
} else {
memory = APP_SERVICES[name].defaultMemoryLimit;
memorySwap = memory * 2;
return callback(new BoxError(BoxError.NOT_FOUND));
}
docker.update(`${name}-${instance}`, memory, memorySwap, callback);
debug(`updateServiceConfig: ${containerName} ${JSON.stringify(serviceConfig)}`);
const memory = system.getMemoryAllocation(memoryLimit);
docker.update(containerName, memory, memoryLimit, callback);
}
function startServices(existingInfra, callback) {
assert.strictEqual(typeof existingInfra, 'object');
assert.strictEqual(typeof callback, 'function');
let startFuncs = [ ];
settings.getServicesConfig(function (error, servicesConfig) {
if (error) return callback(error);
// always start addons on any infra change, regardless of minor or major update
if (existingInfra.version !== infra.version) {
debug(`startServices: ${existingInfra.version} -> ${infra.version}. starting all services`);
startFuncs.push(
startTurn.bind(null, existingInfra),
startMysql.bind(null, existingInfra),
startPostgresql.bind(null, existingInfra),
startMongodb.bind(null, existingInfra),
startRedis.bind(null, existingInfra),
graphs.startGraphite.bind(null, existingInfra),
sftp.startSftp.bind(null, existingInfra),
mail.startMail);
} else {
assert.strictEqual(typeof existingInfra.images, 'object');
let startFuncs = [ ];
if (infra.images.turn.tag !== existingInfra.images.turn.tag) startFuncs.push(startTurn.bind(null, existingInfra));
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) startFuncs.push(startMysql.bind(null, existingInfra));
if (infra.images.postgresql.tag !== existingInfra.images.postgresql.tag) startFuncs.push(startPostgresql.bind(null, existingInfra));
if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) startFuncs.push(startMongodb.bind(null, existingInfra));
if (infra.images.mail.tag !== existingInfra.images.mail.tag) startFuncs.push(mail.startMail);
if (infra.images.redis.tag !== existingInfra.images.redis.tag) startFuncs.push(startRedis.bind(null, existingInfra));
if (infra.images.graphite.tag !== existingInfra.images.graphite.tag) startFuncs.push(graphs.startGraphite.bind(null, existingInfra));
if (infra.images.sftp.tag !== existingInfra.images.sftp.tag) startFuncs.push(sftp.startSftp.bind(null, existingInfra));
// always start addons on any infra change, regardless of minor or major update
if (existingInfra.version !== infra.version) {
debug(`startServices: ${existingInfra.version} -> ${infra.version}. starting all services`);
startFuncs.push(
startTurn.bind(null, existingInfra, servicesConfig['turn'] || {}),
startMysql.bind(null, existingInfra),
startPostgresql.bind(null, existingInfra),
startMongodb.bind(null, existingInfra),
startRedis.bind(null, existingInfra),
graphite.start.bind(null, existingInfra, servicesConfig['graphite'] | {}),
sftp.start.bind(null, existingInfra, servicesConfig['sftp'] | {}),
mail.startMail);
} else {
assert.strictEqual(typeof existingInfra.images, 'object');
debug('startServices: existing infra. incremental service create %j', startFuncs.map(function (f) { return f.name; }));
}
if (infra.images.turn.tag !== existingInfra.images.turn.tag) startFuncs.push(startTurn.bind(null, existingInfra, servicesConfig['turn'] || {}));
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) startFuncs.push(startMysql.bind(null, existingInfra));
if (infra.images.postgresql.tag !== existingInfra.images.postgresql.tag) startFuncs.push(startPostgresql.bind(null, existingInfra));
if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) startFuncs.push(startMongodb.bind(null, existingInfra));
if (infra.images.mail.tag !== existingInfra.images.mail.tag) startFuncs.push(mail.startMail);
if (infra.images.redis.tag !== existingInfra.images.redis.tag) startFuncs.push(startRedis.bind(null, existingInfra));
if (infra.images.graphite.tag !== existingInfra.images.graphite.tag) startFuncs.push(graphite.start.bind(null, existingInfra, servicesConfig['graphite'] | {}));
if (infra.images.sftp.tag !== existingInfra.images.sftp.tag) startFuncs.push(sftp.start.bind(null, existingInfra, servicesConfig['sftp'] || {}));
async.series(startFuncs, callback);
debug('startServices: existing infra. incremental service create %j', startFuncs.map(function (f) { return f.name; }));
}
// we always start db containers with unlimited memory. we then scale them down per configuration
let updateFuncs = [
applyServiceConfig.bind(null, 'mysql', servicesConfig['mysql'] || {}),
applyServiceConfig.bind(null, 'postgresql', servicesConfig['postgresql'] || {}),
applyServiceConfig.bind(null, 'mongodb', servicesConfig['mongodb'] || {}),
];
async.series(startFuncs.concat(updateFuncs), callback);
});
}
function getEnvironment(app, callback) {
@@ -1572,8 +1557,9 @@ function restorePostgreSql(app, options, callback) {
});
}
function startTurn(existingInfra, callback) {
function startTurn(existingInfra, serviceConfig, callback) {
assert.strictEqual(typeof existingInfra, 'object');
assert.strictEqual(typeof serviceConfig, 'object');
assert.strictEqual(typeof callback, 'function');
// get and ensure we have a turn secret
@@ -1584,7 +1570,8 @@ function startTurn(existingInfra, callback) {
}
const tag = infra.images.turn.tag;
const memoryLimit = 256;
const memoryLimit = serviceConfig.memoryLimit || SERVICES['turn'].defaultMemoryLimit;
const memory = system.getMemoryAllocation(memoryLimit);
const realm = settings.adminFqdn();
// this exports 3478/tcp, 5349/tls and 50000-51000/udp. note that this runs on the host network!
@@ -1595,8 +1582,8 @@ function startTurn(existingInfra, callback) {
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=turn \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
-m ${memory} \
--memory-swap ${memoryLimit} \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_TURN_SECRET="${turnSecret}" \
@@ -1872,7 +1859,8 @@ function setupRedis(app, options, callback) {
const redisServiceToken = hat(4 * 48);
// Compute redis memory limit based on app's memory limit (this is arbitrary)
const memoryLimit = app.servicesConfig['redis'] ? app.servicesConfig['redis'].memory : APP_SERVICES['redis'].defaultMemoryLimit;
const memoryLimit = app.servicesConfig['redis'] ? app.servicesConfig['redis'].memoryLimit : APP_SERVICES['redis'].defaultMemoryLimit;
const memory = system.getMemoryAllocation(memoryLimit);
const tag = infra.images.redis.tag;
const label = app.fqdn;
@@ -1886,7 +1874,7 @@ function setupRedis(app, options, callback) {
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag="${redisName}" \
-m ${memoryLimit/2} \
-m ${memory} \
--memory-swap ${memoryLimit} \
--dns 172.18.0.1 \
--dns-search=. \