services: add recoveryMode

This commit is contained in:
Girish Ramakrishnan
2021-10-01 12:09:13 -07:00
parent 54731392ff
commit 6a3cec3de8
5 changed files with 71 additions and 36 deletions
+56 -29
View File
@@ -384,6 +384,7 @@ async function configureService(id, data, auditSource) {
assert.strictEqual(typeof auditSource, 'object');
const [name, instance ] = id.split(':');
let needsRebuild = false;
if (instance) {
if (!APP_SERVICES[name]) throw new BoxError(BoxError.NOT_FOUND, 'Service not found');
@@ -392,21 +393,27 @@ async function configureService(id, data, auditSource) {
if (!app) throw new BoxError(BoxError.NOT_FOUND, 'App not found');
const servicesConfig = app.servicesConfig;
needsRebuild = servicesConfig[name]?.recoveryMode != data.recoveryMode;
servicesConfig[name] = data;
await apps.update(instance, { servicesConfig });
await applyServiceConfig(id, data);
} else if (SERVICES[name]) {
const servicesConfig = await settings.getServicesConfig();
needsRebuild = servicesConfig[name]?.recoveryMode != data.recoveryMode; // intentional != since 'debug' may or may not be there
servicesConfig[name] = data;
await settings.setServicesConfig(servicesConfig);
await applyServiceConfig(id, data);
} else {
throw new BoxError(BoxError.NOT_FOUND, 'No such service');
}
// do this in background
if (needsRebuild) {
safe(rebuildService(id, auditSource), { debug });
} else {
safe(applyMemoryLimit(id), { debug });
}
await eventlog.add(eventlog.ACTION_SERVICE_CONFIGURE, auditSource, { id, data });
}
@@ -516,8 +523,9 @@ async function rebuildService(id, auditSource) {
// nothing to rebuild for now.
}
// TODO: missing redis container is not created
safe(applyMemoryLimit(id), { debug }); // do this in background. ok to fail
// TODO: missing redis container is not created
await eventlog.add(eventlog.ACTION_SERVICE_REBUILD, auditSource, { id });
}
@@ -744,12 +752,12 @@ async function exportDatabase(addon) {
await shell.promises.sudo(`exportDatabase - removeAddonDir${addon}`, [ RMADDONDIR_CMD, addon ], {}); // ready to start afresh
}
async function applyServiceConfig(id, serviceConfig) {
async function applyMemoryLimit(id) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof serviceConfig, 'object');
const [name, instance] = id.split(':');
let containerName, memoryLimit;
const serviceConfig = await getServiceConfig(id);
if (instance) {
if (!APP_SERVICES[name]) throw new BoxError(BoxError.NOT_FOUND, 'Service not found');
@@ -807,10 +815,8 @@ async function startServices(existingInfra) {
}
// we always start db containers with unlimited memory. we then scale them down per configuration
const servicesConfig = await settings.getServicesConfig();
for (const id of [ 'mysql', 'postgresql', 'mongodb' ]) {
const serviceConfig = servicesConfig[id] || {};
safe(applyServiceConfig(id, serviceConfig), { debug }); // no waiting. and it's ok if applying service configs fails
safe(applyMemoryLimit(id), { debug }); // no waiting. and it's ok if applying service configs fails
}
}
@@ -911,8 +917,11 @@ async function startTurn(existingInfra) {
const turnSecret = await blobs.get(blobs.ADDON_TURN_SECRET);
if (!turnSecret) throw new BoxError(BoxError.ADDONS_ERROR, 'Turn secret is missing');
const readOnly = !serviceConfig.recoveryMode ? '--read-only' : '';
const cmd = serviceConfig.recoveryMode ? '/bin/bash -c \'echo "Debug mode. Sleeping" && sleep infinity\'' : '';
// this exports 3478/tcp, 5349/tls and 50000-51000/udp. note that this runs on the host network!
const cmd = `docker run --restart=always -d --name="turn" \
const runCmd = `docker run --restart=always -d --name="turn" \
--hostname turn \
--net host \
--log-driver syslog \
@@ -926,11 +935,11 @@ async function startTurn(existingInfra) {
-e CLOUDRON_TURN_SECRET="${turnSecret}" \
-e CLOUDRON_REALM="${realm}" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
${readOnly} -v /tmp -v /run "${tag}" ${cmd}`;
await shell.promises.exec('stopTurn', 'docker stop turn || true');
await shell.promises.exec('removeTurn', 'docker rm -f turn || true');
await shell.promises.exec('startTurn', cmd);
await shell.promises.exec('startTurn', runCmd);
}
async function teardownTurn(app, options) {
@@ -1107,8 +1116,12 @@ async function startMysql(existingInfra) {
await exportDatabase('mysql');
}
const serviceConfig = await getServiceConfig('mysql');
const readOnly = !serviceConfig.recoveryMode ? '--read-only' : '';
const cmd = serviceConfig.recoveryMode ? '/bin/bash -c \'echo "Debug mode. Sleeping" && sleep infinity\'' : '';
// memory options are applied dynamically. import requires all the memory we can get
const cmd = `docker run --restart=always -d --name="mysql" \
const runCmd = `docker run --restart=always -d --name="mysql" \
--hostname mysql \
--net cloudron \
--net-alias mysql \
@@ -1124,11 +1137,11 @@ async function startMysql(existingInfra) {
-v "${dataDir}/mysql:/var/lib/mysql" \
--label isCloudronManaged=true \
--cap-add SYS_NICE \
--read-only -v /tmp -v /run "${tag}"`;
${readOnly} -v /tmp -v /run "${tag}" ${cmd}`;
await shell.promises.exec('stopMysql', 'docker stop mysql || true');
await shell.promises.exec('removeMysql', 'docker rm -f mysql || true');
await shell.promises.exec('startMysql', cmd);
await shell.promises.exec('startMysql', runCmd);
await waitForContainer('mysql', 'CLOUDRON_MYSQL_TOKEN');
@@ -1308,8 +1321,12 @@ async function startPostgresql(existingInfra) {
await exportDatabase('postgresql');
}
const serviceConfig = await getServiceConfig('postgresql');
const readOnly = !serviceConfig.recoveryMode ? '--read-only' : '';
const cmd = serviceConfig.recoveryMode ? '/bin/bash -c \'echo "Debug mode. Sleeping" && sleep infinity\'' : '';
// memory options are applied dynamically. import requires all the memory we can get
const cmd = `docker run --restart=always -d --name="postgresql" \
const runCmd = `docker run --restart=always -d --name="postgresql" \
--hostname postgresql \
--net cloudron \
--net-alias postgresql \
@@ -1324,11 +1341,11 @@ async function startPostgresql(existingInfra) {
-e CLOUDRON_POSTGRESQL_TOKEN="${cloudronToken}" \
-v "${dataDir}/postgresql:/var/lib/postgresql" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
${readOnly} -v /tmp -v /run "${tag}" ${cmd}`;
await shell.promises.exec('stopPostgresql', 'docker stop postgresql || true');
await shell.promises.exec('removePostgresql', 'docker rm -f postgresql || true');
await shell.promises.exec('startPostgresql', cmd);
await shell.promises.exec('startPostgresql', runCmd);
await waitForContainer('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN');
if (upgrading) await importDatabase('postgresql');
@@ -1466,8 +1483,12 @@ async function startMongodb(existingInfra) {
await exportDatabase('mongodb');
}
const serviceConfig = await getServiceConfig('mongodb');
const readOnly = !serviceConfig.recoveryMode ? '--read-only' : '';
const cmd = serviceConfig.recoveryMode ? '/bin/bash -c \'echo "Debug mode. Sleeping" && sleep infinity\'' : '';
// memory options are applied dynamically. import requires all the memory we can get
const cmd = `docker run --restart=always -d --name="mongodb" \
const runCmd = `docker run --restart=always -d --name="mongodb" \
--hostname mongodb \
--net cloudron \
--net-alias mongodb \
@@ -1481,11 +1502,11 @@ async function startMongodb(existingInfra) {
-e CLOUDRON_MONGODB_TOKEN="${cloudronToken}" \
-v "${dataDir}/mongodb:/var/lib/mongodb" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
${readOnly} -v /tmp -v /run "${tag}" ${cmd}`;
await shell.promises.exec('stopMongodb', 'docker stop mongodb || true');
await shell.promises.exec('removeMongodb', 'docker rm -f mongodb || true');
await shell.promises.exec('startMongodb', cmd);
await shell.promises.exec('startMongodb', runCmd);
await waitForContainer('mongodb', 'CLOUDRON_MONGODB_TOKEN');
if (upgrading) await importDatabase('mongodb');
@@ -1628,7 +1649,10 @@ async function startGraphite(existingInfra) {
if (upgrading) debug('startGraphite: graphite will be upgraded');
const cmd = `docker run --restart=always -d --name="graphite" \
const readOnly = !serviceConfig.recoveryMode ? '--read-only' : '';
const cmd = serviceConfig.recoveryMode ? '/bin/bash -c \'echo "Debug mode. Sleeping" && sleep infinity\'' : '';
const runCmd = `docker run --restart=always -d --name="graphite" \
--hostname graphite \
--net cloudron \
--net-alias graphite \
@@ -1645,12 +1669,12 @@ async function startGraphite(existingInfra) {
-p 127.0.0.1:8417:8000 \
-v "${paths.PLATFORM_DATA_DIR}/graphite:/var/lib/graphite" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
${readOnly} -v /tmp -v /run "${tag}" ${cmd}`;
await shell.promises.exec('stopGraphite', 'docker stop graphite || true');
await shell.promises.exec('removeGraphite', 'docker rm -f graphite || true');
if (upgrading) await shell.promises.sudo('removeGraphiteDir', [ RMADDONDIR_CMD, 'graphite' ], {});
await shell.promises.exec('startGraphite', cmd);
await shell.promises.exec('startGraphite', runCmd);
// restart collectd to get the disk stats after graphite starts. currently, there is no way to do graphite health check
setTimeout(async () => await safe(shell.promises.sudo('restartcollectd', [ RESTART_SERVICE_CMD, 'collectd' ], {})), 60000);
@@ -1697,7 +1721,6 @@ async function startRedis(existingInfra) {
await setupRedis(app, app.manifest.addons.redis); // starts the container
}
if (upgrading) await importDatabase('redis');
}
@@ -1714,13 +1737,17 @@ async function setupRedis(app, options) {
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'].memoryLimit : APP_SERVICES['redis'].defaultMemoryLimit;
const memoryLimit = app.servicesConfig['redis']?.memoryLimit || APP_SERVICES['redis'].defaultMemoryLimit;
const memory = system.getMemoryAllocation(memoryLimit);
const recoveryMode = app.servicesConfig['redis']?.recoveryMode || false;
const readOnly = !recoveryMode ? '--read-only' : '';
const cmd = recoveryMode ? '/bin/bash -c \'echo "Debug mode. Sleeping" && sleep infinity\'' : '';
const tag = infra.images.redis.tag;
const label = app.fqdn;
// note that we do not add appId label because this interferes with the stop/start app logic
const cmd = `docker run --restart=always -d --name=${redisName} \
const runCmd = `docker run --restart=always -d --name=${redisName} \
--hostname ${redisName} \
--label=location=${label} \
--net cloudron \
@@ -1737,7 +1764,7 @@ async function setupRedis(app, options) {
-e CLOUDRON_REDIS_TOKEN="${redisServiceToken}" \
-v "${paths.PLATFORM_DATA_DIR}/redis/${app.id}:/var/lib/redis" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run ${tag}`;
${readOnly} -v /tmp -v /run ${tag} ${cmd}`;
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
@@ -1750,7 +1777,7 @@ async function setupRedis(app, options) {
const [inspectError, result] = await safe(docker.inspect(redisName));
if (inspectError) {
await shell.promises.exec('startRedis', cmd);
await shell.promises.exec('startRedis', runCmd);
} else { // fast path
debug(`Re-using existing redis container with state: ${JSON.stringify(result.State)}`);
}