diff --git a/src/appdb.js b/src/appdb.js index 1ed7f5ac6..c16f8e25c 100644 --- a/src/appdb.js +++ b/src/appdb.js @@ -475,7 +475,7 @@ function setHealth(appId, health, healthTime, callback) { var values = { health, healthTime }; - var constraints = 'AND runState NOT LIKE "pending_%" AND installationState = "installed"'; + var constraints = `AND runState NOT LIKE "pending_%" AND installationState = "${exports.ISTATE_INSTALLED}"`; updateWithConstraints(appId, values, constraints, callback); } @@ -483,36 +483,13 @@ function setHealth(appId, health, healthTime, callback) { function setInstallationCommand(appId, installationState, values, callback) { assert.strictEqual(typeof appId, 'string'); assert.strictEqual(typeof installationState, 'string'); - - if (typeof values === 'function') { - callback = values; - values = { }; - } else { - assert.strictEqual(typeof values, 'object'); - assert.strictEqual(typeof callback, 'function'); - } + assert.strictEqual(typeof values, 'object'); + assert.strictEqual(typeof callback, 'function'); values.installationState = installationState; values.errorMessage = ''; - // Rules are: - // uninstall is allowed in any state - // force update is allowed in any state including pending_uninstall! (for better or worse) - // restore is allowed from installed or error state or currently restoring - // configure is allowed in installed state or currently configuring or in error state - // update and backup are allowed only in installed state - - if (installationState === exports.ISTATE_PENDING_UNINSTALL || installationState === exports.ISTATE_PENDING_FORCE_UPDATE) { - updateWithConstraints(appId, values, '', callback); - } else if (installationState === exports.ISTATE_PENDING_RESTORE) { - updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error" OR installationState = "pending_restore")', callback); - } else if (installationState === exports.ISTATE_PENDING_UPDATE || installationState === exports.ISTATE_PENDING_BACKUP) { - updateWithConstraints(appId, values, 'AND installationState = "installed"', callback); - } else if (installationState === exports.ISTATE_PENDING_CONFIGURE) { - updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "pending_configure" OR installationState = "error")', callback); - } else { - callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, 'invalid installationState')); - } + updateWithConstraints(appId, values, `AND (installationState = "${exports.ISTATE_INSTALLED}" OR installationState = "${exports.ISTATE_ERROR}")`, callback); } function setRunCommand(appId, runState, callback) { @@ -521,7 +498,7 @@ function setRunCommand(appId, runState, callback) { assert.strictEqual(typeof callback, 'function'); var values = { runState: runState }; - updateWithConstraints(appId, values, 'AND runState NOT LIKE "pending_%" AND installationState = "installed"', callback); + updateWithConstraints(appId, values, `AND installationState = "${exports.ISTATE_INSTALLED}"`, callback); } function getAppStoreIds(callback) { diff --git a/src/apps.js b/src/apps.js index d734809af..b38460ba5 100644 --- a/src/apps.js +++ b/src/apps.js @@ -750,6 +750,7 @@ function configure(appId, data, user, auditSource, callback) { get(appId, function (error, app) { if (error) return callback(error); + if (app.installationState !== appdb.ISTATE_INSTALLED && app.installationState !== appdb.ISTATE_ERROR) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`)); let domain, location, portBindings, values = { }; if ('location' in data && 'domain' in data) { @@ -896,6 +897,7 @@ function update(appId, data, auditSource, callback) { get(appId, function (error, app) { if (error) return callback(error); + if (app.installationState !== appdb.ISTATE_INSTALLED && app.installationState !== appdb.ISTATE_ERROR) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`)); downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) { if (error) return callback(error); @@ -1029,6 +1031,7 @@ function restore(appId, data, auditSource, callback) { get(appId, function (error, app) { if (error) return callback(error); + if (app.installationState !== appdb.ISTATE_INSTALLED && app.installationState !== appdb.ISTATE_ERROR) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`)); // for empty or null backupId, use existing manifest to mimic a reinstall var func = data.backupId ? backups.get.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); }; @@ -1186,6 +1189,7 @@ function uninstall(appId, auditSource, callback) { get(appId, function (error, app) { if (error) return callback(error); + if (app.installationState !== appdb.ISTATE_INSTALLED && app.installationState !== appdb.ISTATE_ERROR) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`)); appstore.unpurchaseApp(appId, { appstoreId: app.appStoreId, manifestId: app.manifest.id }, function (error) { if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND)); @@ -1216,11 +1220,17 @@ function start(appId, callback) { debug('Will start app with id:%s', appId); - appdb.setRunCommand(appId, appdb.RSTATE_PENDING_START, function (error) { - if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess - if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); + get(appId, function (error, app) { + if (error) return callback(error); + if (app.installationState !== appdb.ISTATE_INSTALLED) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`)); + if (app.runState !== appdb.RSTATE_STOPPED) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`)); - scheduleTask(appId, {}, callback); + appdb.setRunCommand(appId, appdb.RSTATE_PENDING_START, function (error) { + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess + if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); + + scheduleTask(appId, {}, callback); + }); }); } @@ -1230,11 +1240,17 @@ function stop(appId, callback) { debug('Will stop app with id:%s', appId); - appdb.setRunCommand(appId, appdb.RSTATE_PENDING_STOP, function (error) { - if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess - if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); + get(appId, function (error, app) { + if (error) return callback(error); + if (app.installationState !== appdb.ISTATE_INSTALLED) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`)); + if (app.runState !== appdb.RSTATE_RUNNING) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`)); - scheduleTask(appId, {}, callback); + appdb.setRunCommand(appId, appdb.RSTATE_PENDING_STOP, function (error) { + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess + if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); + + scheduleTask(appId, {}, callback); + }); }); } diff --git a/src/routes/apps.js b/src/routes/apps.js index 2b34a8929..6ad9028b4 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -276,6 +276,7 @@ function uninstallApp(req, res, next) { apps.uninstall(req.params.id, auditSource.fromRequest(req), function (error, result) { if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error)); + if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message)); if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app')); if (error) return next(new HttpError(500, error)); diff --git a/src/routes/test/apps-test.js b/src/routes/test/apps-test.js index 087b485a9..3ce38b8fb 100644 --- a/src/routes/test/apps-test.js +++ b/src/routes/test/apps-test.js @@ -228,6 +228,8 @@ function stopBox(done) { } describe('App API', function () { + let taskId = ''; + before(startBox); after(stopBox); @@ -424,6 +426,7 @@ describe('App API', function () { APP_ID = res.body.id; expect(fake2.isDone()).to.be.ok(); expect(fake3.isDone()).to.be.ok(); + taskId = res.body.taskId; done(); }); }); @@ -500,6 +503,15 @@ describe('App API', function () { }); }); + it('can stop the task', function (done) { + superagent.post(SERVER_URL + '/api/v1/tasks/' + taskId + '/stop') + .query({ access_token: token }) + .end(function (err, res) { + expect(res.statusCode).to.equal(204); + setTimeout(done, 4000); // wait for it to really die + }); + }); + it('can uninstall app', function (done) { var fake1 = nock(settings.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/cloudronapps/') >= 0; }).reply(200, { }); var fake2 = nock(settings.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/cloudronapps/') >= 0; }).reply(204, { });