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');