From 2692f6ef4e26a9f7285ddce0f37c8a74b5d686ba Mon Sep 17 00:00:00 2001 From: Girish Ramakrishnan Date: Fri, 20 Dec 2019 10:29:29 -0800 Subject: [PATCH] Add restart route for atomicity --- src/apps.js | 26 ++++++++++++++++++++++++++ src/apptask.js | 23 +++++++++++++++++++++++ src/docker.js | 19 ++++++++++++++++++- src/routes/apps.js | 13 +++++++++++++ src/routes/test/apps-test.js | 28 ++++++++++++++++++++++++++++ src/server.js | 1 + 6 files changed, 109 insertions(+), 1 deletion(-) diff --git a/src/apps.js b/src/apps.js index 740b07d81..296a49aff 100644 --- a/src/apps.js +++ b/src/apps.js @@ -43,6 +43,7 @@ exports = module.exports = { start: start, stop: stop, + restart: restart, exec: exec, @@ -79,6 +80,7 @@ exports = module.exports = { ISTATE_PENDING_BACKUP: 'pending_backup', // backup the app. this is state because it blocks other operations ISTATE_PENDING_START: 'pending_start', ISTATE_PENDING_STOP: 'pending_stop', + ISTATE_PENDING_RESTART: 'pending_restart', ISTATE_ERROR: 'error', // error executing last pending_* command ISTATE_INSTALLED: 'installed', // app is installed @@ -1678,6 +1680,30 @@ function stop(appId, callback) { }); } +function restart(appId, callback) { + assert.strictEqual(typeof appId, 'string'); + assert.strictEqual(typeof callback, 'function'); + + debug('Will restart app with id:%s', appId); + + get(appId, function (error, app) { + if (error) return callback(error); + + error = checkAppState(app, exports.ISTATE_PENDING_RESTART); + if (error) return callback(error); + + const task = { + args: {}, + values: { runState: exports.RSTATE_RUNNING } + }; + addTask(appId, exports.ISTATE_PENDING_RESTART, task, function (error, result) { + if (error) return callback(error); + + callback(null, { taskId: result.taskId }); + }); + }); +} + function checkManifestConstraints(manifest) { assert(manifest && typeof manifest === 'object'); diff --git a/src/apptask.js b/src/apptask.js index e3acf5f0e..d393f7ccf 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -952,6 +952,27 @@ function stop(app, args, progressCallback, callback) { }); } +function restart(app, args, progressCallback, callback) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof args, 'object'); + assert.strictEqual(typeof progressCallback, 'function'); + assert.strictEqual(typeof callback, 'function'); + + async.series([ + progressCallback.bind(null, { percent: 20, message: 'Restarting container' }), + docker.restartContainer.bind(null, app.id), + + progressCallback.bind(null, { percent: 100, message: 'Done' }), + updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }) + ], function seriesDone(error) { + if (error) { + debugApp(app, 'error starting app: %s', error); + return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error)); + } + callback(null); + }); +} + function uninstall(app, args, progressCallback, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof args, 'object'); @@ -1029,6 +1050,8 @@ function run(appId, args, progressCallback, callback) { return start(app, args, progressCallback, callback); case apps.ISTATE_PENDING_STOP: return stop(app, args, progressCallback, callback); + case apps.ISTATE_PENDING_RESTART: + return restart(app, args, progressCallback, callback); default: debugApp(app, 'apptask launched with invalid command'); return callback(new BoxError(BoxError.INTERNAL_ERROR, 'Unknown install command in apptask:' + app.installationState)); diff --git a/src/docker.js b/src/docker.js index bda5c111a..748dc5c9b 100644 --- a/src/docker.js +++ b/src/docker.js @@ -14,6 +14,7 @@ exports = module.exports = { downloadImage: downloadImage, createContainer: createContainer, startContainer: startContainer, + restartContainer: restartContainer, stopContainer: stopContainer, stopContainerByName: stopContainer, stopContainers: stopContainers, @@ -344,7 +345,23 @@ function startContainer(containerId, callback) { container.start(function (error) { if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND)); if (error && error.statusCode === 400) return callback(new BoxError(BoxError.BAD_FIELD, error)); // e.g start.sh is not executable - if (error && error.statusCode !== 304) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); + if (error && error.statusCode !== 304) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); // 304 means already started + + return callback(null); + }); +} + +function restartContainer(containerId, callback) { + assert.strictEqual(typeof containerId, 'string'); + assert.strictEqual(typeof callback, 'function'); + + var container = gConnection.getContainer(containerId); + debug('Restarting container %s', containerId); + + container.restart(function (error) { + if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND)); + if (error && error.statusCode === 400) return callback(new BoxError(BoxError.BAD_FIELD, error)); // e.g start.sh is not executable + if (error && error.statusCode !== 204) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); return callback(null); }); diff --git a/src/routes/apps.js b/src/routes/apps.js index 674321e31..48662a2d6 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -32,6 +32,7 @@ exports = module.exports = { stopApp: stopApp, startApp: startApp, + restartApp: restartApp, exec: exec, execWebSocket: execWebSocket, @@ -480,6 +481,18 @@ function stopApp(req, res, next) { }); } +function restartApp(req, res, next) { + assert.strictEqual(typeof req.params.id, 'string'); + + debug('Restart app id:%s', req.params.id); + + apps.restart(req.params.id, function (error, result) { + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(202, { taskId: result.taskId })); + }); +} + function updateApp(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); assert.strictEqual(typeof req.body, 'object'); diff --git a/src/routes/test/apps-test.js b/src/routes/test/apps-test.js index 45b544a9f..dace5740e 100644 --- a/src/routes/test/apps-test.js +++ b/src/routes/test/apps-test.js @@ -1570,6 +1570,34 @@ describe('App API', function () { }); }); }); + + it('can restart app', function (done) { + superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/restart') + .query({ access_token: token }) + .end(function (err, res) { + expect(res.statusCode).to.equal(202); + taskId = res.body.taskId; + done(); + }); + }); + + it('wait for app to restart', function (done) { + waitForTask(taskId, function () { setTimeout(done, 12000); }); // give app 12 seconds (to die and start) + }); + + it('did restart the app', function (done) { + apps.get(APP_ID, function (error, app) { + if (error) return done(error); + + superagent.get('http://localhost:' + app.httpPort + APP_MANIFEST.healthCheckPath) + .end(function (err, res) { + if (res && res.statusCode === 200) return done(); + done(new Error('app is not running')); + }); + }); + }); + + }); describe('uninstall', function () { diff --git a/src/server.js b/src/server.js index 97b009033..c81465362 100644 --- a/src/server.js +++ b/src/server.js @@ -277,6 +277,7 @@ function initializeExpressSync() { router.get ('/api/v1/apps/:id/backups', appsManageScope, routes.apps.listBackups); router.post('/api/v1/apps/:id/stop', appsManageScope, routes.apps.stopApp); router.post('/api/v1/apps/:id/start', appsManageScope, routes.apps.startApp); + router.post('/api/v1/apps/:id/restart', appsManageScope, routes.apps.restartApp); router.get ('/api/v1/apps/:id/logstream', appsManageScope, routes.apps.getLogStream); router.get ('/api/v1/apps/:id/logs', appsManageScope, routes.apps.getLogs); router.get ('/api/v1/apps/:id/exec', appsManageScope, routes.apps.exec);