diff --git a/src/addons.js b/src/addons.js index 7c0632f15..52cbca744 100644 --- a/src/addons.js +++ b/src/addons.js @@ -5,6 +5,7 @@ exports = module.exports = { teardownAddons: teardownAddons, backupAddons: backupAddons, restoreAddons: restoreAddons, + clearAddons: clearAddons, getEnvironment: getEnvironment, getMountsSync: getMountsSync, @@ -49,73 +50,85 @@ var KNOWN_ADDONS = { setup: setupEmail, teardown: teardownEmail, backup: NOOP, - restore: setupEmail + restore: setupEmail, + clear: NOOP }, ldap: { setup: setupLdap, teardown: teardownLdap, backup: NOOP, - restore: setupLdap + restore: setupLdap, + clear: NOOP }, localstorage: { setup: setupLocalStorage, // docker creates the directory for us teardown: teardownLocalStorage, backup: NOOP, // no backup because it's already inside app data - restore: NOOP + restore: NOOP, + clear: clearLocalStorage }, mongodb: { setup: setupMongoDb, teardown: teardownMongoDb, backup: backupMongoDb, - restore: restoreMongoDb + restore: restoreMongoDb, + clear: clearMongodb }, mysql: { setup: setupMySql, teardown: teardownMySql, backup: backupMySql, restore: restoreMySql, + clear: clearMySql }, oauth: { setup: setupOauth, teardown: teardownOauth, backup: NOOP, - restore: setupOauth + restore: setupOauth, + clear: NOOP }, postgresql: { setup: setupPostgreSql, teardown: teardownPostgreSql, backup: backupPostgreSql, - restore: restorePostgreSql + restore: restorePostgreSql, + clear: clearPostgreSql }, recvmail: { setup: setupRecvMail, teardown: teardownRecvMail, backup: NOOP, - restore: setupRecvMail + restore: setupRecvMail, + clear: NOOP }, redis: { setup: setupRedis, teardown: teardownRedis, backup: backupRedis, - restore: setupRedis // same thing + restore: setupRedis, // same thing + clear: clearRedis }, sendmail: { setup: setupSendMail, teardown: teardownSendMail, backup: NOOP, - restore: setupSendMail + restore: setupSendMail, + clear: NOOP }, scheduler: { setup: NOOP, teardown: NOOP, backup: NOOP, - restore: NOOP + restore: NOOP, + clear: NOOP }, docker: { setup: NOOP, teardown: NOOP, backup: NOOP, - restore: NOOP + restore: NOOP, + clear: NOOP } }; @@ -179,6 +192,24 @@ function backupAddons(app, addons, callback) { }, callback); } +function clearAddons(app, addons, callback) { + assert.strictEqual(typeof app, 'object'); + assert(!addons || typeof addons === 'object'); + assert.strictEqual(typeof callback, 'function'); + + debugApp(app, 'clearAddons'); + + if (!addons) return callback(null); + + debugApp(app, 'clearAddons: clearing %j', Object.keys(addons)); + + async.eachSeries(Object.keys(addons), function iterator (addon, iteratorCallback) { + if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon)); + + KNOWN_ADDONS[addon].clear(app, addons[addon], iteratorCallback); + }, callback); +} + function restoreAddons(app, addons, callback) { assert.strictEqual(typeof app, 'object'); assert(!addons || typeof addons === 'object'); @@ -267,6 +298,16 @@ function setupLocalStorage(app, options, callback) { docker.createVolume(app, `${app.id}-localstorage`, 'data', callback); } +function clearLocalStorage(app, options, callback) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof options, 'object'); + assert.strictEqual(typeof callback, 'function'); + + debugApp(app, 'clearLocalStorage'); + + docker.clearVolume(app, `${app.id}-localstorage`, 'data', callback); +} + function teardownLocalStorage(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); @@ -525,6 +566,23 @@ function setupMySql(app, options, callback) { }); } +function clearMySql(app, options, callback) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof options, 'object'); + assert.strictEqual(typeof callback, 'function'); + + const dbname = mysqlDatabaseName(app.id); + var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'clear-prefix' : 'clear', dbname ]; // FIXME: clear-prefix does not exist! + + debugApp(app, 'Clearing mysql'); + + docker.execContainer('mysql', cmd, { }, function (error) { + if (error) return callback(error); + + callback(); + }); +} + function teardownMySql(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); @@ -567,18 +625,14 @@ function restoreMySql(app, options, callback) { callback = once(callback); // ChildProcess exit may or may not be called after error - setupMySql(app, options, function (error) { - if (error) return callback(error); + debugApp(app, 'restoreMySql'); - debugApp(app, 'restoreMySql'); + var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'mysqldump')); + input.on('error', callback); - var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'mysqldump')); - input.on('error', callback); - - const dbname = mysqlDatabaseName(app.id); - var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', dbname ]; - docker.execContainer('mysql', cmd, { stdin: input }, callback); - }); + const dbname = mysqlDatabaseName(app.id); + var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', dbname ]; + docker.execContainer('mysql', cmd, { stdin: input }, callback); } function setupPostgreSql(app, options, callback) { @@ -614,6 +668,24 @@ function setupPostgreSql(app, options, callback) { }); } +function clearPostgreSql(app, options, callback) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof options, 'object'); + assert.strictEqual(typeof callback, 'function'); + + const appId = app.id.replace(/-/g, ''); + + var cmd = [ '/addons/postgresql/service.sh', 'clear', appId ]; + + debugApp(app, 'Clearing postgresql'); + + docker.execContainer('postgresql', cmd, { }, function (error) { + if (error) return callback(error); + + callback(); + }); +} + function teardownPostgreSql(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); @@ -657,19 +729,15 @@ function restorePostgreSql(app, options, callback) { callback = once(callback); - setupPostgreSql(app, options, function (error) { - if (error) return callback(error); + debugApp(app, 'restorePostgreSql'); - debugApp(app, 'restorePostgreSql'); + var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'postgresqldump')); + input.on('error', callback); - var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'postgresqldump')); - input.on('error', callback); + const appId = app.id.replace(/-/g, ''); + var cmd = [ '/addons/postgresql/service.sh', 'restore', appId ]; - const appId = app.id.replace(/-/g, ''); - var cmd = [ '/addons/postgresql/service.sh', 'restore', appId ]; - - docker.execContainer('postgresql', cmd, { stdin: input }, callback); - }); + docker.execContainer('postgresql', cmd, { stdin: input }, callback); } function getAddonDetails(containerName, tokenEnvName, callback) { @@ -735,6 +803,25 @@ function setupMongoDb(app, options, callback) { }); } +function clearMongodb(app, options, callback) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof options, 'object'); + assert.strictEqual(typeof callback, 'function'); + + debugApp(app, 'Clearing mongodb'); + + getAddonDetails('mongodb', 'MONGODB_CLOUDRON_TOKEN', function (error, result) { + if (error) return callback(error); + + request.post(`https://${result.ip}:3000/databases/${app.id}/clear?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response, body) { + if (error) return callback(new Error('Error clearing mongodb: ' + error)); + if (response.statusCode !== 200) return callback(new Error(`Error clearing mongodb. Status code: ${response.statusCode}`)); + + callback(); + }); + }); +} + function teardownMongoDb(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); @@ -786,26 +873,22 @@ function restoreMongoDb(app, options, callback) { callback = once(callback); // protect from multiple returns with streams - setupMongoDb(app, options, function (error) { + debugApp(app, 'restoreMongoDb'); + + getAddonDetails('mongodb', 'MONGODB_CLOUDRON_TOKEN', function (error, result) { if (error) return callback(error); - debugApp(app, 'restoreMongoDb'); + const readStream = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'mongodbdump')); + readStream.on('error', callback); - getAddonDetails('mongodb', 'MONGODB_CLOUDRON_TOKEN', function (error, result) { + const restoreReq = request.post(`https://${result.ip}:3000/databases/${app.id}/restore?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response, body) { if (error) return callback(error); + if (response.statusCode !== 200) return callback(new Error(`Unexpected response from mongodb addon ${response.statusCode}`)); - const readStream = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'mongodbdump')); - readStream.on('error', callback); - - const restoreReq = request.post(`https://${result.ip}:3000/databases/${app.id}/restore?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response, body) { - if (error) return callback(error); - if (response.statusCode !== 200) return callback(new Error(`Unexpected response from mongodb addon ${response.statusCode}`)); - - callback(null); - }); - - readStream.pipe(restoreReq); + callback(null); }); + + readStream.pipe(restoreReq); }); } @@ -873,6 +956,25 @@ function setupRedis(app, options, callback) { }); } +function clearRedis(app, options, callback) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof options, 'object'); + assert.strictEqual(typeof callback, 'function'); + + debugApp(app, 'Clearing redis'); + + getAddonDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) { + if (error) return callback(error); + + request.post(`https://${result.ip}:3000/clear?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response, body) { + if (error) return callback(new Error('Error clearing redis: ' + error)); + if (response.statusCode !== 201) return callback(new Error(`Error clearing redis. Status code: ${response.statusCode}`)); + + callback(null); + }); + }); +} + function teardownRedis(app, options, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); diff --git a/src/apptask.js b/src/apptask.js index b9e748786..67f702a33 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -172,21 +172,23 @@ function deleteAppDir(app, options, callback) { assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); - // remove symlinked directory contents and not the symlink - const volumeDir = path.join(paths.APPS_DATA_DIR, app.id); - const isSymlinked = safe.fs.readlinkSync(volumeDir) !== volumeDir; + const appDataDir = path.join(paths.APPS_DATA_DIR, app.id); + let resolvedAppDataDir = safe.fs.readlinkSync(appDataDir) || appDataDir; - // only remove folder contents if removeDirectory is falsy and we do have a symlink - const subdir = options.removeDirectory || isSymlinked ? '' : '*'; + if (!safe.fs.existsSync(resolvedAppDataDir)) return callback(); - docker.removeVolume(app, app.id, subdir, function (error) { - if (error) { - debug(`deleteVolume: error removing ${volumeDir} (symlink ${isSymlinked}): ${error}`); - return callback(error); - } + const dirents = safe.fs.readdirSync(resolvedAppDataDir, { withFileTypes: true }); + if (!dirents) return callback(`Error listing ${resolvedAppDataDir}: ${safe.error.message}`); - callback(); + // remove only files. directories inside app dir are currently volumes managed by the addons + dirents.forEach(function (dirent) { + if (!dirent.isDirectory()) safe.fs.unlinkSync(path.join(resolvedAppDataDir, name)); }); + + // if this fails, it's probably because the localstorage/redis addons have not cleaned up properly + if (options.removeDirectory && !safe.fs.rmdirSync(appDataDir)) return callback(safe.error.code === 'ENOENT' ? null : safe.error); + + callback(null); } function addCollectdProfile(app, callback) { @@ -473,9 +475,7 @@ function install(app, callback) { deleteMainContainer.bind(null, app), function teardownAddons(next) { // when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords - var addonsToRemove = !isRestoring - ? app.manifest.addons - : _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons)); + var addonsToRemove = !isRestoring ? app.manifest.addons : _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons)); addons.teardownAddons(app, addonsToRemove, next); }, @@ -511,6 +511,8 @@ function install(app, callback) { } else { async.series([ updateApp.bind(null, app, { installationProgress: '60, Download backup and restoring addons' }), + addons.setupAddons.bind(null, app, app.manifest.addons), + addons.clearAddons.bind(null, app, app.manifest.addons), backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig), ], next); } diff --git a/src/docker.js b/src/docker.js index 05d1f2d78..a91a8395c 100644 --- a/src/docker.js +++ b/src/docker.js @@ -18,7 +18,8 @@ exports = module.exports = { inspectByName: inspect, execContainer: execContainer, createVolume: createVolume, - removeVolume: removeVolume + removeVolume: removeVolume, + clearVolume: clearVolume }; function connectionInstance() { @@ -469,6 +470,15 @@ function createVolume(app, name, subdir, callback) { }); } +function clearVolume(app, name, subdir, callback) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof name, 'string'); + assert.strictEqual(typeof subdir, 'string'); + assert.strictEqual(typeof callback, 'function'); + + shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], callback); +} + function removeVolume(app, name, subdir, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof name, 'string');