diff --git a/src/apps.js b/src/apps.js index f0d2f37c9..5dd33bbc4 100644 --- a/src/apps.js +++ b/src/apps.js @@ -12,7 +12,6 @@ exports = module.exports = { update, setHealth, del, - delPortBinding, get, getByIpAddress, @@ -192,7 +191,7 @@ const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationS 'apps.enableMailbox', 'apps.mailboxDisplayName', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableInbox', 'apps.inboxName', 'apps.inboxDomain', 'apps.enableTurn', 'apps.enableRedis', 'apps.storageVolumeId', 'apps.storageVolumePrefix', 'apps.ts', 'apps.healthTime', '(apps.icon IS NOT NULL) AS hasIcon', '(apps.appStoreIcon IS NOT NULL) AS hasAppStoreIcon' ].join(','); -// const PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(','); +// const PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId', 'count' ].join(','); const LOCATION_FIELDS = [ 'appId', 'subdomain', 'domain', 'type', 'certificateJson' ]; const CHECKVOLUME_CMD = path.join(__dirname, 'scripts/checkvolume.sh'); @@ -652,13 +651,15 @@ function postProcess(result) { const hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(','); const environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(','); const portTypes = result.portTypes === null ? [ ] : result.portTypes.split(','); + const portCounts = result.portCounts === null ? [ ] : result.portCounts.split(','); delete result.hostPorts; delete result.environmentVariables; delete result.portTypes; + delete result.portCounts; for (let i = 0; i < environmentVariables.length; i++) { - result.portBindings[environmentVariables[i]] = { hostPort: parseInt(hostPorts[i], 10), type: portTypes[i] }; + result.portBindings[environmentVariables[i]] = { hostPort: parseInt(hostPorts[i], 10), type: portTypes[i], portCount: portCounts[i] }; } assert(result.accessRestrictionJson === null || typeof result.accessRestrictionJson === 'string'); @@ -949,7 +950,6 @@ async function updateWithConstraints(id, app, constraints) { assert(!('checklist' in app) || typeof app.checklist === 'object'); assert(!('env' in app) || typeof app.env === 'object'); - const queries = [ ]; if ('portBindings' in app) { @@ -1075,14 +1075,6 @@ async function del(id) { if (results[5].affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'App not found'); } -async function delPortBinding(hostPort, type) { - assert.strictEqual(typeof hostPort, 'number'); - assert.strictEqual(typeof type, 'string'); - - const result = await database.query('DELETE FROM appPortBindings WHERE hostPort=? AND type=?', [ hostPort, type ]); - if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'App not found'); -} - async function clear() { await database.query('DELETE FROM locations'); await database.query('DELETE FROM appPortBindings'); @@ -1092,11 +1084,11 @@ async function clear() { } // each query simply join apps table with another table by id. we then join the full result together -const PB_QUERY = 'SELECT id, GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes FROM apps LEFT JOIN appPortBindings ON apps.id = appPortBindings.appId GROUP BY apps.id'; +const PB_QUERY = 'SELECT id, GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes, GROUP_CONCAT(CAST(appPortBindings.count AS CHAR(6))) AS portCounts FROM apps LEFT JOIN appPortBindings ON apps.id = appPortBindings.appId GROUP BY apps.id'; const ENV_QUERY = 'SELECT id, JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues FROM apps LEFT JOIN appEnvVars ON apps.id = appEnvVars.appId GROUP BY apps.id'; const SUBDOMAIN_QUERY = 'SELECT id, JSON_ARRAYAGG(locations.subdomain) AS subdomains, JSON_ARRAYAGG(locations.domain) AS domains, JSON_ARRAYAGG(locations.type) AS subdomainTypes, JSON_ARRAYAGG(locations.environmentVariable) AS subdomainEnvironmentVariables, JSON_ARRAYAGG(locations.certificateJson) AS subdomainCertificateJsons FROM apps LEFT JOIN locations ON apps.id = locations.appId GROUP BY apps.id'; const MOUNTS_QUERY = 'SELECT id, JSON_ARRAYAGG(appMounts.volumeId) AS volumeIds, JSON_ARRAYAGG(appMounts.readOnly) AS volumeReadOnlys FROM apps LEFT JOIN appMounts ON apps.id = appMounts.appId GROUP BY apps.id'; -const APPS_QUERY = `SELECT ${APPS_FIELDS_PREFIXED}, hostPorts, environmentVariables, portTypes, envNames, envValues, subdomains, domains, subdomainTypes, subdomainEnvironmentVariables, subdomainCertificateJsons, volumeIds, volumeReadOnlys FROM apps` +const APPS_QUERY = `SELECT ${APPS_FIELDS_PREFIXED}, hostPorts, environmentVariables, portTypes, portCounts, envNames, envValues, subdomains, domains, subdomainTypes, subdomainEnvironmentVariables, subdomainCertificateJsons, volumeIds, volumeReadOnlys FROM apps` + ` LEFT JOIN (${PB_QUERY}) AS q1 on q1.id = apps.id` + ` LEFT JOIN (${ENV_QUERY}) AS q2 on q2.id = apps.id` + ` LEFT JOIN (${SUBDOMAIN_QUERY}) AS q3 on q3.id = apps.id` @@ -2025,8 +2017,6 @@ async function updateApp(app, data, auditSource) { error = await checkManifest(manifest); if (error) throw error; - // TODO check if checklist needs to be adjusted either here or in the apptask - const updateConfig = { skipBackup, manifest, appStoreId }; // this will clear appStoreId when updating from a repo and set it if passed in for update route // prevent user from installing a app with different manifest id over an existing app diff --git a/src/apptask.js b/src/apptask.js index 740b3fb1d..d587e8b95 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -633,23 +633,42 @@ async function update(app, args, progressCallback) { await services.teardownAddons(app, unusedAddons); if (Object.keys(unusedAddons).includes('localstorage')) await updateApp(app, { storageVolumeId: null, storageVolumePrefix: null }); // lose reference - // free unused ports - const currentPorts = app.portBindings || {}; + // free unused ports. this is done after backup, so the app object is not in some inconsistent state should backup fail const newTcpPorts = updateConfig.manifest.tcpPorts || {}; const newUdpPorts = updateConfig.manifest.udpPorts || {}; + const portBindings = { ...app.portBindings }; - for (const portName of Object.keys(currentPorts)) { + for (const portName of Object.keys(portBindings)) { if (newTcpPorts[portName] || newUdpPorts[portName]) continue; // port still in use - - const [error] = await safe(apps.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP)); - if (error && error.reason === BoxError.NOT_FOUND) debug('update: portbinding does not exist in database: %o', error); - else if (error) throw error; - - // also delete from app object for further processing (the db is updated in the next step) - delete app.portBindings[portName]; + delete portBindings[portName]; } - await updateApp(app, _.pick(updateConfig, 'manifest', 'appStoreId', 'memoryLimit')); // switch over to the new config + // clear aliasDomains if needed based multiDomain change + const aliasDomains = app.manifest.multiDomain && !updateConfig.manifest.multiDomain ? [] : app.aliasDomains; + + // clear unused secondaryDomains + const secondaryDomains = [ ...app.secondaryDomains ]; + const newHttpPorts = updateConfig.manifest.httpPorts || {}; + for (let i = secondaryDomains.length-1; i >= 0; i--) { + const { environmentVariable } = secondaryDomains[i]; + if (environmentVariable in newHttpPorts) continue; // domain still in use + secondaryDomains.splice(i, 1); // remove domain + } + + const values = { + manifest: updateConfig.manifest, + appStoreId: updateConfig.appStoreId, + memoryLimit: updateConfig.memoryLimit, + portBindings, + // all domains have to be updated together + subdomain: app.subdomain, + domain: app.domain, + aliasDomains, + secondaryDomains, + redirectDomains: app.redirectDomains + }; + + await updateApp(app, values); // switch over to the new config await progressCallback({ percent: 45, message: 'Downloading icon' }); await downloadIcon(app);