diff --git a/src/addonconfigs.js b/src/addonconfigs.js new file mode 100644 index 000000000..b8569ebe8 --- /dev/null +++ b/src/addonconfigs.js @@ -0,0 +1,81 @@ +'use strict'; + +exports = module.exports = { + get, + set, + unset, + + getByAppId, + getByName, + unsetByAppId, + getAppIdByValue, +}; + +const assert = require('assert'), + database = require('./database.js'); + +async function set(appId, addonId, env) { + assert.strictEqual(typeof appId, 'string'); + assert.strictEqual(typeof addonId, 'string'); + assert(Array.isArray(env)); + + await unset(appId, addonId); + if (env.length === 0) return; + + const query = 'INSERT INTO appAddonConfigs(appId, addonId, name, value) VALUES '; + const args = [ ], queryArgs = [ ]; + for (let i = 0; i < env.length; i++) { + args.push(appId, addonId, env[i].name, env[i].value); + queryArgs.push('(?, ?, ?, ?)'); + } + + await database.query(query + queryArgs.join(','), args); +} + +async function unset(appId, addonId) { + assert.strictEqual(typeof appId, 'string'); + assert.strictEqual(typeof addonId, 'string'); + + await database.query('DELETE FROM appAddonConfigs WHERE appId = ? AND addonId = ?', [ appId, addonId ]); +} + +async function unsetByAppId(appId) { + assert.strictEqual(typeof appId, 'string'); + + await database.query('DELETE FROM appAddonConfigs WHERE appId = ?', [ appId ]); +} + +async function get(appId, addonId) { + assert.strictEqual(typeof appId, 'string'); + assert.strictEqual(typeof addonId, 'string'); + + const results = await database.query('SELECT name, value FROM appAddonConfigs WHERE appId = ? AND addonId = ?', [ appId, addonId ]); + return results; +} + +async function getByAppId(appId) { + assert.strictEqual(typeof appId, 'string'); + + const results = await database.query('SELECT name, value FROM appAddonConfigs WHERE appId = ?', [ appId ]); + return results; +} + +async function getAppIdByValue(addonId, namePattern, value) { + assert.strictEqual(typeof addonId, 'string'); + assert.strictEqual(typeof namePattern, 'string'); + assert.strictEqual(typeof value, 'string'); + + const results = await database.query('SELECT appId FROM appAddonConfigs WHERE addonId = ? AND name LIKE ? AND value = ?', [ addonId, namePattern, value ]); + if (results.length === 0) return null; + return results[0].appId; +} + +async function getByName(appId, addonId, namePattern) { + assert.strictEqual(typeof appId, 'string'); + assert.strictEqual(typeof addonId, 'string'); + assert.strictEqual(typeof namePattern, 'string'); + + const results = await database.query('SELECT value FROM appAddonConfigs WHERE appId = ? AND addonId = ? AND name LIKE ?', [ appId, addonId, namePattern ]); + if (results.length === 0) return null; + return results[0].value; +} diff --git a/src/appdb.js b/src/appdb.js index 2afb8af84..082bc2c27 100644 --- a/src/appdb.js +++ b/src/appdb.js @@ -10,13 +10,6 @@ exports = module.exports = { getPortBindings, delPortBinding, - setAddonConfig, - getAddonConfig, - getAddonConfigByAppId, - getAddonConfigByName, - unsetAddonConfig, - unsetAddonConfigByAppId, - getAppIdByAddonConfigValue, getByIpAddress, getIcons, @@ -495,103 +488,3 @@ function getAppStoreIds(callback) { callback(null, results); }); } - -function setAddonConfig(appId, addonId, env, callback) { - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof addonId, 'string'); - assert(Array.isArray(env)); - assert.strictEqual(typeof callback, 'function'); - - unsetAddonConfig(appId, addonId, function (error) { - if (error) return callback(error); - - if (env.length === 0) return callback(null); - - var query = 'INSERT INTO appAddonConfigs(appId, addonId, name, value) VALUES '; - var args = [ ], queryArgs = [ ]; - for (var i = 0; i < env.length; i++) { - args.push(appId, addonId, env[i].name, env[i].value); - queryArgs.push('(?, ?, ?, ?)'); - } - - database.query(query + queryArgs.join(','), args, function (error) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - return callback(null); - }); - }); -} - -function unsetAddonConfig(appId, addonId, callback) { - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof addonId, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('DELETE FROM appAddonConfigs WHERE appId = ? AND addonId = ?', [ appId, addonId ], function (error) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null); - }); -} - -function unsetAddonConfigByAppId(appId, callback) { - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('DELETE FROM appAddonConfigs WHERE appId = ?', [ appId ], function (error) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null); - }); -} - -function getAddonConfig(appId, addonId, callback) { - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof addonId, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT name, value FROM appAddonConfigs WHERE appId = ? AND addonId = ?', [ appId, addonId ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null, results); - }); -} - -function getAddonConfigByAppId(appId, callback) { - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT name, value FROM appAddonConfigs WHERE appId = ?', [ appId ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null, results); - }); -} - -function getAppIdByAddonConfigValue(addonId, namePattern, value, callback) { - assert.strictEqual(typeof addonId, 'string'); - assert.strictEqual(typeof namePattern, 'string'); - assert.strictEqual(typeof value, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT appId FROM appAddonConfigs WHERE addonId = ? AND name LIKE ? AND value = ?', [ addonId, namePattern, value ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found')); - - callback(null, results[0].appId); - }); -} - -function getAddonConfigByName(appId, addonId, namePattern, callback) { - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof addonId, 'string'); - assert.strictEqual(typeof namePattern, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT value FROM appAddonConfigs WHERE appId = ? AND addonId = ? AND name LIKE ?', [ appId, addonId, namePattern ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found')); - - callback(null, results[0].value); - }); -} diff --git a/src/apppasswords.js b/src/apppasswords.js index 2facd71d7..909f7cacd 100644 --- a/src/apppasswords.js +++ b/src/apppasswords.js @@ -15,7 +15,6 @@ const assert = require('assert'), database = require('./database.js'), hat = require('./hat.js'), safe = require('safetydance'), - uuid = require('uuid'), _ = require('underscore'); diff --git a/src/docker.js b/src/docker.js index 3938d32c3..dd55e7fb5 100644 --- a/src/docker.js +++ b/src/docker.js @@ -329,103 +329,102 @@ function createSubcontainer(app, name, cmd, options, callback) { // if required, we can make this a manifest and runtime argument later if (!isAppContainer) memoryLimit *= 2; - services.getEnvironment(app, function (error, addonEnv) { + getMounts(app, async function (error, mounts) { if (error) return callback(error); - getMounts(app, function (error, mounts) { - if (error) return callback(error); + const [getEnvError, addonEnv] = await services.getEnvironment(app); + if (getEnvError) return callback(getEnvError); - let containerOptions = { - name: name, // for referencing containers - Tty: isAppContainer, - Image: app.manifest.dockerImage, - Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd, - Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv), - ExposedPorts: isAppContainer ? exposedPorts : { }, - Volumes: { // see also ReadonlyRootfs - '/tmp': {}, - '/run': {} + let containerOptions = { + name: name, // for referencing containers + Tty: isAppContainer, + Image: app.manifest.dockerImage, + Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd, + Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv), + ExposedPorts: isAppContainer ? exposedPorts : { }, + Volumes: { // see also ReadonlyRootfs + '/tmp': {}, + '/run': {} + }, + Labels: { + 'fqdn': app.fqdn, + 'appId': app.id, + 'isSubcontainer': String(!isAppContainer), + 'isCloudronManaged': String(true) + }, + HostConfig: { + Mounts: mounts, + LogConfig: { + Type: 'syslog', + Config: { + 'tag': app.id, + 'syslog-address': 'udp://127.0.0.1:2514', // see apps.js:validatePortBindings() + 'syslog-format': 'rfc5424' + } }, - Labels: { - 'fqdn': app.fqdn, - 'appId': app.id, - 'isSubcontainer': String(!isAppContainer), - 'isCloudronManaged': String(true) + Memory: system.getMemoryAllocation(memoryLimit), + MemorySwap: memoryLimit, // Memory + Swap + PortBindings: isAppContainer ? dockerPortBindings : { }, + PublishAllPorts: false, + ReadonlyRootfs: app.debugMode ? !!app.debugMode.readonlyRootfs : true, + RestartPolicy: { + 'Name': isAppContainer ? 'unless-stopped' : 'no', + 'MaximumRetryCount': 0 }, - HostConfig: { - Mounts: mounts, - LogConfig: { - Type: 'syslog', - Config: { - 'tag': app.id, - 'syslog-address': 'udp://127.0.0.1:2514', // see apps.js:validatePortBindings() - 'syslog-format': 'rfc5424' - } - }, - Memory: system.getMemoryAllocation(memoryLimit), - MemorySwap: memoryLimit, // Memory + Swap - PortBindings: isAppContainer ? dockerPortBindings : { }, - PublishAllPorts: false, - ReadonlyRootfs: app.debugMode ? !!app.debugMode.readonlyRootfs : true, - RestartPolicy: { - 'Name': isAppContainer ? 'unless-stopped' : 'no', - 'MaximumRetryCount': 0 - }, - CpuShares: app.cpuShares, - VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ], - SecurityOpt: [ 'apparmor=docker-cloudron-app' ], - CapAdd: [], - CapDrop: [] + CpuShares: app.cpuShares, + VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ], + SecurityOpt: [ 'apparmor=docker-cloudron-app' ], + CapAdd: [], + CapDrop: [] + } + }; + + // do no set hostname of containers to location as it might conflict with addons names. for example, an app installed in mail + // location may not reach mail container anymore by DNS. We cannot set hostname to fqdn either as that sets up the dns + // name to look up the internal docker ip. this makes curl from within container fail + // Note that Hostname has no effect on DNS. We have to use the --net-alias for dns. + // Hostname cannot be set with container NetworkMode. Subcontainers run is the network space of the app container + // This is done to prevent lots of up/down events and iptables locking + if (isAppContainer) { + containerOptions.Hostname = app.id; + containerOptions.HostConfig.NetworkMode = 'cloudron'; // user defined bridge network + containerOptions.HostConfig.Dns = ['172.18.0.1']; // use internal dns + containerOptions.HostConfig.DnsSearch = ['.']; // use internal dns + + containerOptions.NetworkingConfig = { + EndpointsConfig: { + cloudron: { + IPAMConfig: { + IPv4Address: app.containerIp + }, + Aliases: [ name ] // adds hostname entry with container name + } } }; + } else { + containerOptions.HostConfig.NetworkMode = `container:${app.containerId}`; // scheduler containers must have same IP as app for various addon auth + } - // do no set hostname of containers to location as it might conflict with addons names. for example, an app installed in mail - // location may not reach mail container anymore by DNS. We cannot set hostname to fqdn either as that sets up the dns - // name to look up the internal docker ip. this makes curl from within container fail - // Note that Hostname has no effect on DNS. We have to use the --net-alias for dns. - // Hostname cannot be set with container NetworkMode. Subcontainers run is the network space of the app container - // This is done to prevent lots of up/down events and iptables locking - if (isAppContainer) { - containerOptions.Hostname = app.id; - containerOptions.HostConfig.NetworkMode = 'cloudron'; // user defined bridge network - containerOptions.HostConfig.Dns = ['172.18.0.1']; // use internal dns - containerOptions.HostConfig.DnsSearch = ['.']; // use internal dns + var capabilities = manifest.capabilities || []; - containerOptions.NetworkingConfig = { - EndpointsConfig: { - cloudron: { - IPAMConfig: { - IPv4Address: app.containerIp - }, - Aliases: [ name ] // adds hostname entry with container name - } - } - }; - } else { - containerOptions.HostConfig.NetworkMode = `container:${app.containerId}`; // scheduler containers must have same IP as app for various addon auth - } + // https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities + if (capabilities.includes('net_admin')) containerOptions.HostConfig.CapAdd.push('NET_ADMIN', 'NET_RAW'); + if (capabilities.includes('mlock')) containerOptions.HostConfig.CapAdd.push('IPC_LOCK'); // mlock prevents swapping + if (!capabilities.includes('ping')) containerOptions.HostConfig.CapDrop.push('NET_RAW'); // NET_RAW is included by default by Docker - var capabilities = manifest.capabilities || []; + if (capabilities.includes('vaapi') && safe.fs.existsSync('/dev/dri')) { + containerOptions.HostConfig.Devices = [ + { PathOnHost: '/dev/dri', PathInContainer: '/dev/dri', CgroupPermissions: 'rwm' } + ]; + } - // https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities - if (capabilities.includes('net_admin')) containerOptions.HostConfig.CapAdd.push('NET_ADMIN', 'NET_RAW'); - if (capabilities.includes('mlock')) containerOptions.HostConfig.CapAdd.push('IPC_LOCK'); // mlock prevents swapping - if (!capabilities.includes('ping')) containerOptions.HostConfig.CapDrop.push('NET_RAW'); // NET_RAW is included by default by Docker + containerOptions = _.extend(containerOptions, options); - if (capabilities.includes('vaapi') && safe.fs.existsSync('/dev/dri')) { - containerOptions.HostConfig.Devices = [ - { PathOnHost: '/dev/dri', PathInContainer: '/dev/dri', CgroupPermissions: 'rwm' } - ]; - } + gConnection.createContainer(containerOptions, function (error, container) { + if (error && error.statusCode === 409) return callback(new BoxError(BoxError.ALREADY_EXISTS, error)); + if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); - containerOptions = _.extend(containerOptions, options); - - gConnection.createContainer(containerOptions, function (error, container) { - if (error && error.statusCode === 409) return callback(new BoxError(BoxError.ALREADY_EXISTS, error)); - if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); - - callback(null, container); - }); + callback(null, container); }); }); } diff --git a/src/ldap.js b/src/ldap.js index 34b321cdf..ca9ccb2f0 100644 --- a/src/ldap.js +++ b/src/ldap.js @@ -7,8 +7,8 @@ exports = module.exports = { _MOCK_APP: null }; -const assert = require('assert'), - appdb = require('./appdb.js'), +const addonConfigs = require('./addonconfigs.js'), + assert = require('assert'), apps = require('./apps.js'), async = require('async'), BoxError = require('./boxerror.js'), @@ -628,24 +628,18 @@ function userSearchSftp(req, res, next) { }); } -function verifyAppMailboxPassword(addonId, username, password, callback) { +async function verifyAppMailboxPassword(addonId, username, password) { assert.strictEqual(typeof addonId, 'string'); assert.strictEqual(typeof username, 'string'); assert.strictEqual(typeof password, 'string'); - assert.strictEqual(typeof callback, 'function'); const pattern = addonId === 'sendmail' ? 'MAIL_SMTP' : 'MAIL_IMAP'; - appdb.getAppIdByAddonConfigValue(addonId, `%${pattern}_PASSWORD`, password, function (error, appId) { // search by password because this is unique for each app - if (error) return callback(error); + const appId = await addonConfigs.getAppIdByValue(addonId, `%${pattern}_PASSWORD`, password); // search by password because this is unique for each app + if (!appId) throw new BoxError(BoxError.NOT_FOUND); - appdb.getAddonConfig(appId, addonId, function (error, result) { - if (error) return callback(error); + const result = await addonConfigs.get(appId, addonId); - if (!result.some(r => r.name.endsWith(`${pattern}_USERNAME`) && r.value === username)) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); - - callback(null); - }); - }); + if (!result.some(r => r.name.endsWith(`${pattern}_USERNAME`) && r.value === username)) throw new BoxError(BoxError.INVALID_CREDENTIALS); } async function authenticateMailAddon(req, res, next) { @@ -666,7 +660,7 @@ async function authenticateMailAddon(req, res, next) { if (addonId === 'recvmail' && !domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString())); - const [appPasswordError] = await safe(util.promisify(verifyAppMailboxPassword)(addonId, email, req.credentials || '')); + const [appPasswordError] = await safe(verifyAppMailboxPassword(addonId, email, req.credentials || '')); if (!appPasswordError) return res.end(); // validated as app if (appPasswordError && appPasswordError.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString())); diff --git a/src/services.js b/src/services.js index 14d669139..02f5540b3 100644 --- a/src/services.js +++ b/src/services.js @@ -31,7 +31,8 @@ exports = module.exports = { SERVICE_STATUS_STOPPED: 'stopped' }; -const appdb = require('./appdb.js'), +const addonConfigs = require('./addonconfigs.js'), + appdb = require('./appdb.js'), apps = require('./apps.js'), assert = require('assert'), async = require('async'), @@ -68,9 +69,13 @@ const NOOP_CALLBACK = function (error) { if (error) debug(error); }; const RMADDONDIR_CMD = path.join(__dirname, 'scripts/rmaddondir.sh'); const RESTART_SERVICE_CMD = path.join(__dirname, 'scripts/restartservice.sh'); +const setAddonConfig = util.callbackify(addonConfigs.set), + unsetAddonConfig = util.callbackify(addonConfigs.unset), + getAddonConfigByName = util.callbackify(addonConfigs.getByName); + // setup can be called multiple times for the same app (configure crash restart) and existing data must not be lost // teardown is destructive. app data stored with the addon is lost -var ADDONS = { +const ADDONS = { turn: { setup: setupTurn, teardown: teardownTurn, @@ -883,17 +888,14 @@ function startServices(existingInfra, callback) { }); } -function getEnvironment(app, callback) { +async function getEnvironment(app) { assert.strictEqual(typeof app, 'object'); - assert.strictEqual(typeof callback, 'function'); - appdb.getAddonConfigByAppId(app.id, function (error, result) { - if (error) return callback(error); + const result = await addonConfigs.getByAppId(app.id); - if (app.manifest.addons['docker']) result.push({ name: 'CLOUDRON_DOCKER_HOST', value: `tcp://172.18.0.1:${constants.DOCKER_PROXY_PORT}` }); + if (app.manifest.addons['docker']) result.push({ name: 'CLOUDRON_DOCKER_HOST', value: `tcp://172.18.0.1:${constants.DOCKER_PROXY_PORT}` }); - return callback(null, result.map(function (e) { return e.name + '=' + e.value; })); - }); + return result.map(function (e) { return e.name + '=' + e.value; }); } function getContainerNamesSync(app, addons) { @@ -978,7 +980,7 @@ function setupTurn(app, options, callback) { debugApp(app, 'Setting up TURN'); - appdb.setAddonConfig(app.id, 'turn', env, callback); + setAddonConfig(app.id, 'turn', env, callback); }); } @@ -989,7 +991,7 @@ function teardownTurn(app, options, callback) { debugApp(app, 'Tearing down TURN'); - appdb.unsetAddonConfig(app.id, 'turn', callback); + unsetAddonConfig(app.id, 'turn', callback); } function setupEmail(app, options, callback) { @@ -1022,7 +1024,7 @@ function setupEmail(app, options, callback) { debugApp(app, 'Setting up Email'); - appdb.setAddonConfig(app.id, 'email', env, callback); + setAddonConfig(app.id, 'email', env, callback); }); } @@ -1033,7 +1035,7 @@ function teardownEmail(app, options, callback) { debugApp(app, 'Tearing down Email'); - appdb.unsetAddonConfig(app.id, 'email', callback); + unsetAddonConfig(app.id, 'email', callback); } function setupLdap(app, options, callback) { @@ -1058,7 +1060,7 @@ function setupLdap(app, options, callback) { debugApp(app, 'Setting up LDAP'); - appdb.setAddonConfig(app.id, 'ldap', env, callback); + setAddonConfig(app.id, 'ldap', env, callback); } function teardownLdap(app, options, callback) { @@ -1068,7 +1070,7 @@ function teardownLdap(app, options, callback) { debugApp(app, 'Tearing down LDAP'); - appdb.unsetAddonConfig(app.id, 'ldap', callback); + unsetAddonConfig(app.id, 'ldap', callback); } function setupSendMail(app, options, callback) { @@ -1079,9 +1081,9 @@ function setupSendMail(app, options, callback) { debugApp(app, 'Setting up SendMail'); const disabled = app.manifest.addons.sendmail.optional && !app.enableMailbox; - if (disabled) return appdb.setAddonConfig(app.id, 'sendmail', [], callback); + if (disabled) return setAddonConfig(app.id, 'sendmail', [], callback); - appdb.getAddonConfigByName(app.id, 'sendmail', '%MAIL_SMTP_PASSWORD', function (error, existingPassword) { + getAddonConfigByName(app.id, 'sendmail', '%MAIL_SMTP_PASSWORD', function (error, existingPassword) { if (error && error.reason !== BoxError.NOT_FOUND) return callback(error); var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length @@ -1099,7 +1101,7 @@ function setupSendMail(app, options, callback) { { name: `${envPrefix}MAIL_DOMAIN`, value: app.mailboxDomain } ]; debugApp(app, 'Setting sendmail addon config to %j', env); - appdb.setAddonConfig(app.id, 'sendmail', env, callback); + setAddonConfig(app.id, 'sendmail', env, callback); }); } @@ -1110,7 +1112,7 @@ function teardownSendMail(app, options, callback) { debugApp(app, 'Tearing down sendmail'); - appdb.unsetAddonConfig(app.id, 'sendmail', callback); + unsetAddonConfig(app.id, 'sendmail', callback); } function setupRecvMail(app, options, callback) { @@ -1120,7 +1122,7 @@ function setupRecvMail(app, options, callback) { debugApp(app, 'Setting up recvmail'); - appdb.getAddonConfigByName(app.id, 'recvmail', '%MAIL_IMAP_PASSWORD', function (error, existingPassword) { + getAddonConfigByName(app.id, 'recvmail', '%MAIL_IMAP_PASSWORD', function (error, existingPassword) { if (error && error.reason !== BoxError.NOT_FOUND) return callback(error); var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length @@ -1137,7 +1139,7 @@ function setupRecvMail(app, options, callback) { ]; debugApp(app, 'Setting sendmail addon config to %j', env); - appdb.setAddonConfig(app.id, 'recvmail', env, callback); + setAddonConfig(app.id, 'recvmail', env, callback); }); } @@ -1148,7 +1150,7 @@ function teardownRecvMail(app, options, callback) { debugApp(app, 'Tearing down recvmail'); - appdb.unsetAddonConfig(app.id, 'recvmail', callback); + unsetAddonConfig(app.id, 'recvmail', callback); } function mysqlDatabaseName(appId) { @@ -1219,7 +1221,7 @@ function setupMySql(app, options, callback) { debugApp(app, 'Setting up mysql'); - appdb.getAddonConfigByName(app.id, 'mysql', '%MYSQL_PASSWORD', function (error, existingPassword) { + getAddonConfigByName(app.id, 'mysql', '%MYSQL_PASSWORD', function (error, existingPassword) { if (error && error.reason !== BoxError.NOT_FOUND) return callback(error); const tmp = mysqlDatabaseName(app.id); @@ -1257,7 +1259,7 @@ function setupMySql(app, options, callback) { } debugApp(app, 'Setting mysql addon config to %j', env); - appdb.setAddonConfig(app.id, 'mysql', env, callback); + setAddonConfig(app.id, 'mysql', env, callback); }); }); }); @@ -1297,7 +1299,7 @@ function teardownMySql(app, options, callback) { if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mysql: ${error.message}`)); if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mysql. Status code: ${response.statusCode} message: ${response.body.message}`)); - appdb.unsetAddonConfig(app.id, 'mysql', callback); + unsetAddonConfig(app.id, 'mysql', callback); }); }); } @@ -1439,7 +1441,7 @@ function setupPostgreSql(app, options, callback) { const { database, username } = postgreSqlNames(app.id); - appdb.getAddonConfigByName(app.id, 'postgresql', '%POSTGRESQL_PASSWORD', function (error, existingPassword) { + getAddonConfigByName(app.id, 'postgresql', '%POSTGRESQL_PASSWORD', function (error, existingPassword) { if (error && error.reason !== BoxError.NOT_FOUND) return callback(error); const data = { @@ -1468,7 +1470,7 @@ function setupPostgreSql(app, options, callback) { ]; debugApp(app, 'Setting postgresql addon config to %j', env); - appdb.setAddonConfig(app.id, 'postgresql', env, callback); + setAddonConfig(app.id, 'postgresql', env, callback); }); }); }); @@ -1510,7 +1512,7 @@ function teardownPostgreSql(app, options, callback) { if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error tearing down postgresql: ${error.message}`)); if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down postgresql. Status code: ${response.statusCode} message: ${response.body.message}`)); - appdb.unsetAddonConfig(app.id, 'postgresql', callback); + unsetAddonConfig(app.id, 'postgresql', callback); }); }); } @@ -1658,10 +1660,10 @@ function setupMongoDb(app, options, callback) { debugApp(app, 'Setting up mongodb'); - appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_PASSWORD', function (error, existingPassword) { + getAddonConfigByName(app.id, 'mongodb', '%MONGODB_PASSWORD', function (error, existingPassword) { if (error && error.reason !== BoxError.NOT_FOUND) return callback(error); - appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) { + getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) { database = database || hat(8 * 8); // 16 bytes. keep this short, so as to not overflow the 127 byte index length in MongoDB < 4.4 const data = { @@ -1694,7 +1696,7 @@ function setupMongoDb(app, options, callback) { } debugApp(app, 'Setting mongodb addon config to %j', env); - appdb.setAddonConfig(app.id, 'mongodb', env, callback); + setAddonConfig(app.id, 'mongodb', env, callback); }); }); }); @@ -1709,7 +1711,7 @@ function clearMongodb(app, options, callback) { getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) { if (error) return callback(error); - appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) { + getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) { if (error) return callback(error); request.post(`https://${result.ip}:3000/databases/${database}/clear?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) { @@ -1730,7 +1732,7 @@ function teardownMongoDb(app, options, callback) { getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) { if (error) return callback(error); - appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) { + getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null); if (error) return callback(error); @@ -1738,7 +1740,7 @@ function teardownMongoDb(app, options, callback) { if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mongodb: ${error.message}`)); if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error tearing down mongodb. Status code: ${response.statusCode} message: ${response.body.message}`)); - appdb.unsetAddonConfig(app.id, 'mongodb', callback); + unsetAddonConfig(app.id, 'mongodb', callback); }); }); }); @@ -1754,7 +1756,7 @@ function backupMongoDb(app, options, callback) { getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) { if (error) return callback(error); - appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) { + getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) { if (error) return callback(error); const url = `https://${result.ip}:3000/databases/${database}/backup?access_token=${result.token}`; @@ -1775,7 +1777,7 @@ function restoreMongoDb(app, options, callback) { getContainerDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) { if (error) return callback(error); - appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) { + getAddonConfigByName(app.id, 'mongodb', '%MONGODB_DATABASE', function (error, database) { if (error) return callback(error); const readStream = fs.createReadStream(dumpPath('mongodb', app.id)); @@ -1853,7 +1855,7 @@ function setupProxyAuth(app, options, callback) { if (!enabled) return callback(); const env = [ { name: 'CLOUDRON_PROXY_AUTH', value: '1' } ]; - appdb.setAddonConfig(app.id, 'proxyauth', env, callback); + setAddonConfig(app.id, 'proxyauth', env, callback); } function teardownProxyAuth(app, options, callback) { @@ -1861,7 +1863,7 @@ function teardownProxyAuth(app, options, callback) { assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); - appdb.unsetAddonConfig(app.id, 'proxyauth', callback); + unsetAddonConfig(app.id, 'proxyauth', callback); } function startRedis(existingInfra, callback) { @@ -1906,7 +1908,7 @@ function setupRedis(app, options, callback) { const redisName = 'redis-' + app.id; - appdb.getAddonConfigByName(app.id, 'redis', '%REDIS_PASSWORD', function (error, existingPassword) { + getAddonConfigByName(app.id, 'redis', '%REDIS_PASSWORD', function (error, existingPassword) { if (error && error.reason !== BoxError.NOT_FOUND) return callback(error); const redisPassword = options.noPassword ? '' : (error ? hat(4 * 48) : existingPassword); // see box#362 for password length @@ -1957,7 +1959,7 @@ function setupRedis(app, options, callback) { shell.exec('startRedis', cmd, next); }); }, - appdb.setAddonConfig.bind(null, app.id, 'redis', env), + setAddonConfig.bind(null, app.id, 'redis', env), waitForContainer.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN') ], function (error) { if (error) debug('Error setting up redis: ', error); @@ -1999,7 +2001,7 @@ function teardownRedis(app, options, callback) { rimraf(path.join(paths.LOG_DIR, `redis-${app.id}`), function (error) { if (error) debugApp(app, 'cannot cleanup logs:', error); - appdb.unsetAddonConfig(app.id, 'redis', callback); + unsetAddonConfig(app.id, 'redis', callback); }); }); }); @@ -2183,5 +2185,5 @@ function teardownOauth(app, options, callback) { debugApp(app, 'teardownOauth'); - appdb.unsetAddonConfig(app.id, 'oauth', callback); + unsetAddonConfig(app.id, 'oauth', callback); } diff --git a/src/test/addonconfigs-test.js b/src/test/addonconfigs-test.js new file mode 100644 index 000000000..7dc56dc68 --- /dev/null +++ b/src/test/addonconfigs-test.js @@ -0,0 +1,80 @@ +/* global it:false */ +/* global describe:false */ +/* global before:false */ +/* global after:false */ + +'use strict'; + +const addonConfigs = require('../addonconfigs.js'), + common = require('./common.js'), + expect = require('expect.js'); + +describe('Addon config', function () { + const { setup, cleanup, app } = common; + + before(setup); + after(cleanup); + + it('returns empty addon config array for invalid app', async function () { + const results = await addonConfigs.getByAppId('randomid'); + expect(results).to.eql([ ]); + }); + + it('set succeeds', async function () { + await addonConfigs.set(app.id, 'addonid1', [ { name: 'ENV1', value: 'env' }, { name: 'ENV2', value: 'env2' } ]); + await addonConfigs.set(app.id, 'addonid2', [ { name: 'ENV3', value: 'env' } ]); + }); + + it('get succeeds', async function () { + const results = await addonConfigs.get(app.id, 'addonid1'); + expect(results).to.eql([ { name: 'ENV1', value: 'env' }, { name: 'ENV2', value: 'env2' } ]); + }); + + it('getByAppId succeeds', async function () { + const results = await addonConfigs.getByAppId(app.id); + expect(results).to.eql([ { name: 'ENV1', value: 'env' }, { name: 'ENV2', value: 'env2' }, { name: 'ENV3', value: 'env' } ]); + }); + + it('getByName succeeds', async function () { + const value = await addonConfigs.getByName(app.id, 'addonid1', 'ENV2'); + expect(value).to.be('env2'); + }); + + it('getByName of unknown value succeeds', async function () { + const value = await addonConfigs.getByName(app.id, 'addonid1', 'ENVRANDOM'); + expect(value).to.be(null); + }); + + it('getAppIdByValue succeeds', async function () { + const appId = await addonConfigs.getAppIdByValue('addonid1', 'ENV1', 'env'); + expect(appId).to.be(app.id); + }); + + it('getAppIdByValue pattern succeeds', async function () { + const appId = await addonConfigs.getAppIdByValue('addonid1', '%ENV1', 'env'); + expect(appId).to.be(app.id); + }); + + it('getAppIdByValue pattern of unknown succeeds', async function () { + const appId = await addonConfigs.getAppIdByValue('addonid1', '%ENV1', 'envx'); + expect(appId).to.be(null); + }); + + it('unset succeeds', async function () { + await addonConfigs.unset(app.id, 'addonid1'); + }); + + it('unsetAddonConfig did remove configs', async function () { + const results = await addonConfigs.getByAppId(app.id); + expect(results).to.eql([ { name: 'ENV3', value: 'env' }]); + }); + + it('unsetByAppId succeeds', async function () { + await addonConfigs.unsetByAppId(app.id); + }); + + it('unsetByAppId did remove configs', async function () { + const results = await addonConfigs.getByAppId(app.id); + expect(results).to.eql([ ]); + }); +}); diff --git a/src/test/database-test.js b/src/test/database-test.js index f1bf7156c..9647e4de0 100644 --- a/src/test/database-test.js +++ b/src/test/database-test.js @@ -309,89 +309,6 @@ describe('database', function () { done(); }); }); - - it('return empty addon config array for invalid app', function (done) { - appdb.getAddonConfigByAppId('randomid', function (error, results) { - expect(error).to.be(null); - expect(results).to.eql([ ]); - done(); - }); - }); - - it('setAddonConfig succeeds', function (done) { - appdb.setAddonConfig(APP_1.id, 'addonid1', [ { name: 'ENV1', value: 'env' }, { name: 'ENV2', value: 'env2' } ], function (error) { - expect(error).to.be(null); - done(); - }); - }); - - it('setAddonConfig succeeds', function (done) { - appdb.setAddonConfig(APP_1.id, 'addonid2', [ { name: 'ENV3', value: 'env' } ], function (error) { - expect(error).to.be(null); - done(); - }); - }); - - it('getAddonConfig succeeds', function (done) { - appdb.getAddonConfig(APP_1.id, 'addonid1', function (error, results) { - expect(error).to.be(null); - expect(results).to.eql([ { name: 'ENV1', value: 'env' }, { name: 'ENV2', value: 'env2' } ]); - done(); - }); - }); - - it('getAddonConfigByAppId succeeds', function (done) { - appdb.getAddonConfigByAppId(APP_1.id, function (error, results) { - expect(error).to.be(null); - expect(results).to.eql([ { name: 'ENV1', value: 'env' }, { name: 'ENV2', value: 'env2' }, { name: 'ENV3', value: 'env' } ]); - done(); - }); - }); - - it('getAddonConfigByName succeeds', function (done) { - appdb.getAddonConfigByName(APP_1.id, 'addonid1', 'ENV2', function (error, value) { - expect(error).to.be(null); - expect(value).to.be('env2'); - done(); - }); - }); - - it('getAddonConfigByName of unknown value succeeds', function (done) { - appdb.getAddonConfigByName(APP_1.id, 'addonid1', 'NOPE', function (error) { - expect(error.reason).to.be(BoxError.NOT_FOUND); - done(); - }); - }); - - it('unsetAddonConfig succeeds', function (done) { - appdb.unsetAddonConfig(APP_1.id, 'addonid1', function (error) { - expect(error).to.be(null); - done(); - }); - }); - - it('unsetAddonConfig did remove configs', function (done) { - appdb.getAddonConfigByAppId(APP_1.id, function (error, results) { - expect(error).to.be(null); - expect(results).to.eql([ { name: 'ENV3', value: 'env' }]); - done(); - }); - }); - - it('unsetAddonConfigByAppId succeeds', function (done) { - appdb.unsetAddonConfigByAppId(APP_1.id, function (error) { - expect(error).to.be(null); - done(); - }); - }); - - it('unsetAddonConfigByAppId did remove configs', function (done) { - appdb.getAddonConfigByAppId(APP_1.id, function (error, results) { - expect(error).to.be(null); - expect(results).to.eql([ ]); - done(); - }); - }); }); describe('importFromFile', function () { diff --git a/src/test/ldap-test.js b/src/test/ldap-test.js index eb4b210a6..1cbd10083 100644 --- a/src/test/ldap-test.js +++ b/src/test/ldap-test.js @@ -6,7 +6,7 @@ 'use strict'; -const appdb = require('../appdb.js'), +const addonConfigs = require('../addonconfigs.js'), async = require('async'), common = require('./common.js'), constants = require('../constants.js'), @@ -15,8 +15,7 @@ const appdb = require('../appdb.js'), ldap = require('ldapjs'), ldapServer = require('../ldap.js'), mail = require('../mail.js'), - safe = require('safetydance'), - util = require('util'); + safe = require('safetydance'); async function ldapBind(dn, password) { return new Promise((resolve, reject) => { @@ -417,9 +416,7 @@ describe('Ldap', function () { }); it('allows with valid password', async function () { - const setAddonConfig = util.promisify(appdb.setAddonConfig); - - await setAddonConfig(app.id, 'sendmail', [{ name: 'MAIL_SMTP_USERNAME', value : `${app.location}.app@${domain.domain}` }, { name: 'MAIL_SMTP_PASSWORD', value : 'sendmailpassword' }]), + await addonConfigs.set(app.id, 'sendmail', [{ name: 'MAIL_SMTP_USERNAME', value : `${app.location}.app@${domain.domain}` }, { name: 'MAIL_SMTP_PASSWORD', value : 'sendmailpassword' }]), await ldapBind(`cn=${app.location}.app@${domain.domain},ou=sendmail,dc=cloudron`, 'sendmailpassword'); }); @@ -469,8 +466,7 @@ describe('Ldap', function () { }); it('allows with valid password', async function () { - const setAddonConfig = util.promisify(appdb.setAddonConfig); - await setAddonConfig(app.id, 'recvmail', [{ name: 'MAIL_IMAP_USERNAME', value : `${app.location}.app@${domain.domain}` }, { name: 'MAIL_IMAP_PASSWORD', value : 'recvmailpassword' }]), + await addonConfigs.set(app.id, 'recvmail', [{ name: 'MAIL_IMAP_USERNAME', value : `${app.location}.app@${domain.domain}` }, { name: 'MAIL_IMAP_PASSWORD', value : 'recvmailpassword' }]), await ldapBind(`cn=${app.location}.app@${domain.domain},ou=recvmail,dc=cloudron`, 'recvmailpassword'); }); });