diff --git a/CHANGES b/CHANGES index df0de95e4..ee51dfbed 100644 --- a/CHANGES +++ b/CHANGES @@ -2167,4 +2167,5 @@ * Display upload size and size progress * nfs: chown the backups for hardlinks to work * remove user add/remove/role change email notifications +* persist update indicator across restarts diff --git a/src/apps.js b/src/apps.js index 61926bb6f..ea03b0e02 100644 --- a/src/apps.js +++ b/src/apps.js @@ -135,7 +135,6 @@ var appdb = require('./appdb.js'), superagent = require('superagent'), tasks = require('./tasks.js'), TransformStream = require('stream').Transform, - updateChecker = require('./updatechecker.js'), users = require('./users.js'), util = require('util'), uuid = require('uuid'), @@ -1321,9 +1320,6 @@ function update(app, data, auditSource, callback) { eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId, app, skipBackup, toManifest: manifest, fromManifest: app.manifest, force: data.force, taskId: result.taskId }); - // clear update indicator, if update fails, it will come back through the update checker - updateChecker.resetAppUpdateInfo(appId); - callback(null, { taskId: result.taskId }); }); } diff --git a/src/test/updatechecker-test.js b/src/test/updatechecker-test.js index 355775c1f..325128b09 100644 --- a/src/test/updatechecker-test.js +++ b/src/test/updatechecker-test.js @@ -70,7 +70,7 @@ function cleanup(done) { ], done); } -describe('updatechecker - box - manual (email)', function () { +describe('updatechecker - box', function () { before(function (done) { safe.fs.unlinkSync(paths.UPDATE_CHECKER_FILE); @@ -102,7 +102,7 @@ describe('updatechecker - box - manual (email)', function () { updatechecker.checkForUpdates({ automatic: false }, function (error) { expect(!error).to.be.ok(); - expect(updatechecker.getUpdateInfo().box).to.be(null); + expect(updatechecker.getUpdateInfo().box).to.not.be.ok(); expect(scope.isDone()).to.be.ok(); checkMails(0, done); @@ -123,11 +123,11 @@ describe('updatechecker - box - manual (email)', function () { expect(updatechecker.getUpdateInfo().box.sourceTarballUrl).to.be('box.tar.gz'); expect(scope.isDone()).to.be.ok(); - checkMails(0, done); + checkMails(0, done); // it seems we stopped sending mails for box updates! }); }); - it('bad response offers nothing', function (done) { + it('bad response offers whatever was last valid', function (done) { nock.cleanAll(); var scope = nock('http://localhost:4444') @@ -137,42 +137,8 @@ describe('updatechecker - box - manual (email)', function () { updatechecker.checkForUpdates({ automatic: false }, function (error) { expect(error).to.be.ok(); - expect(updatechecker.getUpdateInfo().box).to.be(null); - expect(scope.isDone()).to.be.ok(); - - checkMails(0, done); - }); - }); -}); - -describe('updatechecker - box - automatic (no email)', function () { - before(function (done) { - mailer._mailQueue = []; - - async.series([ - database.initialize, - settings._setApiServerOrigin.bind(null, 'http://localhost:4444'), - cron.startJobs, - domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE), - settings.setAdminLocation.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain), - users.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE), - settingsdb.set.bind(null, settings.CLOUDRON_TOKEN_KEY, 'atoken'), - ], done); - }); - - after(cleanup); - - it('new version', function (done) { - nock.cleanAll(); - - var scope = nock('http://localhost:4444') - .get('/api/v1/boxupdate') - .query({ boxVersion: constants.VERSION, accessToken: 'atoken', automatic: false }) - .reply(200, { version: UPDATE_VERSION, changelog: [''], sourceTarballUrl: 'box.tar.gz', sourceTarballSigUrl: 'box.tar.gz.sig', boxVersionsUrl: 'box.versions', boxVersionsSigUrl: 'box.versions.sig' } ); - - updatechecker.checkForUpdates({ automatic: false }, function (error) { - expect(!error).to.be.ok(); expect(updatechecker.getUpdateInfo().box.version).to.be(UPDATE_VERSION); + expect(updatechecker.getUpdateInfo().box.sourceTarballUrl).to.be('box.tar.gz'); expect(scope.isDone()).to.be.ok(); checkMails(0, done); @@ -180,42 +146,7 @@ describe('updatechecker - box - automatic (no email)', function () { }); }); -describe('updatechecker - box - automatic free (email)', function () { - before(function (done) { - mailer._mailQueue = []; - - async.series([ - database.initialize, - settings._setApiServerOrigin.bind(null, 'http://localhost:4444'), - cron.startJobs, - domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE), - settings.setAdminLocation.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain), - users.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE), - settingsdb.set.bind(null, settings.CLOUDRON_TOKEN_KEY, 'atoken'), - ], done); - }); - - after(cleanup); - - it('new version', function (done) { - nock.cleanAll(); - - var scope = nock('http://localhost:4444') - .get('/api/v1/boxupdate') - .query({ boxVersion: constants.VERSION, accessToken: 'atoken', automatic: false }) - .reply(200, { version: UPDATE_VERSION, changelog: [''], sourceTarballUrl: 'box.tar.gz', sourceTarballSigUrl: 'box.tar.gz.sig', boxVersionsUrl: 'box.versions', boxVersionsSigUrl: 'box.versions.sig' } ); - - updatechecker.checkForUpdates({ automatic: false }, function (error) { - expect(!error).to.be.ok(); - expect(updatechecker.getUpdateInfo().box.version).to.be(UPDATE_VERSION); - expect(scope.isDone()).to.be.ok(); - - checkMails(0, done); - }); - }); -}); - -describe('updatechecker - app - manual (email)', function () { +describe('updatechecker - app', function () { var APP_0 = { id: 'appid-0', appStoreId: 'io.cloudron.app', @@ -271,7 +202,7 @@ describe('updatechecker - app - manual (email)', function () { updatechecker._checkAppUpdates({ automatic: false }, function (error) { expect(!error).to.be.ok(); - expect(updatechecker.getUpdateInfo().apps).to.eql({}); + expect(updatechecker.getUpdateInfo()).to.eql({}); expect(scope.isDone()).to.be.ok(); checkMails(0, done); @@ -288,7 +219,7 @@ describe('updatechecker - app - manual (email)', function () { updatechecker._checkAppUpdates({ automatic: false }, function (error) { expect(!error).to.be.ok(); - expect(updatechecker.getUpdateInfo().apps).to.eql({}); + expect(updatechecker.getUpdateInfo()).to.eql({}); expect(scope.isDone()).to.be.ok(); checkMails(0, done); @@ -305,7 +236,7 @@ describe('updatechecker - app - manual (email)', function () { updatechecker._checkAppUpdates({ automatic: false }, function (error) { expect(!error).to.be.ok(); - expect(updatechecker.getUpdateInfo().apps).to.eql({ 'appid-0': { manifest: { version: '2.0.0', changelog: '* some changes' }, unstable: false } }); + expect(updatechecker.getUpdateInfo()).to.eql({ 'appid-0': { manifest: { version: '2.0.0', changelog: '* some changes' }, unstable: false } }); expect(scope.isDone()).to.be.ok(); checkMails(1, done); @@ -317,136 +248,8 @@ describe('updatechecker - app - manual (email)', function () { updatechecker._checkAppUpdates({ automatic: false }, function (error) { expect(!error).to.be.ok(); - expect(updatechecker.getUpdateInfo().apps).to.eql({ }); + expect(updatechecker.getUpdateInfo()).to.eql({ }); checkMails(0, done); }); }); }); - -describe('updatechecker - app - automatic (no email)', function () { - var APP_0 = { - id: 'appid-0', - appStoreId: 'io.cloudron.app', - installationState: apps.ISTATE_PENDING_INSTALL, - error: null, - runState: 'running', - location: 'some-location-0', - domain: DOMAIN_0.domain, - manifest: { - version: '1.0.0', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0', - tcpPorts: { - PORT: { - description: 'this is a port that i expose', - containerPort: '1234' - } - } - }, - containerId: null, - portBindings: { PORT: 5678 }, - healthy: null, - accessRestriction: null, - memoryLimit: 0, - mailboxName: 'mail', - mailboxDomain: DOMAIN_0.domain - }; - - before(function (done) { - mailer._mailQueue = []; - - async.series([ - database.initialize, - database._clear, - settings._setApiServerOrigin.bind(null, 'http://localhost:4444'), - cron.startJobs, - domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE), - settings.setAdminLocation.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain), - users.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE), - appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0), - settings.setAutoupdatePattern.bind(null, '00 00 1,3,5,23 * * *'), - settingsdb.set.bind(null, settings.CLOUDRON_TOKEN_KEY, 'atoken'), - ], done); - }); - - after(cleanup); - - it('offers new version', function (done) { - nock.cleanAll(); - - var scope = nock('http://localhost:4444') - .get('/api/v1/appupdate') - .query({ boxVersion: constants.VERSION, accessToken: 'atoken', appId: APP_0.appStoreId, appVersion: APP_0.manifest.version, automatic: false }) - .reply(200, { manifest: { version: '2.0.0', changelog: 'c' } } ); - - updatechecker._checkAppUpdates({ automatic: false }, function (error) { - expect(!error).to.be.ok(); - expect(updatechecker.getUpdateInfo().apps).to.eql({ 'appid-0': { manifest: { version: '2.0.0', changelog: 'c' }, unstable: false } }); - expect(scope.isDone()).to.be.ok(); - - checkMails(1, done); - }); - }); -}); - -describe('updatechecker - app - automatic free (email)', function () { - var APP_0 = { - id: 'appid-0', - appStoreId: 'io.cloudron.app', - installationState: apps.ISTATE_PENDING_INSTALL, - error: null, - runState: 'running', - location: 'some-location-0', - domain: DOMAIN_0.domain, - manifest: { - version: '1.0.0', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0', - tcpPorts: { - PORT: { - description: 'this is a port that i expose', - containerPort: '1234' - } - } - }, - containerId: null, - portBindings: { PORT: 5678 }, - healthy: null, - accessRestriction: null, - memoryLimit: 0, - mailboxName: 'mail', - mailboxDomain: DOMAIN_0.domain - }; - - before(function (done) { - mailer._mailQueue = []; - - async.series([ - database.initialize, - database._clear, - settings._setApiServerOrigin.bind(null, 'http://localhost:4444'), - cron.startJobs, - domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE), - settings.setAdminLocation.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain), - users.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE), - appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0), - settings.setAutoupdatePattern.bind(null, '00 00 1,3,5,23 * * *'), - settingsdb.set.bind(null, settings.CLOUDRON_TOKEN_KEY, 'atoken'), - ], done); - }); - - after(cleanup); - - it('offers new version', function (done) { - nock.cleanAll(); - - var scope = nock('http://localhost:4444') - .get('/api/v1/appupdate') - .query({ boxVersion: constants.VERSION, accessToken: 'atoken', appId: APP_0.appStoreId, appVersion: APP_0.manifest.version, automatic: false }) - .reply(200, { manifest: { version: '2.0.0', changelog: 'c' } } ); - - updatechecker._checkAppUpdates({ automatic: false }, function (error) { - expect(!error).to.be.ok(); - expect(updatechecker.getUpdateInfo().apps).to.eql({ 'appid-0': { manifest: { version: '2.0.0', changelog: 'c' }, unstable: false } }); - expect(scope.isDone()).to.be.ok(); - - checkMails(1, done); - }); - }); -}); diff --git a/src/updatechecker.js b/src/updatechecker.js index 477431f0e..102907fe2 100644 --- a/src/updatechecker.js +++ b/src/updatechecker.js @@ -4,10 +4,7 @@ exports = module.exports = { checkForUpdates, getUpdateInfo, - resetUpdateInfo, - resetAppUpdateInfo, - _setUpdateInfo: setUpdateInfo, _checkAppUpdates: checkAppUpdates }; @@ -24,42 +21,16 @@ var apps = require('./apps.js'), settings = require('./settings.js'), users = require('./users.js'); -var gAppUpdateInfo = { }, // id -> update info { creationDate, manifest } - gBoxUpdateInfo = null; // { version, changelog, upgrade, sourceTarballUrl } - -function loadState() { - var state = safe.JSON.parse(safe.fs.readFileSync(paths.UPDATE_CHECKER_FILE, 'utf8')); - return state || { }; -} - -function saveState(state) { +function setUpdateInfo(state) { + console.dir(state); + // appid -> update info { creationDate, manifest } + // box -> { version, changelog, upgrade, sourceTarballUrl } safe.fs.writeFileSync(paths.UPDATE_CHECKER_FILE, JSON.stringify(state, null, 4), 'utf8'); } function getUpdateInfo() { - return { - apps: gAppUpdateInfo, - box: gBoxUpdateInfo - }; -} - -function setUpdateInfo(info) { - gBoxUpdateInfo = info.box; - gAppUpdateInfo = info.apps; -} - -function resetUpdateInfo() { - gBoxUpdateInfo = null; - resetAppUpdateInfo(); -} - -// If no appId provided all apps are reset -function resetAppUpdateInfo(appId) { - if (!appId) { - gAppUpdateInfo = {}; - } else { - delete gAppUpdateInfo[appId]; - } + const state = safe.JSON.parse(safe.fs.readFileSync(paths.UPDATE_CHECKER_FILE, 'utf8')); + return state || {}; } function checkAppUpdates(options, callback) { @@ -68,9 +39,8 @@ function checkAppUpdates(options, callback) { debug('Checking App Updates'); - gAppUpdateInfo = { }; - var oldState = loadState(); - var newState = { }; // create new state so that old app ids are removed + let state = getUpdateInfo(); + let newState = { }; // create new state so that old app ids are removed settings.getAutoupdatePattern(function (error, result) { if (error) return callback(error); @@ -90,36 +60,25 @@ function checkAppUpdates(options, callback) { return iteratorDone(); // continue to next } - // skip if no next version is found - if (!updateInfo) { - delete gAppUpdateInfo[app.id]; - return iteratorDone(); - } + if (!updateInfo) return iteratorDone(); // skip if no next version is found - gAppUpdateInfo[app.id] = updateInfo; + newState[app.id] = updateInfo; - // decide whether to send email - newState[app.id] = updateInfo.manifest.version; - - if (oldState[app.id] === newState[app.id]) { - debug('Skipping notification of app update %s since user was already notified', app.id); + if (safe.query(state[app.id], 'manifest.version') === updateInfo.manifest.version) { + debug(`Skipping app update notification of ${app.id} since user was already notified of ${updateInfo.manifest.version}`); return iteratorDone(); } const canAutoupdateApp = apps.canAutoupdateApp(app, updateInfo); if (autoupdatesEnabled && canAutoupdateApp) return iteratorDone(); - debug('Notifying of app update for %s from %s to %s', app.id, app.manifest.version, updateInfo.manifest.version); - notificationPending.push({ - app: app, - updateInfo: updateInfo - }); - + debug(`Notifying of app update for ${app.id} from ${app.manifest.version} to ${updateInfo.manifest.version}`); + notificationPending.push({ app, updateInfo }); iteratorDone(); }); }, function () { - newState.box = loadState().box; // preserve the latest box state information - saveState(newState); + if ('box' in state) newState.box = state.box; // preserve the latest box state information + setUpdateInfo(newState); if (notificationPending.length === 0) return callback(); @@ -142,17 +101,12 @@ function checkBoxUpdates(options, callback) { debug('Checking Box Updates'); - gBoxUpdateInfo = null; - appstore.getBoxUpdate(options, function (error, updateInfo) { if (error || !updateInfo) return callback(error); - gBoxUpdateInfo = updateInfo; + let state = getUpdateInfo(); - // decide whether to send email - var state = loadState(); - - if (state.box === gBoxUpdateInfo.version) { + if (state.box && state.box.version === updateInfo.version) { debug('Skipping notification of box update as user was already notified'); return callback(); } @@ -164,8 +118,8 @@ function checkBoxUpdates(options, callback) { notifications.alert(notifications.ALERT_BOX_UPDATE, `Cloudron v${updateInfo.version} is available`, message, function (error) { if (error) return callback(error); - state.box = updateInfo.version; - saveState(state); + state.box = updateInfo; + setUpdateInfo(state); callback(); });