diff --git a/src/infra_version.js b/src/infra_version.js index 72709b182..b221d4903 100644 --- a/src/infra_version.js +++ b/src/infra_version.js @@ -19,7 +19,7 @@ exports = module.exports = { 'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:3.0.7@sha256:6679c2fb96f8d6d62349b607748570640a90fc46b50aad80ca2c0161655d07f4' }, 'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:4.1.1@sha256:86e4e2f4fd43809efca7c9cb1def4d7608cf36cb9ea27052f9b64da4481db43a' }, 'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:4.0.2@sha256:9df297ccc3370f38c54f8d614e214e082b363777cd1c6c9522e29663cc8f5362' }, - 'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:3.0.4@sha256:5c60de75d078ae609da5565f32dcd91030f45907e945756cc976ff207b8c6199' }, + 'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:3.1.0@sha256:794c886d856ec3e7f5d097b067b640c77361d1e1d7ec8321fc4fe7e33e7d8583' }, 'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:3.6.0@sha256:f67c3659d5122153f746f009e3a61a49619f95b838f357fcdf02024276252036' }, 'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:3.0.1@sha256:bed9f6b5d06fe2c5289e895e806cfa5b74ad62993d705be55d4554a67d128029' }, 'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:3.4.2@sha256:810306478c3dac7caa7497e5f6381cc7ce2f68aafda849a4945d39a67cc04bc1' } diff --git a/src/services.js b/src/services.js index 61bd0b580..6d7fd52a8 100644 --- a/src/services.js +++ b/src/services.js @@ -1,5 +1,7 @@ 'use strict'; +const { pipeline } = require('stream'); + exports = module.exports = { listServices, getServiceStatus, @@ -43,6 +45,7 @@ const addonConfigs = require('./addonconfigs.js'), eventlog = require('./eventlog.js'), fs = require('fs'), hat = require('./hat.js'), + http = require('http'), infra = require('./infra_version.js'), mail = require('./mail.js'), once = require('once'), @@ -590,7 +593,12 @@ async function waitForContainer(containerName, tokenEnvName) { const result = await getContainerDetails(containerName, tokenEnvName); await promiseRetry({ times: 10, interval: 15000, debug }, async () => { - const [networkError, response] = await safe(superagent.get(`https://${result.ip}:3000/healthcheck?access_token=${result.token}`) + // temporary workaround till we move all containers to http + const url = containerName.includes('redis') + ? `http://${result.ip}:3000/healthcheck?access_token=${result.token}` + : `https://${result.ip}:3000/healthcheck?access_token=${result.token}`; + + const [networkError, response] = await safe(superagent.get(url) .timeout(5000) .disableTLSCerts() .ok(() => true)); @@ -1266,6 +1274,51 @@ async function pipeRequestToFile(url, filename) { }); } +async function pipeRequestToFile2(url, filename) { + assert.strictEqual(typeof url, 'string'); + assert.strictEqual(typeof filename, 'string'); + + return new Promise((resolve, reject) => { + const writeStream = fs.createWriteStream(filename); + const request = http.request(url, { method: 'POST' }); // ClientRequest + request.setTimeout(60000, () => request.destroy(new Error('Request timedout'))); // connect OR post-connect idle timeout + + request.on('error', (error) => reject(new BoxError(BoxError.NETWORK_ERROR, `Could not pipe ${url} to ${filename}: ${error.message}`))); // network error, dns error + request.on('response', (response) => { + if (response.statusCode !== 200) return reject(new BoxError(BoxError.ADDONS_ERROR, `Unexpected response code or HTTP error when piping ${url} to ${filename}: status ${response.statusCode}`)); + + pipeline(response, writeStream, (error) => { + if (error) return reject(new BoxError(BoxError.ADDONS_ERROR, `Error piping ${url} to ${filename}: ${error.message}`)); + + if (!response.complete) return reject(new BoxError(BoxError.ADDONS_ERROR, `Response not complete when piping ${url} to ${filename}`)); + resolve(); + }); + }); + request.end(); // make the request + }); +} + +async function pipeFileToRequest(filename, url) { + assert.strictEqual(typeof filename, 'string'); + assert.strictEqual(typeof url, 'string'); + + return new Promise((resolve, reject) => { + const readStream = fs.createReadStream(filename); + const request = http.request(url, { method: 'POST' }); // ClientRequest + request.setTimeout(60000, () => request.destroy(new Error('Request timedout'))); // connect OR post-connect idle timeout + request.on('response', (response) => { + response.resume(); // drain the response + if (response.statusCode !== 200) return reject(new BoxError(BoxError.ADDONS_ERROR, `Unexpected response code or HTTP error when piping ${filename} to ${url}: status ${response.statusCode} complete ${response.complete}`)); + + resolve(); + }); + + pipeline(readStream, request, function (error) { + if (error) return reject(new BoxError(BoxError.ADDONS_ERROR, `Error piping file ${filename} to request ${url}`)); + }); + }); +} + async function backupMySql(app, options) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); @@ -1800,8 +1853,7 @@ async function clearRedis(app, options) { const result = await getContainerDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN'); - const [networkError, response] = await safe(superagent.post(`https://${result.ip}:3000/clear?access_token=${result.token}`) - .disableTLSCerts() + const [networkError, response] = await safe(superagent.post(`http://${result.ip}:3000/clear?access_token=${result.token}`) .ok(() => true)); if (networkError) throw new BoxError(BoxError.ADDONS_ERROR, `Network error clearing redis: ${networkError.message}`); @@ -1830,9 +1882,7 @@ async function backupRedis(app, options) { debug('Backing up redis'); const result = await getContainerDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN'); - - const url = `https://${result.ip}:3000/backup?access_token=${result.token}`; - await pipeRequestToFile(url, dumpPath('redis', app.id)); + await pipeRequestToFile2(`http://${result.ip}:3000/backup?access_token=${result.token}`, dumpPath('redis', app.id)); } async function restoreRedis(app, options) { @@ -1842,28 +1892,7 @@ async function restoreRedis(app, options) { debug('Restoring redis'); const result = await getContainerDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN'); - - await new Promise((resolve, reject) => { - reject = once(reject); // protect from multiple returns with streams - - let input; - const newDumpLocation = dumpPath('redis', app.id); - if (fs.existsSync(newDumpLocation)) { - input = fs.createReadStream(newDumpLocation); - } else { // old location of dumps - input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'redis/dump.rdb')); - } - input.on('error', (error) => reject(new BoxError(BoxError.FS_ERROR, `Error reading input stream when restoring redis: ${error.message}`))); - - const restoreReq = request.post(`https://${result.ip}:3000/restore?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { - if (error) return reject(new BoxError(BoxError.ADDONS_ERROR, `Error restoring redis: ${error.message}`)); - if (response.statusCode !== 200) return reject(new BoxError(BoxError.ADDONS_ERROR, `Error restoring redis. Status code: ${response.statusCode} message: ${response.body.message}`)); - - resolve(); - }); - - input.pipe(restoreReq); - }); + await pipeFileToRequest(dumpPath('redis', app.id), `http://${result.ip}:3000/restore?access_token=${result.token}`); } async function statusTurn() {