docker.js and services.js: async'ify
This commit is contained in:
220
src/apptask.js
220
src/apptask.js
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user