diff --git a/migrations/schema.sql b/migrations/schema.sql index 90ecd5681..dd0265257 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -64,8 +64,8 @@ CREATE TABLE IF NOT EXISTS clients( CREATE TABLE IF NOT EXISTS apps( id VARCHAR(128) NOT NULL UNIQUE, appStoreId VARCHAR(128) NOT NULL, - installationState VARCHAR(512) NOT NULL, - runState VARCHAR(512), + installationState VARCHAR(512) NOT NULL, // the active task on the app + runState VARCHAR(512), // if the app is stopped health VARCHAR(128), healthTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app last responded containerId VARCHAR(128), diff --git a/src/appdb.js b/src/appdb.js index 9160bfac2..1e3d32dbe 100644 --- a/src/appdb.js +++ b/src/appdb.js @@ -239,6 +239,7 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal const accessRestrictionJson = JSON.stringify(accessRestriction); const memoryLimit = data.memoryLimit || 0; const installationState = data.installationState || 'pending_install'; // FIXME + const runState = data.runState || 'running'; // FIXME const sso = 'sso' in data ? data.sso : null; const robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null; const debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null; @@ -250,10 +251,10 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal var queries = []; queries.push({ - query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, ' + query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, ' + 'sso, debugModeJson, robotsTxt, mailboxName, label, tagsJson) ' - + ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', - args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, + + ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, sso, debugModeJson, robotsTxt, mailboxName, label, tagsJson ] }); diff --git a/src/apps.js b/src/apps.js index f80cd9922..18d012911 100644 --- a/src/apps.js +++ b/src/apps.js @@ -64,7 +64,7 @@ exports = module.exports = { PORT_TYPE_TCP: 'tcp', PORT_TYPE_UDP: 'udp', - // installation codes (keep in sync in UI) + // task codes - the installation state is now a misnomer (keep in sync in UI) ISTATE_PENDING_INSTALL: 'pending_install', // installs and fresh reinstalls ISTATE_PENDING_CLONE: 'pending_clone', // clone ISTATE_PENDING_CONFIGURE: 'pending_configure', // infra update @@ -78,13 +78,13 @@ exports = module.exports = { ISTATE_PENDING_RESTORE: 'pending_restore', // restore to previous backup or on upgrade ISTATE_PENDING_UPDATE: 'pending_update', // update from installed state preserving data 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_ERROR: 'error', // error executing last pending_* command ISTATE_INSTALLED: 'installed', // app is installed // run states RSTATE_RUNNING: 'running', - RSTATE_PENDING_START: 'pending_start', - RSTATE_PENDING_STOP: 'pending_stop', RSTATE_STOPPED: 'stopped', // app stopped by us // health states (keep in sync in UI) @@ -789,7 +789,8 @@ function install(data, user, auditSource, callback) { alternateDomains: alternateDomains, env: env, label: label, - tags: tags + tags: tags, + runState: exports.RSTATE_RUNNING }; appdb.add(appId, appStoreId, manifest, location, domain, translatePortBindings(portBindings, manifest), data, function (error) { @@ -1564,10 +1565,10 @@ function start(appId, callback) { get(appId, function (error, app) { if (error) return callback(error); - error = checkAppState(app, exports.exports.ISTATE_INSTALLED); // FIXME + error = checkAppState(app, exports.ISTATE_PENDING_START); if (error) return callback(error); - scheduleTask(appId, { /* args */ }, { runState: exports.RSTATE_PENDING_START }, callback); + scheduleTask(appId, { /* args */ }, { installationState: exports.ISTATE_PENDING_START, runState: exports.RSTATE_RUNNING }, callback); }); } @@ -1580,10 +1581,10 @@ function stop(appId, callback) { get(appId, function (error, app) { if (error) return callback(error); - error = checkAppState(app, exports.ISTATE_INSTALLED); // FIXME + error = checkAppState(app, exports.ISTATE_PENDING_STOP); if (error) return callback(error); - scheduleTask(appId, { /* args */ }, { runState: exports.RSTATE_PENDING_STOP }, callback); + scheduleTask(appId, { /* args */ }, { installationState: exports.ISTATE_PENDING_STOP, runState: exports.RSTATE_STOPPED }, callback); }); } diff --git a/src/apptask.js b/src/apptask.js index 90d54436e..30c3ed076 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -489,6 +489,12 @@ function downloadImage(manifest, callback) { }); } +function startApp(app, callback){ + if (app.runState === apps.RSTATE_STOPPED) return callback(); + + docker.startContainer(app.id, callback); +} + // Ordering is based on the following rationale: // - configure nginx, icon, oauth // - register subdomain. @@ -520,7 +526,7 @@ function install(app, args, progressCallback, callback) { unconfigureReverseProxy.bind(null, app), removeCollectdProfile.bind(null, app), removeLogrotateConfig.bind(null, app), - stopApp.bind(null, app, progressCallback), + docker.stopContainers.bind(null, app.id), deleteContainers.bind(null, app, { managedOnly: true }), function teardownAddons(next) { // when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords @@ -580,7 +586,7 @@ function install(app, args, progressCallback, callback) { progressCallback.bind(null, { percent: 80, message: 'Setting up collectd profile' }), addCollectdProfile.bind(null, app), - runApp.bind(null, app, progressCallback), + startApp.bind(null, app), progressCallback.bind(null, { percent: 85, message: 'Waiting for DNS propagation' }), exports._waitForDnsPropagation.bind(null, app), @@ -631,7 +637,7 @@ function create(app, args, progressCallback, callback) { async.series([ progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }), - stopApp.bind(null, app, progressCallback), + docker.stopContainers.bind(null, app.id), deleteContainers.bind(null, app, { managedOnly: true }), // FIXME: re-setup addons only because sendmail addon to re-inject env vars on mailboxName change @@ -641,8 +647,7 @@ function create(app, args, progressCallback, callback) { progressCallback.bind(null, { percent: 60, message: 'Creating container' }), createContainer.bind(null, app), - progressCallback.bind(null, { percent: 80, message: 'Starting app' }), - runApp.bind(null, app, progressCallback), + startApp.bind(null, app), progressCallback.bind(null, { percent: 100, message: 'Done' }), updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }) @@ -667,7 +672,7 @@ function changeLocation(app, args, progressCallback, callback) { async.series([ progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }), unconfigureReverseProxy.bind(null, app), - stopApp.bind(null, app, progressCallback), + docker.stopContainers.bind(null, app.id), deleteContainers.bind(null, app, { managedOnly: true }), function (next) { let obsoleteDomains = oldConfig.alternateDomains.filter(function (o) { @@ -691,7 +696,7 @@ function changeLocation(app, args, progressCallback, callback) { progressCallback.bind(null, { percent: 60, message: 'Creating container' }), createContainer.bind(null, app), - runApp.bind(null, app, progressCallback), + startApp.bind(null, app), progressCallback.bind(null, { percent: 80, message: 'Waiting for DNS propagation' }), exports._waitForDnsPropagation.bind(null, app), @@ -721,7 +726,7 @@ function migrateDataDir(app, args, progressCallback, callback) { async.series([ progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }), - stopApp.bind(null, app, progressCallback), + docker.stopContainers.bind(null, app.id), deleteContainers.bind(null, app, { managedOnly: true }), progressCallback.bind(null, { percent: 45, message: 'Ensuring app data directory' }), @@ -743,8 +748,7 @@ function migrateDataDir(app, args, progressCallback, callback) { progressCallback.bind(null, { percent: 60, message: 'Creating container' }), createContainer.bind(null, app), - progressCallback.bind(null, { percent: 80, message: 'Starting app' }), - runApp.bind(null, app, progressCallback), + startApp.bind(null, app), progressCallback.bind(null, { percent: 100, message: 'Done' }), updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }) @@ -773,7 +777,7 @@ function configure(app, args, progressCallback, callback) { unconfigureReverseProxy.bind(null, app), removeCollectdProfile.bind(null, app), removeLogrotateConfig.bind(null, app), - stopApp.bind(null, app, progressCallback), + docker.stopContainers.bind(null, app.id), deleteContainers.bind(null, app, { managedOnly: true }), function (next) { let obsoleteDomains = oldConfig.alternateDomains.filter(function (o) { @@ -821,7 +825,7 @@ function configure(app, args, progressCallback, callback) { progressCallback.bind(null, { percent: 70, message: 'Add collectd profile' }), addCollectdProfile.bind(null, app), - runApp.bind(null, app, progressCallback), + startApp.bind(null, app), progressCallback.bind(null, { percent: 80, message: 'Waiting for DNS propagation' }), exports._waitForDnsPropagation.bind(null, app), @@ -883,7 +887,7 @@ function update(app, args, progressCallback, callback) { // note: we cleanup first and then backup. this is done so that the app is not running should backup fail // 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' }), - stopApp.bind(null, app, progressCallback), + docker.stopContainers.bind(null, app.id), deleteContainers.bind(null, app, { managedOnly: true }), function deleteImageIfChanged(done) { if (app.manifest.dockerImage === updateConfig.manifest.dockerImage) return done(); @@ -926,7 +930,7 @@ function update(app, args, progressCallback, callback) { progressCallback.bind(null, { percent: 80, message: 'Creating container' }), createContainer.bind(null, app), - runApp.bind(null, app, progressCallback), + startApp.bind(null, app), progressCallback.bind(null, { percent: 100, message: 'Done' }), updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null, updateTime: new Date() }) @@ -945,6 +949,48 @@ function update(app, args, progressCallback, callback) { }); } +function start(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: 'Starting container' }), + docker.startContainer.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, args) }, callback.bind(null, error)); + } + callback(null); + }); +} + +function stop(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: 'Stopping container' }), + docker.stopContainers.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, args) }, callback.bind(null, error)); + } + callback(null); + }); +} + function uninstall(app, args, progressCallback, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof args, 'object'); @@ -959,7 +1005,7 @@ function uninstall(app, args, progressCallback, callback) { removeLogrotateConfig.bind(null, app), progressCallback.bind(null, { percent: 10, message: 'Stopping app' }), - stopApp.bind(null, app, progressCallback), + docker.stopContainers.bind(null, app.id), progressCallback.bind(null, { percent: 20, message: 'Deleting container' }), deleteContainers.bind(null, app, {}), @@ -996,34 +1042,6 @@ function uninstall(app, args, progressCallback, callback) { }); } -function runApp(app, progressCallback, callback) { - assert.strictEqual(typeof app, 'object'); - assert.strictEqual(typeof progressCallback, 'function'); - assert.strictEqual(typeof callback, 'function'); - - progressCallback({ message: 'Starting app' }); - - docker.startContainer(app.containerId, function (error) { - if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, `Error starting container: ${error.message}`)); - - updateApp(app, { runState: apps.RSTATE_RUNNING }, callback); - }); -} - -function stopApp(app, progressCallback, callback) { - assert.strictEqual(typeof app, 'object'); - assert.strictEqual(typeof progressCallback, 'function'); - assert.strictEqual(typeof callback, 'function'); - - progressCallback({ message: 'Stopping app' }); - - docker.stopContainers(app.id, function (error) { - if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, `Error starting container: ${error.message}`)); - - updateApp(app, { runState: apps.RSTATE_STOPPED, health: null }, callback); - }); -} - function run(appId, args, progressCallback, callback) { assert.strictEqual(typeof appId, 'string'); assert.strictEqual(typeof args, 'object'); @@ -1065,13 +1083,10 @@ function run(appId, args, progressCallback, callback) { return update(app, args, progressCallback, callback); case apps.ISTATE_PENDING_BACKUP: return backup(app, args, progressCallback, callback); - case apps.ISTATE_INSTALLED: - switch (app.runState) { - case apps.RSTATE_PENDING_STOP: return stopApp(app, progressCallback, callback); - case apps.RSTATE_PENDING_START: return runApp(app, progressCallback, callback); - default: return callback(new Error('Unknown run command in apptask:' + app.runState)); - } - + case apps.ISTATE_PENDING_START: + return start(app, args, progressCallback, callback); + case apps.ISTATE_PENDING_STOP: + return stop(app, args, progressCallback, callback); default: debugApp(app, 'apptask launched with invalid command'); return callback(new Error('Unknown install command in apptask:' + app.installationState));