diff --git a/CHANGES b/CHANGES index aace9b1be..1d7e9ae6f 100644 --- a/CHANGES +++ b/CHANGES @@ -525,4 +525,5 @@ - Enable Developer mode by default for new Cloudrons - Reverse proxy fixes for apps exposing a WebDav server - Allow admins to optionally set the username and displayName on user creation +- Fix app autoupdate logic to detect if one or more in-use port bindings was removed diff --git a/src/apps.js b/src/apps.js index 79f734f98..e104aa16e 100644 --- a/src/apps.js +++ b/src/apps.js @@ -350,6 +350,8 @@ function purchase(appStoreId, callback) { } function downloadManifest(appStoreId, manifest, callback) { + if (!appStoreId && !manifest) return callback(new AppsError(AppsError.BAD_FIELD, 'Neither manifest nor appStoreId provided')); + if (!appStoreId) return callback(null, '', manifest); var parts = appStoreId.split('@'); @@ -841,18 +843,19 @@ function updateApps(updateInfo, auditSource, callback) { // updateInfo is { appI assert.strictEqual(typeof callback, 'function'); function canAutoupdateApp(app, newManifest) { - var tcpPorts = newManifest.tcpPorts || { }; + var newTcpPorts = newManifest.newTcpPorts || { }; + var oldTcpPorts = app.manifest.tcpPorts || { }; var portBindings = app.portBindings; // this is never null - if (Object.keys(tcpPorts).length === 0 && Object.keys(portBindings).length === 0) return null; - if (Object.keys(tcpPorts).length === 0) return new Error('tcpPorts is now empty but portBindings is not'); - if (Object.keys(portBindings).length === 0) return new Error('portBindings is now empty but tcpPorts is not'); - - for (var env in tcpPorts) { - if (!(env in portBindings)) return new Error(env + ' is required from user'); + for (var env in newTcpPorts) { + if (!(env in oldTcpPorts)) return new Error(env + ' is required from user'); } - // it's fine if one or more keys got removed + for (env in portBindings) { + if (!(env in newTcpPorts)) return new Error(env + ' was in use but new update removes it'); + } + + // it's fine if one or more (unused) keys got removed return null; } diff --git a/src/mailer.js b/src/mailer.js index fe76a7022..6cd2e731d 100644 --- a/src/mailer.js +++ b/src/mailer.js @@ -114,7 +114,7 @@ function getTxtRecords(callback) { function checkDns() { getTxtRecords(function (error, records) { if (error || !records) { - debug('checkDns: DNS error or no records looking up TXT records for %s %s', config.adminFqdn(), error, records); + debug('checkDns: DNS error or no records looking up TXT records for %s %s', config.fqdn(), error, records); gCheckDnsTimerId = setTimeout(checkDns, 60000); return; } diff --git a/src/routes/cloudron.js b/src/routes/cloudron.js index b156e973b..3d6ce376c 100644 --- a/src/routes/cloudron.js +++ b/src/routes/cloudron.js @@ -8,10 +8,12 @@ exports = module.exports = { getProgress: getProgress, getConfig: getConfig, update: update, - feedback: feedback + feedback: feedback, + checkForUpdates: checkForUpdates }; var assert = require('assert'), + async = require('async'), cloudron = require('../cloudron.js'), CloudronError = cloudron.CloudronError, config = require('../config.js'), @@ -20,7 +22,8 @@ var assert = require('assert'), HttpSuccess = require('connect-lastmile').HttpSuccess, progress = require('../progress.js'), mailer = require('../mailer.js'), - superagent = require('superagent'); + superagent = require('superagent'), + updateChecker = require('../updatechecker.js'); function auditSource(req) { var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null; @@ -129,6 +132,15 @@ function update(req, res, next) { }); } +function checkForUpdates(req, res, next) { + async.series([ + updateChecker.checkAppUpdates, + updateChecker.checkBoxUpdates + ], function () { + next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() })); + }); +} + function feedback(req, res, next) { assert.strictEqual(typeof req.user, 'object'); diff --git a/src/routes/test/apps-test.js b/src/routes/test/apps-test.js index 01f2cd0aa..028d8e4a7 100644 --- a/src/routes/test/apps-test.js +++ b/src/routes/test/apps-test.js @@ -229,6 +229,39 @@ describe('Apps', function () { }); }); + it('app install fails - null manifest', function (done) { + superagent.post(SERVER_URL + '/api/v1/apps/install') + .query({ access_token: token }) + .send({ manifest: null, password: PASSWORD }) + .end(function (err, res) { + expect(res.statusCode).to.equal(400); + expect(res.body.message).to.eql('appStoreId or manifest is required'); + done(); + }); + }); + + it('app install fails - bad manifest format', function (done) { + superagent.post(SERVER_URL + '/api/v1/apps/install') + .query({ access_token: token }) + .send({ manifest: 'epic', password: PASSWORD }) + .end(function (err, res) { + expect(res.statusCode).to.equal(400); + expect(res.body.message).to.eql('manifest must be an object'); + done(); + }); + }); + + it('app install fails - empty appStoreId format', function (done) { + superagent.post(SERVER_URL + '/api/v1/apps/install') + .query({ access_token: token }) + .send({ manifest: null, appStoreId: '', password: PASSWORD }) + .end(function (err, res) { + expect(res.statusCode).to.equal(400); + expect(res.body.message).to.eql('appStoreId or manifest is required'); + done(); + }); + }); + it('app install fails - invalid json', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/install') .query({ access_token: token }) diff --git a/src/server.js b/src/server.js index dd4b7b290..eb25e148f 100644 --- a/src/server.js +++ b/src/server.js @@ -94,6 +94,7 @@ function initializeExpressSync() { // cloudron routes router.get ('/api/v1/cloudron/config', cloudronScope, routes.cloudron.getConfig); router.post('/api/v1/cloudron/update', cloudronScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.update); + router.post('/api/v1/cloudron/check_for_updates', cloudronScope, routes.user.requireAdmin, routes.cloudron.checkForUpdates); router.post('/api/v1/cloudron/reboot', cloudronScope, routes.cloudron.reboot); router.get ('/api/v1/cloudron/graphs', cloudronScope, routes.graphs.getGraphs);