docker.js and services.js: async'ify

This commit is contained in:
Girish Ramakrishnan
2021-08-25 19:41:46 -07:00
parent 1cc11fece8
commit 42774eac8c
24 changed files with 1618 additions and 2130 deletions

View File

@@ -31,8 +31,8 @@ const apps = require('./apps.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
promiseRetry = require('./promise-retry.js'),
reverseProxy = require('./reverseproxy.js'),
rimraf = require('rimraf'),
safe = require('safetydance'),
services = require('./services.js'),
settings = require('./settings.js'),
@@ -64,54 +64,41 @@ function makeTaskError(error, app) {
}
// updates the app object and the database
function updateApp(app, values, callback) {
async function updateApp(app, values) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof values, 'object');
assert.strictEqual(typeof callback, 'function');
util.callbackify(apps.update)(app.id, values, function (error) {
if (error) return callback(error);
await apps.update(app.id, values);
for (var value in values) {
app[value] = values[value];
}
callback(null);
});
for (const value in values) {
app[value] = values[value];
}
}
function allocateContainerIp(app, callback) {
async function allocateContainerIp(app) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
async.retry({ times: 10 }, function (retryCallback) {
await promiseRetry({ times: 10, interval: 0}, async function () {
const iprange = iputils.intFromIp('172.18.20.255') - iputils.intFromIp('172.18.16.1');
let rnd = Math.floor(Math.random() * iprange);
const containerIp = iputils.ipFromInt(iputils.intFromIp('172.18.16.1') + rnd);
updateApp(app, { containerIp }, retryCallback);
}, callback);
updateApp(app, { containerIp });
});
}
function createContainer(app, callback) {
async function createContainer(app) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
assert(!app.containerId); // otherwise, it will trigger volumeFrom
debugApp(app, 'creating container');
docker.createContainer(app, function (error, container) {
if (error) return callback(error);
const container = await docker.createContainer(app);
updateApp(app, { containerId: container.id }, function (error) {
if (error) return callback(error);
await updateApp(app, { containerId: container.id });
// re-generate configs that rely on container id
async.series([
addLogrotateConfig.bind(null, app),
addCollectdProfile.bind(null, app)
], callback);
});
});
// re-generate configs that rely on container id
await addLogrotateConfig(app);
await addCollectdProfile(app);
}
function deleteContainers(app, options, callback) {
@@ -197,40 +184,30 @@ async function removeCollectdProfile(app) {
await collectd.removeProfile(app.id);
}
function addLogrotateConfig(app, callback) {
async function addLogrotateConfig(app) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
docker.inspect(app.containerId, function (error, result) {
if (error) return callback(error);
const result = await docker.inspect(app.containerId);
var runVolume = result.Mounts.find(function (mount) { return mount.Destination === '/run'; });
if (!runVolume) return callback(new BoxError(BoxError.DOCKER_ERROR, 'App does not have /run mounted'));
const runVolume = result.Mounts.find(function (mount) { return mount.Destination === '/run'; });
if (!runVolume) throw new BoxError(BoxError.DOCKER_ERROR, 'App does not have /run mounted');
// logrotate configs can have arbitrary commands, so the config files must be owned by root
var logrotateConf = ejs.render(LOGROTATE_CONFIG_EJS, { volumePath: runVolume.Source, appId: app.id });
var tmpFilePath = path.join(os.tmpdir(), app.id + '.logrotate');
fs.writeFile(tmpFilePath, logrotateConf, function (error) {
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error writing logrotate config: ${error.message}`));
// logrotate configs can have arbitrary commands, so the config files must be owned by root
const logrotateConf = ejs.render(LOGROTATE_CONFIG_EJS, { volumePath: runVolume.Source, appId: app.id });
const tmpFilePath = path.join(os.tmpdir(), app.id + '.logrotate');
shell.sudo('addLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'add', app.id, tmpFilePath ], {}, function (error) {
if (error) return callback(new BoxError(BoxError.LOGROTATE_ERROR, `Error adding logrotate config: ${error.message}`));
safe.fs.writeFileSync(tmpFilePath, logrotateConf);
if (safe.error) throw new BoxError(BoxError.FS_ERROR, `Error writing logrotate config: ${safe.error.message}`);
callback(null);
});
});
});
const [error] = await safe(shell.promises.sudo('addLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'add', app.id, tmpFilePath ], {}));
if (error) throw new BoxError(BoxError.LOGROTATE_ERROR, `Error adding logrotate config: ${error.message}`);
}
function removeLogrotateConfig(app, callback) {
async function removeLogrotateConfig(app) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
shell.sudo('removeLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'remove', app.id ], {}, function (error) {
if (error) return callback(new BoxError(BoxError.LOGROTATE_ERROR, `Error removing logrotate config: ${error.message}`));
callback(null);
});
const [error] = await safe(shell.promises.sudo('removeLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'remove', app.id ], {}));
if (error) throw new BoxError(BoxError.LOGROTATE_ERROR, `Error removing logrotate config: ${error.message}`);
}
function cleanupLogs(app, callback) {
@@ -238,7 +215,7 @@ function cleanupLogs(app, callback) {
assert.strictEqual(typeof callback, 'function');
// note that redis container logs are cleaned up by the addon
rimraf(path.join(paths.LOG_DIR, app.id), function (error) {
fs.rm(path.join(paths.LOG_DIR, app.id), { force: true, recursive: true }, function (error) {
if (error) debugApp(app, 'cannot cleanup logs:', error);
callback(null);
@@ -258,29 +235,27 @@ function verifyManifest(manifest, callback) {
callback(null);
}
function downloadIcon(app, callback) {
async function downloadIcon(app) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
// nothing to download if we dont have an appStoreId
if (!app.appStoreId) return callback(null);
if (!app.appStoreId) return;
debugApp(app, `Downloading icon of ${app.appStoreId}@${app.manifest.version}`);
const iconUrl = settings.apiServerOrigin() + '/api/v1/apps/' + app.appStoreId + '/versions/' + app.manifest.version + '/icon';
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
superagent
.get(iconUrl)
await promiseRetry({ times: 10, interval: 5000 }, async function () {
const [networkError, response] = await safe(superagent.get(iconUrl)
.buffer(true)
.timeout(30 * 1000)
.end(function (error, res) {
if (error && !error.response) return retryCallback(new BoxError(BoxError.NETWORK_ERROR, `Network error downloading icon : ${error.message}`));
if (res.statusCode !== 200) return retryCallback(null); // ignore error. this can also happen for apps installed with cloudron-cli
.ok(() => true));
updateApp(app, { appStoreIcon: res.body }, retryCallback);
});
}, callback);
if (networkError) throw new BoxError(BoxError.NETWORK_ERROR, `Network error downloading icon : ${networkError.message}`);
if (response.status !== 200) return; // ignore error. this can also happen for apps installed with cloudron-cli
await updateApp(app, { appStoreIcon: response.body });
});
}
function waitForDnsPropagation(app, callback) {
@@ -329,33 +304,24 @@ function moveDataDir(app, targetDir, callback) {
});
}
function downloadImage(manifest, callback) {
async function downloadImage(manifest) {
assert.strictEqual(typeof manifest, 'object');
assert.strictEqual(typeof callback, 'function');
docker.info(function (error, info) {
if (error) return callback(error);
const info = await docker.info();
const [dfError, diskUsage] = await safe(df.file(info.DockerRootDir));
if (dfError) throw new BoxError(BoxError.FS_ERROR, `Error getting file system info: ${dfError.message}`);
const dfAsync = util.callbackify(df.file);
dfAsync(info.DockerRootDir, function (error, diskUsage) {
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error getting file system info: ${error.message}`));
if (diskUsage.available < (1024*1024*1024)) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Not enough disk space to pull docker image', { diskUsage: diskUsage, dockerRootDir: info.DockerRootDir }));
if (diskUsage.available < (1024*1024*1024)) throw new BoxError(BoxError.DOCKER_ERROR, 'Not enough disk space to pull docker image', { diskUsage: diskUsage, dockerRootDir: info.DockerRootDir });
docker.downloadImage(manifest, function (error) {
if (error) return callback(error);
callback(null);
});
});
});
await docker.downloadImage(manifest);
}
function startApp(app, callback){
async function startApp(app) {
debugApp(app, 'startApp: starting container');
if (app.runState === apps.RSTATE_STOPPED) return callback();
if (app.runState === apps.RSTATE_STOPPED) return;
docker.startContainer(app.id, callback);
await docker.startContainer(app.id);
}
function install(app, args, progressCallback, callback) {
@@ -378,7 +344,7 @@ function install(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }),
reverseProxy.unconfigureApp.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
function teardownAddons(next) {
async function teardownAddons() {
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
let addonsToRemove;
if (oldManifest) {
@@ -387,7 +353,7 @@ function install(app, args, progressCallback, callback) {
addonsToRemove = app.manifest.addons;
}
services.teardownAddons(app, addonsToRemove, next);
await services.teardownAddons(app, addonsToRemove);
},
function deleteAppDirIfNeeded(done) {
@@ -396,10 +362,10 @@ function install(app, args, progressCallback, callback) {
deleteAppDir(app, { removeDirectory: false }, done); // do not remove any symlinked appdata dir
},
function deleteImageIfChanged(done) {
if (!oldManifest || oldManifest.dockerImage === app.manifest.dockerImage) return done();
async function deleteImageIfChanged() {
if (!oldManifest || oldManifest.dockerImage === app.manifest.dockerImage) return;
docker.deleteImage(oldManifest, done);
await docker.deleteImage(oldManifest);
},
// allocating container ip here, lets the users "repair" an app if allocation fails at apps.add time
@@ -472,12 +438,12 @@ function install(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
], async function seriesDone(error) {
if (error) {
debugApp(app, 'error installing app:', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
await safe(updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }));
}
callback(null);
callback(error);
});
}
@@ -495,13 +461,13 @@ function backup(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null })
], function seriesDone(error) {
], async function seriesDone(error) {
if (error) {
debugApp(app, 'error backing up app:', error);
// return to installed state intentionally. the error is stashed only in the task and not the app (the UI shows error state otherwise)
return updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null }, callback.bind(null, error));
await safe(updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null }));
}
callback(null);
callback(error);
});
}
@@ -526,12 +492,12 @@ function create(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
], async function seriesDone(error) {
if (error) {
debugApp(app, 'error creating :', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
await safe(updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }));
}
callback(null);
callback(error);
});
}
@@ -600,12 +566,12 @@ function changeLocation(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
], async function seriesDone(error) {
if (error) {
debugApp(app, 'error changing location:', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
await safe(updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }));
}
callback(null);
callback(error);
});
}
@@ -639,13 +605,13 @@ function migrateDataDir(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null, dataDir: newDataDir })
], function seriesDone(error) {
], async function seriesDone(error) {
if (error) {
debugApp(app, 'error migrating data dir:', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
await safe(updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }));
}
callback();
callback(error);
});
}
@@ -684,13 +650,13 @@ function configure(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
], async function seriesDone(error) {
if (error) {
debugApp(app, 'error reconfiguring:', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
await safe(updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }));
}
callback();
callback(error);
});
}
@@ -740,10 +706,10 @@ function update(app, args, progressCallback, callback) {
// we cannot easily 'recover' from backup failures because we have to revert manfest and portBindings
progressCallback.bind(null, { percent: 35, message: 'Cleaning up old install' }),
deleteContainers.bind(null, app, { managedOnly: true }),
function deleteImageIfChanged(done) {
if (app.manifest.dockerImage === updateConfig.manifest.dockerImage) return done();
async function deleteImageIfChanged() {
if (app.manifest.dockerImage === updateConfig.manifest.dockerImage) return;
docker.deleteImage(app.manifest, done);
await docker.deleteImage(app.manifest);
},
// only delete unused addons after backup
@@ -792,16 +758,16 @@ function update(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null, updateTime: new Date() })
], function seriesDone(error) {
], async function seriesDone(error) {
if (error && error.backupError) {
debugApp(app, 'update aborted because backup failed', error);
updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }, callback.bind(null, error));
await safe(updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }));
} else if (error) {
debugApp(app, 'Error updating app:', error);
updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
} else {
callback(null);
await safe(updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }));
}
callback(error);
});
}
@@ -827,12 +793,12 @@ function start(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
], async function seriesDone(error) {
if (error) {
debugApp(app, 'error starting app:', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
await safe(updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }));
}
callback(null);
callback(error);
});
}
@@ -854,12 +820,12 @@ function stop(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
], async function seriesDone(error) {
if (error) {
debugApp(app, 'error starting app:', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
await safe(updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }));
}
callback(null);
callback(error);
});
}
@@ -875,12 +841,12 @@ function restart(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
], async function seriesDone(error) {
if (error) {
debugApp(app, 'error starting app:', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
await safe(updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }));
}
callback(null);
callback(error);
});
}
@@ -914,12 +880,12 @@ function uninstall(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 95, message: 'Remove app from database' }),
apps.del.bind(null, app.id)
], function seriesDone(error) {
], async function seriesDone(error) {
if (error) {
debugApp(app, 'error uninstalling app:', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
await safe(updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }));
}
callback(null);
callback(error);
});
}