diff --git a/CHANGES b/CHANGES index 9bb66f9f8..bcf162f9d 100644 --- a/CHANGES +++ b/CHANGES @@ -2663,4 +2663,5 @@ * translation: fix crash when translated text has single quote (french) * dyndns: show logs * mail: server location get it's own section +* turn: make it an optional service diff --git a/dashboard/src/views/app.html b/dashboard/src/views/app.html index 1c5102dae..9c73ebd2b 100644 --- a/dashboard/src/views/app.html +++ b/dashboard/src/views/app.html @@ -632,6 +632,7 @@
Proxy
{{ 'app.accessControlTabTitle' | tr }}
{{ 'app.resourcesTabTitle' | tr }}
+
{{ 'app.servicesTabTitle' | tr }}
{{ 'app.storageTabTitle' | tr }}
{{ 'app.graphsTabTitle' | tr }}
{{ 'app.securityTabTitle' | tr }}
@@ -965,6 +966,35 @@ +
+
+
+ + +
+ +
+ +
+ +
+
+ +
+
+
+ +
+
+
+
+
diff --git a/dashboard/src/views/app.js b/dashboard/src/views/app.js index f8c3eceae..a918c4fb6 100644 --- a/dashboard/src/views/app.js +++ b/dashboard/src/views/app.js @@ -585,6 +585,36 @@ angular.module('Application').controller('AppController', ['$scope', '$location' }, }; + $scope.services = { + error: {}, + + busy: false, + enableTurn: '1', // curse of radio buttons + + show: function () { + var app = $scope.app; + + $scope.services.error = {}; + $scope.services.enableTurn = app.enableTurn ? '1' : '0'; + }, + + submitTurn: function () { + $scope.services.busy = true; + $scope.services.error = {}; + + Client.configureApp($scope.app.id, 'turn', { enable: $scope.services.enableTurn === '1' }, function (error) { + if (error && error.statusCode === 400) { + $scope.services.busy = false; + $scope.services.error.turn = true; + return; + } + if (error) return Client.error(error); + + $timeout(function () { $scope.services.busy = false; }, 1000); + }); + }, + }; + $scope.storage = { error: {}, diff --git a/src/apps.js b/src/apps.js index 9ba942ee0..0b8583550 100644 --- a/src/apps.js +++ b/src/apps.js @@ -42,6 +42,7 @@ exports = module.exports = { setEnvironment, setMailbox, setInbox, + setTurn, setLocation, setStorage, repair, @@ -106,6 +107,7 @@ exports = module.exports = { ISTATE_PENDING_CONFIGURE: 'pending_configure', // infra update ISTATE_PENDING_RECREATE_CONTAINER: 'pending_recreate_container', // env change or addon change ISTATE_PENDING_LOCATION_CHANGE: 'pending_location_change', + ISTATE_PENDING_SERVICES_CHANGE: 'pending_services_change', ISTATE_PENDING_DATA_DIR_MIGRATION: 'pending_data_dir_migration', ISTATE_PENDING_RESIZE: 'pending_resize', ISTATE_PENDING_DEBUG: 'pending_debug', @@ -198,7 +200,7 @@ const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationS 'apps.sso', 'apps.debugModeJson', 'apps.enableBackup', 'apps.proxyAuth', 'apps.containerIp', 'apps.crontab', 'apps.creationTime', 'apps.updateTime', 'apps.enableAutomaticUpdate', 'apps.upstreamUri', 'apps.enableMailbox', 'apps.mailboxDisplayName', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableInbox', 'apps.inboxName', 'apps.inboxDomain', - 'apps.storageVolumeId', 'apps.storageVolumePrefix', 'apps.ts', 'apps.healthTime', '(apps.icon IS NOT NULL) AS hasIcon', '(apps.appStoreIcon IS NOT NULL) AS hasAppStoreIcon' ].join(','); + 'apps.enableTurn', '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 LOCATION_FIELDS = [ 'appId', 'subdomain', 'domain', 'type', 'certificateJson' ]; @@ -593,7 +595,7 @@ function removeInternalFields(app) { 'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares', 'operators', 'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags', 'label', 'secondaryDomains', 'redirectDomains', 'aliasDomains', 'env', 'enableAutomaticUpdate', - 'storageVolumeId', 'storageVolumePrefix', 'mounts', + 'storageVolumeId', 'storageVolumePrefix', 'mounts', 'enableTurn', 'enableMailbox', 'mailboxDisplayName', 'mailboxName', 'mailboxDomain', 'enableInbox', 'inboxName', 'inboxDomain'); removeCertificateKeys(result); @@ -840,6 +842,7 @@ async function add(id, appStoreId, manifest, subdomain, domain, portBindings, da servicesConfigJson = data.servicesConfig ? JSON.stringify(data.servicesConfig) : null, enableMailbox = data.enableMailbox || false, upstreamUri = data.upstreamUri || '', + enableTurn = 'enableTurn' in data ? data.enableTurn : true, icon = data.icon || null; const queries = []; @@ -847,11 +850,11 @@ async function add(id, appStoreId, manifest, subdomain, domain, portBindings, da queries.push({ query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares, ' + 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, servicesConfigJson, icon, ' - + 'enableMailbox, mailboxDisplayName, upstreamUri) ' - + ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + + 'enableMailbox, mailboxDisplayName, upstreamUri, enableTurn) ' + + ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares, sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, servicesConfigJson, icon, - enableMailbox, mailboxDisplayName, upstreamUri ] + enableMailbox, mailboxDisplayName, upstreamUri, enableTurn ] }); queries.push({ @@ -1324,6 +1327,7 @@ async function install(data, auditSource) { tags = data.tags || [], overwriteDns = 'overwriteDns' in data ? data.overwriteDns : false, skipDnsSetup = 'skipDnsSetup' in data ? data.skipDnsSetup : false, + enableTurn = 'enableTurn' in data ? data.enableTurn : true, appStoreId = data.appStoreId, upstreamUri = data.upstreamUri || '', manifest = data.manifest; @@ -1411,6 +1415,7 @@ async function install(data, auditSource) { icon, enableMailbox, upstreamUri, + enableTurn, runState: exports.RSTATE_RUNNING, installationState: exports.ISTATE_PENDING_INSTALL }; @@ -1733,6 +1738,29 @@ async function setInbox(app, data, auditSource) { return { taskId }; } +async function setTurn(app, enableTurn, auditSource) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof enableTurn, 'boolean'); + assert.strictEqual(typeof auditSource, 'object'); + + const appId = app.id; + let error = checkAppState(app, exports.ISTATE_PENDING_SERVICES_CHANGE); + if (error) throw error; + + if (!app.manifest.addons?.turn) throw new BoxError(BoxError.BAD_FIELD, 'App does not use turn addon'); + if (!app.manifest.addons.turn.optional) throw new BoxError(BoxError.BAD_FIELD, 'turn service is not optional'); + + const task = { + args: {}, + values: { enableTurn } + }; + const taskId = await addTask(appId, exports.ISTATE_PENDING_SERVICES_CHANGE, task, auditSource); + + await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, enableTurn, taskId }); + + return { taskId }; +} + async function setAutomaticBackup(app, enable, auditSource) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof enable, 'boolean'); diff --git a/src/apptask.js b/src/apptask.js index 65a8b7b3a..3722f9f7b 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -508,6 +508,31 @@ async function changeLocation(app, args, progressCallback) { await updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }); } +async function changeServices(app, args, progressCallback) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof args, 'object'); + assert.strictEqual(typeof progressCallback, 'function'); + + await progressCallback({ percent: 10, message: 'Cleaning up old install' }); + await deleteContainers(app, { managedOnly: true }); + + const unusedAddons = {}; + if (app.manifest.addons.turn && !args.enableTurn) unusedAddons.turn = app.manifest.addons.turn; + await progressCallback({ percent: 20, message: 'Removing unused addons' }); + await services.teardownAddons(app, unusedAddons); + + await progressCallback({ percent: 40, message: 'Setting up addons' }); + await services.setupAddons(app, app.manifest.addons); + + await progressCallback({ percent: 60, message: 'Creating container' }); + await createContainer(app); + + await startApp(app); + + await progressCallback({ percent: 100, message: 'Done' }); + await updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }); +} + async function migrateDataDir(app, args, progressCallback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof args, 'object'); @@ -773,6 +798,9 @@ async function run(appId, args, progressCallback) { case apps.ISTATE_PENDING_LOCATION_CHANGE: cmd = changeLocation(app, args, progressCallback); break; + case apps.ISTATE_PENDING_SERVICES_CHANGE: + cmd = changeServices(app, args, progressCallback); + break; case apps.ISTATE_PENDING_DATA_DIR_MIGRATION: cmd = migrateDataDir(app, args, progressCallback); break; diff --git a/src/routes/apps.js b/src/routes/apps.js index f5bc27342..d8353c3b2 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -24,6 +24,7 @@ exports = module.exports = { setLabel, setTags, setIcon, + setTurn, setMemoryLimit, setCpuShares, setAutomaticBackup, @@ -176,6 +177,8 @@ async function install(req, res, next) { if ('skipDnsSetup' in data && typeof data.skipDnsSetup !== 'boolean') return next(new HttpError(400, 'skipDnsSetup must be boolean')); if ('enableMailbox' in data && typeof data.enableMailbox !== 'boolean') return next(new HttpError(400, 'enableMailbox must be boolean')); + if ('enableTurn' in data && typeof data.enableTurn !== 'boolean') return next(new HttpError(400, 'enableTurn must be boolean')); + let [error, result] = await safe(apps.downloadManifest(data.appStoreId, data.manifest)); if (error) return next(BoxError.toHttpError(error)); @@ -407,6 +410,18 @@ async function setInbox(req, res, next) { next(new HttpSuccess(202, { taskId: result.taskId })); } +async function setTurn(req, res, next) { + assert.strictEqual(typeof req.body, 'object'); + assert.strictEqual(typeof req.app, 'object'); + + if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean')); + + const [error, result] = await safe(apps.setTurn(req.app, req.body.enable, AuditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(202, { taskId: result.taskId })); +} + async function setLocation(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); diff --git a/src/server.js b/src/server.js index 166a28cfe..e0f7088f1 100644 --- a/src/server.js +++ b/src/server.js @@ -231,6 +231,7 @@ async function initializeExpressSync() { router.post('/api/v1/apps/:id/configure/debug_mode', json, token, routes.apps.load, authorizeOperator, routes.apps.setDebugMode); router.post('/api/v1/apps/:id/configure/mailbox', json, token, routes.apps.load, authorizeAdmin, routes.apps.setMailbox); router.post('/api/v1/apps/:id/configure/inbox', json, token, routes.apps.load, authorizeAdmin, routes.apps.setInbox); + router.post('/api/v1/apps/:id/configure/turn', json, token, routes.apps.load, authorizeAdmin, routes.apps.setTurn); router.post('/api/v1/apps/:id/configure/env', json, token, routes.apps.load, authorizeOperator, routes.apps.setEnvironment); router.post('/api/v1/apps/:id/configure/storage', json, token, routes.apps.load, authorizeAdmin, routes.apps.setStorage); router.post('/api/v1/apps/:id/configure/location', json, token, routes.apps.load, authorizeAdmin, routes.apps.setLocation); diff --git a/src/services.js b/src/services.js index 99cfccdc2..2d5aa6740 100644 --- a/src/services.js +++ b/src/services.js @@ -887,6 +887,9 @@ async function setupTurn(app, options) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); + const disabled = app.manifest.addons.turn.optional && !app.enableTurn; + if (disabled) return await addonConfigs.set(app.id, 'turn', []); + const turnSecret = await blobs.getString(blobs.ADDON_TURN_SECRET); if (!turnSecret) throw new BoxError(BoxError.ADDONS_ERROR, 'Turn secret is missing');