apps: rework portBindings
ports is REST API input . Map of env var to the host port portBinding is the database structure. Map of env var to host port, count, type etc also, rename portCount -> count in various places to keep things consistent
This commit is contained in:
110
src/apps.js
110
src/apps.js
@@ -134,11 +134,10 @@ exports = module.exports = {
|
||||
|
||||
// exported for testing
|
||||
_checkForPortBindingConflict: checkForPortBindingConflict,
|
||||
_validatePortBindings: validatePortBindings,
|
||||
_validatePorts: validatePorts,
|
||||
_validateAccessRestriction: validateAccessRestriction,
|
||||
_validateUpstreamUri: validateUpstreamUri,
|
||||
_validateLocations: validateLocations,
|
||||
_translatePortBindings: translatePortBindings,
|
||||
_parseCrontab: parseCrontab,
|
||||
_clear: clear
|
||||
};
|
||||
@@ -196,8 +195,9 @@ const LOCATION_FIELDS = [ 'appId', 'subdomain', 'domain', 'type', 'certificateJs
|
||||
|
||||
const CHECKVOLUME_CMD = path.join(__dirname, 'scripts/checkvolume.sh');
|
||||
|
||||
function validatePortBindings(portBindings, manifest) {
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
// ports is a map of envvar -> hostPort
|
||||
function validatePorts(ports, manifest) {
|
||||
assert.strictEqual(typeof ports, 'object');
|
||||
assert.strictEqual(typeof manifest, 'object');
|
||||
|
||||
// keep the public ports in sync with firewall rules in setup/start/cloudron-firewall.sh
|
||||
@@ -236,45 +236,48 @@ function validatePortBindings(portBindings, manifest) {
|
||||
853 // dns over tls
|
||||
];
|
||||
|
||||
if (!portBindings) return null;
|
||||
if (!ports) return null;
|
||||
|
||||
const tcpPorts = manifest.tcpPorts || {};
|
||||
const udpPorts = manifest.udpPorts || {};
|
||||
|
||||
for (const portName in portBindings) {
|
||||
if (!/^[A-Z0-9_]+$/.test(portName)) return new BoxError(BoxError.BAD_FIELD, `${portName} is not a valid environment variable in portBindings`);
|
||||
for (const portName in ports) {
|
||||
if (!/^[A-Z0-9_]+$/.test(portName)) return new BoxError(BoxError.BAD_FIELD, `${portName} is not a valid environment variable in ports`);
|
||||
|
||||
const hostPort = portBindings[portName];
|
||||
if (!Number.isInteger(hostPort)) return new BoxError(BoxError.BAD_FIELD, `${hostPort} is not an integer in ${portName} portBindings`);
|
||||
if (RESERVED_PORTS.indexOf(hostPort) !== -1) return new BoxError(BoxError.BAD_FIELD, `Port ${hostPort} for ${portName} is reserved in portBindings`);
|
||||
if (RESERVED_PORT_RANGES.find(range => (hostPort >= range[0] && hostPort <= range[1]))) return new BoxError(BoxError.BAD_FIELD, `Port ${hostPort} for ${portName} is reserved in portBindings`);
|
||||
if (ALLOWED_PORTS.indexOf(hostPort) === -1 && (hostPort <= 1023 || hostPort > 65535)) return new BoxError(BoxError.BAD_FIELD, `${hostPort} for ${portName} is not in permitted range in portBindings`);
|
||||
const hostPort = ports[portName];
|
||||
if (!Number.isInteger(hostPort)) return new BoxError(BoxError.BAD_FIELD, `${hostPort} is not an integer in ${portName} ports`);
|
||||
if (RESERVED_PORTS.indexOf(hostPort) !== -1) return new BoxError(BoxError.BAD_FIELD, `Port ${hostPort} for ${portName} is reserved in ports`);
|
||||
if (RESERVED_PORT_RANGES.find(range => (hostPort >= range[0] && hostPort <= range[1]))) return new BoxError(BoxError.BAD_FIELD, `Port ${hostPort} for ${portName} is reserved in ports`);
|
||||
if (ALLOWED_PORTS.indexOf(hostPort) === -1 && (hostPort <= 1023 || hostPort > 65535)) return new BoxError(BoxError.BAD_FIELD, `${hostPort} for ${portName} is not in permitted range in ports`);
|
||||
|
||||
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies the service is disabled
|
||||
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and ports. missing values implies the service is disabled
|
||||
const portSpec = tcpPorts[portName] || udpPorts[portName];
|
||||
if (!portSpec) return new BoxError(BoxError.BAD_FIELD, `Invalid portBinding ${portName}`);
|
||||
if (portSpec.readOnly && portSpec.defaultValue !== hostPort) return new BoxError(BoxError.BAD_FIELD, `portBinding ${portName} is readOnly and cannot have a different value that the default`);
|
||||
if ((hostPort + (portSpec.portCount || 1)) > 65535) return new BoxError(BoxError.BAD_FIELD, `${hostPort}+${portSpec.portCount} for ${portName} exceeds valid port range`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function translatePortBindings(portBindings, manifest) {
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
// translates the REST API ports (envvar -> hostPort) to database portBindings (envvar -> { hostPort, count, type })
|
||||
function translateToPortBindings(ports, manifest) {
|
||||
assert.strictEqual(typeof ports, 'object');
|
||||
assert.strictEqual(typeof manifest, 'object');
|
||||
|
||||
if (!portBindings) return null;
|
||||
const portBindings = {};
|
||||
|
||||
const result = {};
|
||||
const tcpPorts = manifest.tcpPorts || { };
|
||||
if (!ports) return portBindings;
|
||||
|
||||
for (const portName in portBindings) {
|
||||
const tcpPorts = manifest.tcpPorts || {};
|
||||
|
||||
for (const portName in ports) {
|
||||
const portType = portName in tcpPorts ? exports.PORT_TYPE_TCP : exports.PORT_TYPE_UDP;
|
||||
const portCount = portBindings[portName].portCount || (portName in tcpPorts ? manifest.tcpPorts[portName].portCount : manifest.udpPorts[portName].portCount);
|
||||
result[portName] = { hostPort: portBindings[portName], type: portType, portCount: portCount || 1 };
|
||||
const portCount = portName in tcpPorts ? tcpPorts[portName].portCount : manifest.udpPorts[portName].portCount; // since count is optional, this can be undefined
|
||||
portBindings[portName] = { hostPort: ports[portName], type: portType, count: portCount || 1 };
|
||||
}
|
||||
|
||||
return result;
|
||||
return portBindings;
|
||||
}
|
||||
|
||||
function validateSecondaryDomains(secondaryDomains, manifest) {
|
||||
@@ -543,9 +546,8 @@ function getDuplicateErrorDetails(errorMessage, locations, portBindings) {
|
||||
}
|
||||
}
|
||||
|
||||
// check if any of the port bindings conflict
|
||||
for (const portName in portBindings) {
|
||||
if (portBindings[portName] === parseInt(match[1])) return new BoxError(BoxError.ALREADY_EXISTS, `Port ${match[1]} is in use`);
|
||||
if (portBindings[portName].hostPort === parseInt(match[1])) return new BoxError(BoxError.ALREADY_EXISTS, `Port ${match[1]} is in use`);
|
||||
}
|
||||
|
||||
if (match[2] === 'apps_storageVolume') {
|
||||
@@ -647,7 +649,7 @@ function postProcess(result) {
|
||||
assert(result.hostPorts === null || typeof result.hostPorts === 'string');
|
||||
assert(result.environmentVariables === null || typeof result.environmentVariables === 'string');
|
||||
|
||||
result.portBindings = { };
|
||||
result.portBindings = {};
|
||||
const hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(',');
|
||||
const environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(',');
|
||||
const portTypes = result.portTypes === null ? [ ] : result.portTypes.split(',');
|
||||
@@ -659,7 +661,7 @@ function postProcess(result) {
|
||||
delete result.portCounts;
|
||||
|
||||
for (let i = 0; i < environmentVariables.length; i++) {
|
||||
result.portBindings[environmentVariables[i]] = { hostPort: parseInt(hostPorts[i], 10), type: portTypes[i], portCount: parseInt(portCounts[i], 10) };
|
||||
result.portBindings[environmentVariables[i]] = { hostPort: parseInt(hostPorts[i], 10), type: portTypes[i], count: parseInt(portCounts[i], 10) };
|
||||
}
|
||||
|
||||
assert(result.accessRestrictionJson === null || typeof result.accessRestrictionJson === 'string');
|
||||
@@ -740,15 +742,11 @@ function postProcess(result) {
|
||||
result.taskId = result.taskId ? String(result.taskId) : null;
|
||||
}
|
||||
|
||||
// attaches computed properties
|
||||
function attachProperties(app, domainObjectMap) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof domainObjectMap, 'object');
|
||||
|
||||
const result = {};
|
||||
for (const portName in app.portBindings) {
|
||||
result[portName] = app.portBindings[portName].hostPort;
|
||||
}
|
||||
app.portBindings = result;
|
||||
app.iconUrl = app.hasIcon || app.hasAppStoreIcon ? `/api/v1/apps/${app.id}/icon` : null;
|
||||
app.fqdn = dns.fqdn(app.subdomain, app.domain);
|
||||
app.secondaryDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, ad.domain); });
|
||||
@@ -804,26 +802,26 @@ async function checkForPortBindingConflict(portBindings, options) {
|
||||
|
||||
if (existingPortBindings.length === 0) return;
|
||||
|
||||
const tcpPorts = existingPortBindings.filter((p) => p.type === 'tcp');
|
||||
const udpPorts = existingPortBindings.filter((p) => p.type === 'udp');
|
||||
const tcpPortBindings = existingPortBindings.filter((p) => p.type === 'tcp');
|
||||
const udpPortBindings = existingPortBindings.filter((p) => p.type === 'udp');
|
||||
|
||||
for (const portName in portBindings) {
|
||||
const p = portBindings[portName];
|
||||
const testPorts = p.type === 'tcp' ? tcpPorts : udpPorts;
|
||||
const portBinding = portBindings[portName];
|
||||
const existingPortBinding = portBinding.type === 'tcp' ? tcpPortBindings : udpPortBindings;
|
||||
|
||||
const found = testPorts.find((e) => {
|
||||
const found = existingPortBinding.find((epb) => {
|
||||
// if one is true we dont have a conflict
|
||||
// a1 <----> a2 b1 <-------> b2
|
||||
// b1 <------> b2 a1 <-----> a2
|
||||
const a2 = (e.hostPort + e.count - 1);
|
||||
const b1 = p.hostPort;
|
||||
const b2 = (p.hostPort + p.portCount -1);
|
||||
const a1 = e.hostPort;
|
||||
const a2 = (epb.hostPort + epb.count - 1);
|
||||
const b1 = portBinding.hostPort;
|
||||
const b2 = (portBinding.hostPort + portBinding.count - 1);
|
||||
const a1 = epb.hostPort;
|
||||
|
||||
return !((a2 < b1) || (b2 < a1));
|
||||
});
|
||||
|
||||
if (found) throw new BoxError(BoxError.CONFLICT, `Conflicting port ${p.hostPort}`);
|
||||
if (found) throw new BoxError(BoxError.CONFLICT, `Conflicting ${portBinding.type} port ${portBinding.hostPort}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -834,11 +832,9 @@ async function add(id, appStoreId, manifest, subdomain, domain, portBindings, da
|
||||
assert.strictEqual(typeof manifest.version, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert(portBindings && typeof portBindings === 'object');
|
||||
assert(data && typeof data === 'object');
|
||||
|
||||
portBindings = portBindings || { };
|
||||
|
||||
const manifestJson = JSON.stringify(manifest),
|
||||
accessRestriction = data.accessRestriction || null,
|
||||
accessRestrictionJson = JSON.stringify(accessRestriction),
|
||||
@@ -863,7 +859,7 @@ async function add(id, appStoreId, manifest, subdomain, domain, portBindings, da
|
||||
enableRedis = 'enableRedis' in data ? data.enableRedis : true,
|
||||
icon = data.icon || null;
|
||||
|
||||
await checkForPortBindingConflict(portBindings, {});
|
||||
await checkForPortBindingConflict(portBindings, { appId: null });
|
||||
|
||||
const queries = [];
|
||||
|
||||
@@ -885,7 +881,7 @@ async function add(id, appStoreId, manifest, subdomain, domain, portBindings, da
|
||||
Object.keys(portBindings).forEach(function (env) {
|
||||
queries.push({
|
||||
query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, type, appId, count) VALUES (?, ?, ?, ?, ?)',
|
||||
args: [ env, portBindings[env].hostPort, portBindings[env].type, id, portBindings[env].portCount ]
|
||||
args: [ env, portBindings[env].hostPort, portBindings[env].type, id, portBindings[env].count ]
|
||||
});
|
||||
});
|
||||
|
||||
@@ -953,14 +949,14 @@ async function updateWithConstraints(id, app, constraints) {
|
||||
const queries = [ ];
|
||||
|
||||
if ('portBindings' in app) {
|
||||
const portBindings = app.portBindings || { };
|
||||
const portBindings = app.portBindings;
|
||||
|
||||
await checkForPortBindingConflict(portBindings, { appId: id });
|
||||
|
||||
// replace entries by app id
|
||||
queries.push({ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] });
|
||||
Object.keys(portBindings).forEach(function (env) {
|
||||
const values = [ portBindings[env].hostPort, portBindings[env].type, env, id, portBindings[env].portCount ];
|
||||
const values = [ portBindings[env].hostPort, portBindings[env].type, env, id, portBindings[env].count ];
|
||||
queries.push({ query: 'INSERT INTO appPortBindings (hostPort, type, environmentVariable, appId, count) VALUES(?, ?, ?, ?, ?)', args: values });
|
||||
});
|
||||
}
|
||||
@@ -1342,9 +1338,9 @@ async function install(data, auditSource) {
|
||||
error = await checkManifest(manifest);
|
||||
if (error) throw error;
|
||||
|
||||
error = validatePortBindings(data.portBindings || null, manifest);
|
||||
error = validatePorts(data.ports || null, manifest);
|
||||
if (error) throw error;
|
||||
const portBindings = translatePortBindings(data.portBindings || null, manifest);
|
||||
const portBindings = translateToPortBindings(data.ports || null, manifest);
|
||||
|
||||
error = validateAccessRestriction(accessRestriction);
|
||||
if (error) throw error;
|
||||
@@ -1906,16 +1902,16 @@ async function setLocation(app, data, auditSource) {
|
||||
subdomain: data.subdomain.toLowerCase(),
|
||||
domain: data.domain.toLowerCase(),
|
||||
// these are intentionally reset, if not set
|
||||
portBindings: null,
|
||||
portBindings: {},
|
||||
secondaryDomains: [],
|
||||
redirectDomains: [],
|
||||
aliasDomains: []
|
||||
};
|
||||
|
||||
if ('portBindings' in data) {
|
||||
error = validatePortBindings(data.portBindings || null, app.manifest);
|
||||
if ('ports' in data) {
|
||||
error = validatePorts(data.ports || null, app.manifest);
|
||||
if (error) throw error;
|
||||
values.portBindings = translatePortBindings(data.portBindings || null, app.manifest);
|
||||
values.portBindings = translateToPortBindings(data.ports || null, app.manifest);
|
||||
}
|
||||
|
||||
// rename the auto-created mailbox to match the new location
|
||||
@@ -1954,7 +1950,7 @@ async function setLocation(app, data, auditSource) {
|
||||
};
|
||||
const [taskError, taskId] = await safe(addTask(appId, exports.ISTATE_PENDING_LOCATION_CHANGE, task, auditSource));
|
||||
if (taskError && taskError.reason !== BoxError.ALREADY_EXISTS) throw taskError;
|
||||
if (taskError && taskError.reason === BoxError.ALREADY_EXISTS) throw getDuplicateErrorDetails(taskError.message, locations, data.portBindings);
|
||||
if (taskError && taskError.reason === BoxError.ALREADY_EXISTS) throw getDuplicateErrorDetails(taskError.message, locations, values.portBindings);
|
||||
|
||||
values.fqdn = dns.fqdn(values.subdomain, values.domain);
|
||||
values.secondaryDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, ad.domain); });
|
||||
@@ -2359,9 +2355,9 @@ async function clone(app, data, user, auditSource) {
|
||||
error = await checkManifest(manifest);
|
||||
if (error) throw error;
|
||||
|
||||
error = validatePortBindings(data.portBindings || null, manifest);
|
||||
error = validatePorts(data.ports || null, manifest);
|
||||
if (error) throw error;
|
||||
const portBindings = translatePortBindings(data.portBindings || null, manifest);
|
||||
const portBindings = translateToPortBindings(data.ports || null, manifest);
|
||||
|
||||
// should we copy the original app's mailbox settings instead?
|
||||
const mailboxName = manifest.addons?.sendmail ? mailboxNameForSubdomain(subdomain, manifest) : null;
|
||||
|
||||
Reference in New Issue
Block a user