Compare commits

..

27 Commits

Author SHA1 Message Date
Johannes Zellner 1395d2971b Send app update changelog with email
Fixes #579
2016-02-04 15:31:40 +01:00
Johannes Zellner e9d6badae7 Changelog is just a flat text, no array 2016-02-04 15:17:18 +01:00
Johannes Zellner 65ddc7f24c Show changelog in app update ui 2016-02-04 15:17:18 +01:00
girish@cloudron.io fa871c7ada some apache configs require Host header to be set 2016-02-03 20:18:59 -08:00
girish@cloudron.io 8652d6c136 Add 0.8.0 changes 2016-02-02 08:51:13 -08:00
girish@cloudron.io 16d976a145 use multidb version of mysql addon 2016-02-02 08:46:09 -08:00
girish@cloudron.io fa1f5cc454 call the multi methods if multipleDatabases is set 2016-02-02 08:41:41 -08:00
Johannes Zellner 84c3b367d5 Actually wait for apps to be stopped 2016-01-29 17:24:18 +01:00
Johannes Zellner 793aa6512d Rework parts of the apps tests to be more reliable
No need to create and tear down the addons everytime.
Docker proxy can also be just injected once.
Wait explicitly for apptasks to be terminated.
2016-01-29 16:27:37 +01:00
Johannes Zellner 98ab99ab34 Add callback to config._reset() for convenience 2016-01-29 16:27:04 +01:00
Johannes Zellner 24a826bdd1 Reduce from 20 to 10 sec wait for addons
Since that is reliable already with 10 on my laptop
I assume this is fine for any other more modern machine
2016-01-29 16:26:15 +01:00
Johannes Zellner 05245f5fc7 Add a way to wait and stop pending tasks 2016-01-29 16:25:31 +01:00
Johannes Zellner b718c8d044 Cleanup config before and after apps tests 2016-01-29 14:30:40 +01:00
Johannes Zellner 2888a85081 Remove cloudron side migrate api
This is some old api when we had a migrate view in the webadmin
This is entirely handled on the appstore for now.
2016-01-29 14:17:33 +01:00
Johannes Zellner 307262244a Also cleanup the config file on config._reset() 2016-01-29 14:17:10 +01:00
Johannes Zellner 9a875634f8 Improve error message in mailer 2016-01-29 12:40:34 +01:00
Johannes Zellner 4af33486ae Add missing fi in shell script 2016-01-29 12:31:59 +01:00
Johannes Zellner befa898f18 Do not show app version, but make it available as a tooltip on the title for us 2016-01-29 12:29:35 +01:00
Johannes Zellner 18525e1236 Show app website in appstore install dialog
Fixes #580
2016-01-29 12:26:42 +01:00
Johannes Zellner 28ffd01cf4 Invoke BACKUP_APP_CMD with the additional backupConfig.url 2016-01-29 11:55:52 +01:00
Johannes Zellner 09c7aa4440 Remove whitespace 2016-01-29 11:54:58 +01:00
Johannes Zellner ea4862d351 Strictly assert on app 2016-01-29 11:54:40 +01:00
Johannes Zellner 3e4d62329e Also copy the backup config json 2016-01-29 11:54:15 +01:00
Johannes Zellner d12366576b Add backup config url to backupapp.sh 2016-01-29 11:44:24 +01:00
Johannes Zellner 7b1d906494 Add backups.getAppBackupConfigUrl() 2016-01-29 11:44:24 +01:00
Johannes Zellner 0972c88b8b Set the correct environment for the installer.sh 2016-01-28 16:36:34 +01:00
girish@cloudron.io 9464a26a7e put version in the mail 2016-01-27 10:35:48 -08:00
23 changed files with 1479 additions and 1588 deletions
+3
View File
@@ -400,3 +400,6 @@
- Improved box update management using prereleases
- Less aggressive disk space checks
[0.8.0]
- MySQL addon : multiple database support
+10 -5
View File
@@ -20,10 +20,15 @@ readonly provider="${5}"
readonly revision="${6}"
# environment specific urls
readonly api_server_origin="https://api.dev.cloudron.io"
readonly web_server_origin="https://dev.cloudron.io"
readonly release_bucket_url="https://s3.amazonaws.com/dev-cloudron-releases"
readonly versions_url="https://s3.amazonaws.com/dev-cloudron-releases/versions.json"
<% if (env === 'prod') { %>
readonly api_server_origin="https://api.cloudron.io"
readonly web_server_origin="https://cloudron.io"
<% } else { %>
readonly api_server_origin="https://api.<%= env %>.cloudron.io"
readonly web_server_origin="https://<%= env %>.cloudron.io"
<% } %>
readonly release_bucket_url="https://s3.amazonaws.com/<%= env %>-cloudron-releases"
readonly versions_url="https://s3.amazonaws.com/<%= env %>-cloudron-releases/versions.json"
readonly installer_code_url="${release_bucket_url}/box-${revision}.tar.gz"
# runtime consts
@@ -132,7 +137,7 @@ cat > /root/provision.json <<EOF
"secretAccessKey": "${aws_access_key_secret}"
},
"tlsConfig": {
"provider": "letsencrypt-dev"
"provider": "letsencrypt-<%= env %>"
}
}
}
+2 -2
View File
@@ -3,12 +3,12 @@
# If you change the infra version, be sure to put a warning
# in the change log
INFRA_VERSION=21
INFRA_VERSION=22
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
# These constants are used in the installer script as well
BASE_IMAGE=cloudron/base:0.8.0
MYSQL_IMAGE=cloudron/mysql:0.8.0
MYSQL_IMAGE=cloudron/mysql:0.9.0
POSTGRESQL_IMAGE=cloudron/postgresql:0.8.0
MONGODB_IMAGE=cloudron/mongodb:0.8.0
REDIS_IMAGE=cloudron/redis:0.8.0 # if you change this, fix src/addons.js as well
+4 -4
View File
@@ -420,7 +420,7 @@ function setupMySql(app, options, callback) {
debugApp(app, 'Setting up mysql');
var container = docker.getContainer('mysql');
var cmd = [ '/addons/mysql/service.sh', 'add', app.id ];
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'add-prefix' : 'add', app.id ];
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
if (error) return callback(error);
@@ -453,7 +453,7 @@ function teardownMySql(app, options, callback) {
assert.strictEqual(typeof callback, 'function');
var container = docker.getContainer('mysql');
var cmd = [ '/addons/mysql/service.sh', 'remove', app.id ];
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'remove-prefix' : 'remove', app.id ];
debugApp(app, 'Tearing down mysql');
@@ -481,7 +481,7 @@ function backupMySql(app, options, callback) {
var output = fs.createWriteStream(path.join(paths.DATA_DIR, app.id, 'mysqldump'));
output.on('error', callback);
var cp = spawn('/usr/bin/docker', [ 'exec', 'mysql', '/addons/mysql/service.sh', 'backup', app.id ]);
var cp = spawn('/usr/bin/docker', [ 'exec', 'mysql', '/addons/mysql/service.sh', options.multipleDatabases ? 'backup-prefix' : 'backup', app.id ]);
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debugApp(app, 'backupMySql: done. code:%s signal:%s', code, signal);
@@ -504,7 +504,7 @@ function restoreMySql(app, options, callback) {
input.on('error', callback);
// cannot get this to work through docker.exec
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', 'mysql', '/addons/mysql/service.sh', 'restore', app.id ]);
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', 'mysql', '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', app.id ]);
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debugApp(app, 'restoreMySql: done %s %s', code, signal);
+2
View File
@@ -3,6 +3,7 @@
var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apphealthmonitor'),
docker = require('./docker.js').connection,
@@ -89,6 +90,7 @@ function checkAppHealth(app, callback) {
var healthCheckUrl = 'http://127.0.0.1:' + app.httpPort + manifest.healthCheckPath;
superagent
.get(healthCheckUrl)
.set('Host', config.appFqdn(app.location)) // required for some apache configs with rewrite rules
.redirects(0)
.timeout(HEALTHCHECK_INTERVAL)
.end(function (error, res) {
+15 -10
View File
@@ -763,21 +763,26 @@ function createNewBackup(app, addonsToBackup, callback) {
assert(!addonsToBackup || typeof addonsToBackup, 'object');
assert.strictEqual(typeof callback, 'function');
backups.getBackupUrl(app, function (error, result) {
backups.getBackupUrl(app, function (error, backupArchive) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
debugApp(app, 'backupApp: backup url:%s backup id:%s', result.url, result.id);
async.series([
ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])),
addons.backupAddons.bind(null, app, addonsToBackup),
shell.sudo.bind(null, 'backupApp', [ BACKUP_APP_CMD, app.id, result.url, result.backupKey, result.sessionToken ]),
ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])),
], function (error) {
backups.getAppBackupConfigUrl(app, function (error, backupConfig) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
callback(null, result.id);
debugApp(app, 'backupApp: backup url:%s backup config url:%s', backupArchive.url, backupConfig.url);
async.series([
ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])),
addons.backupAddons.bind(null, app, addonsToBackup),
shell.sudo.bind(null, 'backupApp', [ BACKUP_APP_CMD, app.id, backupArchive.url, backupConfig.url, backupArchive.backupKey, backupArchive.sessionToken ]),
ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])),
], function (error) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
callback(null, backupArchive.id);
});
});
});
}
+1
View File
@@ -598,6 +598,7 @@ function update(app, callback) {
debugApp(app, 'Updating to %s', safe.query(app, 'manifest.version'));
// app does not want these addons anymore
// FIXME: this does not handle option changes (like multipleDatabases)
var unusedAddons = _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
async.series([
+38 -4
View File
@@ -6,6 +6,7 @@ exports = module.exports = {
getAllPaged: getAllPaged,
getBackupUrl: getBackupUrl,
getAppBackupConfigUrl: getAppBackupConfigUrl,
getRestoreUrl: getRestoreUrl,
copyLastBackup: copyLastBackup
@@ -98,6 +99,31 @@ function getBackupUrl(app, callback) {
});
}
function getAppBackupConfigUrl(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var filename = util.format('appbackup_%s_%s-v%s.json', app.id, (new Date()).toISOString(), app.manifest.version);
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
api(backupConfig.provider).getSignedUploadUrl(backupConfig, filename, function (error, result) {
if (error) return callback(error);
var obj = {
id: filename,
url: result.url,
sessionToken: result.sessionToken
};
debug('getAppBackupConfigUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
callback(null, obj);
});
});
}
// backupId is the s3 filename. appbackup_%s_%s-v%s.tar.gz
function getRestoreUrl(backupId, callback) {
assert.strictEqual(typeof backupId, 'string');
@@ -124,19 +150,27 @@ function getRestoreUrl(backupId, callback) {
}
function copyLastBackup(app, callback) {
assert(app && typeof app === 'object');
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof app.lastBackupId, 'string');
assert.strictEqual(typeof callback, 'function');
var toFilename = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version);
var toFilenameArchive = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version);
var toFilenameConfig = util.format('appbackup_%s_%s-v%s.json', app.id, (new Date()).toISOString(), app.manifest.version);
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
api(backupConfig.provider).copyObject(backupConfig, app.lastBackupId, toFilename, function (error) {
api(backupConfig.provider).copyObject(backupConfig, app.lastBackupId, toFilenameArchive, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
return callback(null, toFilename);
// TODO change that logic by adjusting app.lastBackupId to not contain the file type
var configFileId = app.lastBackupId.slice(0, -'.tar.gz'.length) + '.json';
api(backupConfig.provider).copyObject(backupConfig, configFileId, toFilenameConfig, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
return callback(null, toFilenameArchive);
});
});
});
}
-44
View File
@@ -16,7 +16,6 @@ exports = module.exports = {
updateToLatest: updateToLatest,
update: update,
reboot: reboot,
migrate: migrate,
backup: backup,
retire: retire,
ensureBackup: ensureBackup,
@@ -460,49 +459,6 @@ function reboot(callback) {
shell.sudo('reboot', [ REBOOT_CMD ], callback);
}
function migrate(size, region, callback) {
assert.strictEqual(typeof size, 'string');
assert.strictEqual(typeof region, 'string');
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_MIGRATE);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
function unlock(error) {
if (error) {
debug('Failed to migrate', error);
locker.unlock(locker.OP_MIGRATE);
} else {
debug('Migration initiated successfully');
// do not unlock; cloudron is migrating
}
return;
}
// initiate the migration in the background
backupBoxAndApps(function (error, restoreKey) {
if (error) return unlock(error);
debug('migrate: size %s region %s restoreKey %s', size, region, restoreKey);
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
.query({ token: config.token() })
.send({ size: size, region: region, restoreKey: restoreKey })
.end(function (error, result) {
if (error && !error.response) return unlock(error);
if (result.statusCode === 409) return unlock(new CloudronError(CloudronError.BAD_STATE));
if (result.statusCode === 404) return unlock(new CloudronError(CloudronError.NOT_FOUND));
if (result.statusCode !== 202) return unlock(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
return unlock(null);
});
});
callback(null);
}
function update(boxUpdateInfo, callback) {
assert.strictEqual(typeof boxUpdateInfo, 'object');
assert.strictEqual(typeof callback, 'function');
+9 -1
View File
@@ -37,7 +37,7 @@ exports = module.exports = {
isDev: isDev,
// for testing resets to defaults
_reset: initConfig
_reset: _reset
};
var assert = require('assert'),
@@ -70,6 +70,14 @@ function saveSync() {
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(data, null, 4)); // functions are ignored by JSON.stringify
}
function _reset (callback) {
safe.fs.unlinkSync(cloudronConfigFileName);
initConfig();
if (callback) callback();
}
function initConfig() {
// setup defaults
data.fqdn = 'localhost';
+4 -1
View File
@@ -2,10 +2,13 @@
Dear Admin,
A new version of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
A new version <%= updateInfo.manifest.version %> of the app '<%= app.manifest.title %>' installed at <%= app.fqdn %> is available!
The app will update automatically tonight. Alternately, update immediately at <%= webadminUrl %>.
Changes:
<%= updateInfo.manifest.changelog %>
Thank you,
your Cloudron
+1 -1
View File
@@ -2,7 +2,7 @@
Dear Admin,
A new version of Cloudron <%= fqdn %> is available!
Version <%= newBoxVersion %> of Cloudron <%= fqdn %> is now available!
Your Cloudron will update automatically tonight. Alternately, update immediately at <%= webadminUrl %>.
+3 -3
View File
@@ -176,8 +176,8 @@ function sendMails(queue) {
function enqueue(mailOptions) {
assert.strictEqual(typeof mailOptions, 'object');
if (!mailOptions.from) console.error('from is missing');
if (!mailOptions.to) console.error('to is missing');
if (!mailOptions.from) console.error('sender address is missing');
if (!mailOptions.to) console.error('recipient address is missing');
debug('Queued mail for ' + mailOptions.from + ' to ' + mailOptions.to);
gMailQueue.push(mailOptions);
@@ -353,7 +353,7 @@ function appUpdateAvailable(app, updateInfo) {
from: config.get('adminEmail'),
to: adminEmails.join(', '),
subject: util.format('%s has a new update available', app.fqdn),
text: render('app_update_available.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), app: app, format: 'text' })
text: render('app_update_available.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), app: app, updateInfo: updateInfo, format: 'text' })
};
enqueue(mailOptions);
-15
View File
@@ -10,7 +10,6 @@ exports = module.exports = {
getProgress: getProgress,
getConfig: getConfig,
update: update,
migrate: migrate,
feedback: feedback
};
@@ -129,20 +128,6 @@ function update(req, res, next) {
});
}
function migrate(req, res, next) {
if (typeof req.body.size !== 'string') return next(new HttpError(400, 'size must be string'));
if (typeof req.body.region !== 'string') return next(new HttpError(400, 'region must be string'));
debug('Migration requested', req.body.size, req.body.region);
cloudron.migrate(req.body.size, req.body.region, function (error) {
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
function feedback(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
+1326 -1310
View File
File diff suppressed because it is too large Load Diff
+3 -176
View File
@@ -24,8 +24,8 @@ var token = null; // authentication token
var server;
function setup(done) {
nock.cleanAll();
config._reset();
config.set('version', '0.5.0');
config.set('fqdn', 'localhost');
server.start(done);
}
@@ -33,6 +33,8 @@ function cleanup(done) {
database._clear(function (error) {
expect(error).to.not.be.ok();
config._reset();
server.stop(done);
});
}
@@ -258,181 +260,6 @@ describe('Cloudron', function () {
});
describe('migrate', function () {
before(function (done) {
async.series([
setup,
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
config._reset();
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
},
function setupBackupConfig(callback) {
superagent.post(SERVER_URL + '/api/v1/settings/backup_config')
.send({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
callback();
});
}
], done);
});
after(cleanup);
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo'})
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without password', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo'})
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with missing size', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with wrong size type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 4, region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with missing region', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with wrong region type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 4, password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails when in wrong state', function (done) {
var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
var scope3 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) {
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
})
.reply(200, { id: 'someid' });
var scope1 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', function (body) {
return body.size && body.region && body.restoreKey;
}).reply(409, {});
injectShellMock();
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
restoreShellMock();
return done();
}
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
});
it('succeeds', function (done) {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', function (body) {
return body.size && body.region && body.restoreKey;
}).reply(202, {});
var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) {
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
})
.reply(200, { id: 'someid' });
var scope3 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
injectShellMock();
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
restoreShellMock();
return done();
}
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
});
});
describe('feedback', function () {
before(function (done) {
async.series([
+1 -1
View File
@@ -79,7 +79,7 @@ start_mongodb
start_mail
echo -n "Waiting for addons to start"
for i in {1..20}; do
for i in {1..10}; do
echo -n "."
sleep 1
done
+29 -6
View File
@@ -12,8 +12,8 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
exit 0
fi
if [ $# -lt 3 ]; then
echo "Usage: backupapp.sh <appid> <url> <key> [aws session token]"
if [ $# -lt 4 ]; then
echo "Usage: backupapp.sh <appid> <url> <url> <key> [aws session token]"
exit 1
fi
@@ -21,8 +21,9 @@ readonly DATA_DIR="${HOME}/data"
app_id="$1"
backup_url="$2"
backup_key="$3"
session_token="$4"
backup_config_url="$3"
backup_key="$4"
session_token="$5"
readonly now=$(date "+%Y-%m-%dT%H:%M:%S")
readonly app_data_dir="${DATA_DIR}/${app_id}"
readonly app_data_snapshot="${DATA_DIR}/snapshots/${app_id}-${now}"
@@ -48,12 +49,34 @@ for try in `seq 1 5`; do
cat "${error_log}" && rm "${error_log}"
done
if [[ ${try} -eq 5 ]]; then
echo "Backup failed uploading backup tarball"
exit 1
fi
for try in `seq 1 5`; do
echo "Uploading config.json to ${backup_config_url} (try ${try})"
error_log=$(mktemp)
headers=("-H" "Content-Type:")
# federated tokens in CaaS case need session token
if [ ! -z "$session_token" ]; then
headers=(${headers[@]} "-H" "x-amz-security-token: ${session_token}")
fi
if cat "${app_data_snapshot}/config.json" \
| curl --fail -X PUT ${headers[@]} --data @- "${backup_config_url}" 2>"${error_log}"; then
break
fi
cat "${error_log}" && rm "${error_log}"
done
btrfs subvolume delete "${app_data_snapshot}"
if [[ ${try} -eq 5 ]]; then
echo "Backup failed"
echo "Backup failed uploading config.json"
exit 1
else
echo "Backup successful"
fi
-1
View File
@@ -93,7 +93,6 @@ function initializeExpressSync() {
router.get ('/api/v1/cloudron/config', rootScope, routes.cloudron.getConfig);
router.post('/api/v1/cloudron/update', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.update);
router.post('/api/v1/cloudron/reboot', rootScope, routes.cloudron.reboot);
router.post('/api/v1/cloudron/migrate', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.migrate);
router.get ('/api/v1/cloudron/graphs', rootScope, routes.graphs.getGraphs);
// feedback
+1 -1
View File
@@ -77,7 +77,7 @@ function getSignedUploadUrl(backupConfig, filename, callback) {
var url = s3.getSignedUrl('putObject', params);
callback(null, { url : url, sessionToken: credentials.sessionToken });
callback(null, { url: url, sessionToken: credentials.sessionToken });
});
}
+22 -1
View File
@@ -4,7 +4,10 @@ exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
restartAppTask: restartAppTask
restartAppTask: restartAppTask,
stopPendingTasks: stopPendingTasks,
waitForPendingTasks: waitForPendingTasks
};
var appdb = require('./appdb.js'),
@@ -47,6 +50,24 @@ function uninitialize(callback) {
async.eachSeries(Object.keys(gActiveTasks), stopAppTask, callback);
}
function stopPendingTasks(callback) {
assert.strictEqual(typeof callback, 'function');
gPendingTasks = [];
async.eachSeries(Object.keys(gActiveTasks), stopAppTask, callback);
}
function waitForPendingTasks(callback) {
assert.strictEqual(typeof callback, 'function');
function checkTasks() {
if (Object.keys(gActiveTasks).length === 0 && gPendingTasks.length === 0) return callback();
setTimeout(checkTasks, 1000);
}
checkTasks();
}
// resume app installs and uninstalls
function resumeTasks(callback) {
+3
View File
@@ -189,6 +189,9 @@
<h4 class="modal-title">Update {{ appUpdate.app.location }}</h4>
</div>
<div class="modal-body">
<p>Recent Changes for new version <b>{{ appUpdate.manifest.version}}</b>:</p>
<pre>{{ appUpdate.manifest.changelog }}</pre>
<br/>
<fieldset>
<form class="form-signin" role="form" name="appUpdateForm" ng-submit="doUpdate(appUpdateForm)" autocomplete="off">
<div ng-repeat="(env, info) in appUpdate.portBindingsInfo" ng-class="{ 'newPort': info.isNew }">
+2 -2
View File
@@ -5,11 +5,11 @@
<div class="modal-content">
<div class="modal-header">
<img ng-src="{{appInstall.app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
<h3 class="appstore-install-title">{{ appInstall.app.manifest.title }} <span class="badge badge-danger" ng-show="appInstall.app.publishState === 'testing'">Testing</span></h3>
<h3 class="appstore-install-title" title="Version {{ appInstall.app.manifest.version }}">{{ appInstall.app.manifest.title }} <span class="badge badge-danger" ng-show="appInstall.app.publishState === 'testing'">Testing</span></h3>
<br/>
<span class="appstore-install-meta">{{ appInstall.app.manifest.author }}</span>
<br/>
<span class="appstore-install-meta">{{ appInstall.app.manifest.version }}</span>
<span class="appstore-install-meta"><a href="{{ appInstall.app.manifest.website }}" target="_blank">Website</a></span>
</div>
<div class="modal-body">
<div class="collapse" id="collapseInstallForm" data-toggle="false">