diff --git a/CHANGES b/CHANGES index b2a8a7553..1e4908bc8 100644 --- a/CHANGES +++ b/CHANGES @@ -1773,3 +1773,6 @@ * Certs of stopped apps are not renewed * Fix broken memory sliders in the services UI +[4.5.0] +* Set CPU Shares + diff --git a/migrations/20200129052822-apps-add-cpuShares.js b/migrations/20200129052822-apps-add-cpuShares.js new file mode 100644 index 000000000..3d9369ea2 --- /dev/null +++ b/migrations/20200129052822-apps-add-cpuShares.js @@ -0,0 +1,15 @@ +'use strict'; + +exports.up = function(db, callback) { + db.runSql('ALTER TABLE apps ADD COLUMN cpuShares INTEGER DEFAULT 512', function (error) { + if (error) console.error(error); + callback(error); + }); +}; + +exports.down = function(db, callback) { + db.runSql('ALTER TABLE apps DROP COLUMN cpuShares', function (error) { + if (error) console.error(error); + callback(error); + }); +}; diff --git a/migrations/schema.sql b/migrations/schema.sql index b5c2f66d0..3d791ff21 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -78,6 +78,7 @@ CREATE TABLE IF NOT EXISTS apps( updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, // when this db record was updated (useful for UI caching) memoryLimit BIGINT DEFAULT 0, + cpuShares INTEGER DEFAULT 512, xFrameOptions VARCHAR(512), sso BOOLEAN DEFAULT 1, // whether user chose to enable SSO debugModeJson TEXT, // options for development mode diff --git a/src/appdb.js b/src/appdb.js index 67a4bcac1..6ff36b018 100644 --- a/src/appdb.js +++ b/src/appdb.js @@ -40,7 +40,7 @@ var assert = require('assert'), var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState', 'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain', - 'apps.accessRestrictionJson', 'apps.memoryLimit', + 'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares', 'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.sso', 'apps.debugModeJson', 'apps.enableBackup', 'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate', @@ -242,6 +242,7 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal const accessRestriction = data.accessRestriction || null; const accessRestrictionJson = JSON.stringify(accessRestriction); const memoryLimit = data.memoryLimit || 0; + const cpuShares = data.cpuShares || 512; const installationState = data.installationState; const runState = data.runState; const sso = 'sso' in data ? data.sso : null; @@ -256,10 +257,10 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal var queries = []; queries.push({ - query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, ' + query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares, ' + 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson) ' - + ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', - args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, + + ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares, sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson ] }); diff --git a/src/apps.js b/src/apps.js index 98921c8d0..6ad380050 100644 --- a/src/apps.js +++ b/src/apps.js @@ -19,6 +19,7 @@ exports = module.exports = { setIcon: setIcon, setTags: setTags, setMemoryLimit: setMemoryLimit, + setCpuShares: setCpuShares, setAutomaticBackup: setAutomaticBackup, setAutomaticUpdate: setAutomaticUpdate, setReverseProxyConfig: setReverseProxyConfig, @@ -249,6 +250,14 @@ function validateMemoryLimit(manifest, memoryLimit) { return null; } +function validateCpuShares(cpuShares) { + assert.strictEqual(typeof cpuShares, 'number'); + + if (cpuShares <= 0 || cpuShares > 1024) return new BoxError(BoxError.BAD_FIELD, 'cpuShares has to be between 0 and 1024'); + + return null; +} + function validateDebugMode(debugMode) { assert.strictEqual(typeof debugMode, 'object'); @@ -385,7 +394,7 @@ function removeInternalFields(app) { return _.pick(app, 'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', 'location', 'domain', 'fqdn', 'mailboxName', 'mailboxDomain', - 'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', + 'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares', 'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags', 'label', 'alternateDomains', 'env', 'enableAutomaticUpdate', 'dataDir'); } @@ -935,6 +944,35 @@ function setMemoryLimit(appId, memoryLimit, auditSource, callback) { }); } +function setCpuShares(appId, cpuShares, auditSource, callback) { + assert.strictEqual(typeof appId, 'string'); + assert.strictEqual(typeof cpuShares, 'number'); + assert.strictEqual(typeof auditSource, 'object'); + assert.strictEqual(typeof callback, 'function'); + + get(appId, function (error, app) { + if (error) return callback(error); + + error = checkAppState(app, exports.ISTATE_PENDING_RESIZE); + if (error) return callback(error); + + error = validateCpuShares(cpuShares); + if (error) return callback(error); + + const task = { + args: {}, + values: { cpuShares } + }; + addTask(appId, exports.ISTATE_PENDING_RESIZE, task, function (error, result) { + if (error) return callback(error); + + eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: app, cpuShares, taskId: result.taskId }); + + callback(null, { taskId: result.taskId }); + }); + }); +} + function setEnvironment(appId, env, auditSource, callback) { assert.strictEqual(typeof appId, 'string'); assert.strictEqual(typeof env, 'object'); diff --git a/src/docker.js b/src/docker.js index cf8bda0b1..4ddc16f79 100644 --- a/src/docker.js +++ b/src/docker.js @@ -296,7 +296,7 @@ function createSubcontainer(app, name, cmd, options, callback) { 'Name': isAppContainer ? 'unless-stopped' : 'no', 'MaximumRetryCount': 0 }, - CpuShares: 512, // relative to 1024 for system processes + CpuShares: app.cpuShares, VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ], NetworkMode: 'cloudron', // user defined bridge network Dns: ['172.18.0.1'], // use internal dns diff --git a/src/routes/apps.js b/src/routes/apps.js index 48662a2d6..e8ef5829b 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -20,6 +20,7 @@ exports = module.exports = { setTags: setTags, setIcon: setIcon, setMemoryLimit: setMemoryLimit, + setCpuShares: setCpuShares, setAutomaticBackup: setAutomaticBackup, setAutomaticUpdate: setAutomaticUpdate, setReverseProxyConfig: setReverseProxyConfig, @@ -207,6 +208,19 @@ function setMemoryLimit(req, res, next) { }); } +function setCpuShares(req, res, next) { + assert.strictEqual(typeof req.body, 'object'); + assert.strictEqual(typeof req.params.id, 'string'); + + if (typeof req.body.cpuShares !== 'number') return next(new HttpError(400, 'cpuShares is not a number')); + + apps.setCpuShares(req.params.id, req.body.cpuShares, auditSource.fromRequest(req), function (error, result) { + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(202, { taskId: result.taskId })); + }); +} + function setAutomaticBackup(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.params.id, 'string'); diff --git a/src/server.js b/src/server.js index c81465362..b6e2a1817 100644 --- a/src/server.js +++ b/src/server.js @@ -259,6 +259,7 @@ function initializeExpressSync() { router.post('/api/v1/apps/:id/configure/tags', appsManageScope, routes.apps.setTags); router.post('/api/v1/apps/:id/configure/icon', appsManageScope, routes.apps.setIcon); router.post('/api/v1/apps/:id/configure/memory_limit', appsManageScope, routes.apps.setMemoryLimit); + router.post('/api/v1/apps/:id/configure/cpu_shares', appsManageScope, routes.apps.setCpuShares); router.post('/api/v1/apps/:id/configure/automatic_backup', appsManageScope, routes.apps.setAutomaticBackup); router.post('/api/v1/apps/:id/configure/automatic_update', appsManageScope, routes.apps.setAutomaticUpdate); router.post('/api/v1/apps/:id/configure/reverse_proxy', appsManageScope, routes.apps.setReverseProxyConfig); diff --git a/src/test/apps-test.js b/src/test/apps-test.js index 6f90c36c6..9e4b1b756 100644 --- a/src/test/apps-test.js +++ b/src/test/apps-test.js @@ -113,6 +113,7 @@ describe('Apps', function () { portBindings: { PORT: 5678 }, accessRestriction: null, memoryLimit: 0, + cpuShares: 512, reverseProxyConfig: null, sso: false, mailboxDomain: DOMAIN_0.domain, @@ -137,6 +138,7 @@ describe('Apps', function () { portBindings: {}, accessRestriction: { users: [ 'someuser' ], groups: [ GROUP_0.id ] }, memoryLimit: 0, + cpuShares: 512, env: {}, dataDir: '', mailboxDomain: DOMAIN_0.domain, @@ -157,6 +159,7 @@ describe('Apps', function () { portBindings: {}, accessRestriction: { users: [ 'someuser', USER_0.id ], groups: [ GROUP_1.id ] }, memoryLimit: 0, + cpuShares: 512, reverseProxyConfig: null, sso: false, env: {}, @@ -232,6 +235,7 @@ describe('Apps', function () { expect(app.iconUrl).to.be(null); expect(app.fqdn).to.eql(APP_0.location + '.' + DOMAIN_0.domain); expect(app.memoryLimit).to.eql(0); + expect(app.cpuShares).to.eql(512); done(); }); }); diff --git a/src/test/database-test.js b/src/test/database-test.js index 34a2e48ff..ec9a47315 100644 --- a/src/test/database-test.js +++ b/src/test/database-test.js @@ -405,6 +405,7 @@ describe('database', function () { accessRestriction: null, lastBackupId: null, memoryLimit: 4294967296, + cpuShares: 1024, sso: true, debugMode: null, reverseProxyConfig: {}, @@ -983,6 +984,7 @@ describe('database', function () { health: null, accessRestriction: null, memoryLimit: 4294967296, + cpuShares: 256, sso: true, debugMode: null, reverseProxyConfig: {}, @@ -1015,6 +1017,7 @@ describe('database', function () { health: null, accessRestriction: { users: [ 'foobar' ] }, memoryLimit: 0, + cpuShares: 512, sso: true, debugMode: null, reverseProxyConfig: {}, @@ -1111,6 +1114,7 @@ describe('database', function () { APP_0.accessRestriction = ''; APP_0.httpPort = 1337; APP_0.memoryLimit = 1337; + APP_0.cpuShares = 1024; var data = { installationState: APP_0.installationState, @@ -1119,7 +1123,8 @@ describe('database', function () { manifest: APP_0.manifest, accessRestriction: APP_0.accessRestriction, httpPort: APP_0.httpPort, - memoryLimit: APP_0.memoryLimit + memoryLimit: APP_0.memoryLimit, + cpuShares: APP_0.cpuShares }; appdb.update(APP_0.id, data, function (error) {