Add binds support to containers

This commit is contained in:
Girish Ramakrishnan
2020-04-29 21:55:21 -07:00
parent 66f66fd14f
commit e9d0ed8e1e
10 changed files with 109 additions and 3 deletions
+1
View File
@@ -1923,4 +1923,5 @@
* Add UI to download backup config as JSON (and import it)
* Ensure stopped apps are getting backed up
* Add OVH Object Storage backend
* Add bind mounts (aka volumes)
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN bindsJson TEXT', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN bindsJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
+1
View File
@@ -86,6 +86,7 @@ CREATE TABLE IF NOT EXISTS apps(
dataDir VARCHAR(256) UNIQUE,
taskId INTEGER, // current task
errorJson TEXT,
bindsJson TEXT, // bind mounts
FOREIGN KEY(mailboxDomain) REFERENCES domains(domain),
FOREIGN KEY(taskId) REFERENCES tasks(id),
+5
View File
@@ -20,6 +20,11 @@ readonly ubuntu_version=$(lsb_release -rs)
cp -f "${script_dir}/../scripts/cloudron-support" /usr/bin/cloudron-support
# this needs to match the cloudron/base:2.0.0 gid
if ! getent group media; then
addgroup --gid 500 --system media
fi
echo "==> Configuring docker"
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
systemctl enable apparmor
+6 -2
View File
@@ -41,7 +41,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.cpuShares',
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.serviceConfigJson',
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.serviceConfigJson', 'apps.bindsJson',
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate',
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
@@ -98,6 +98,10 @@ function postProcess(result) {
result.serviceConfig = safe.JSON.parse(result.serviceConfigJson) || {};
delete result.serviceConfigJson;
assert(result.bindsJson === null || typeof result.bindsJson === 'string');
result.binds = safe.JSON.parse(result.bindsJson) || {};
delete result.bindsJson;
result.alternateDomains = result.alternateDomains || [];
result.alternateDomains.forEach(function (d) {
delete d.appId;
@@ -431,7 +435,7 @@ function updateWithConstraints(id, app, constraints, callback) {
var fields = [ ], values = [ ];
for (var p in app) {
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'serviceConfig') {
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'serviceConfig' || p === 'binds') {
fields.push(`${p}Json = ?`);
values.push(JSON.stringify(app[p]));
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
+42 -1
View File
@@ -20,6 +20,7 @@ exports = module.exports = {
setTags: setTags,
setMemoryLimit: setMemoryLimit,
setCpuShares: setCpuShares,
setBinds: setBinds,
setAutomaticBackup: setAutomaticBackup,
setAutomaticUpdate: setAutomaticUpdate,
setReverseProxyConfig: setReverseProxyConfig,
@@ -332,6 +333,20 @@ function validateEnv(env) {
return null;
}
function validateBinds(binds) {
for (let name of Object.keys(binds)) {
// just have friendly characters under /media
if (!/^[-0-9a-zA-Z_@$=#.%+]+$/.test(name)) return new BoxError(BoxError.BAD_FIELD, 'Invalid bind name');
const bind = binds[name];
if (!bind.hostPath.startsWith('/mnt') && !bind.hostPath.startsWith('/media')) return new BoxError(BoxError.BAD_FIELD, 'hostPath must be in /mnt or /media');
if (path.normalize(bind.hostPath) !== bind.hostPath) return new BoxError(BoxError.BAD_FIELD, 'hostPath is not normalized');
}
return null;
}
function validateDataDir(dataDir) {
if (dataDir === null) return null;
@@ -404,7 +419,7 @@ function removeInternalFields(app) {
'location', 'domain', 'fqdn', 'mailboxName', 'mailboxDomain',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares',
'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
'label', 'alternateDomains', 'env', 'enableAutomaticUpdate', 'dataDir');
'label', 'alternateDomains', 'env', 'enableAutomaticUpdate', 'dataDir', 'binds');
}
// non-admins can only see these
@@ -968,6 +983,32 @@ function setCpuShares(app, cpuShares, auditSource, callback) {
});
}
function setBinds(app, binds, auditSource, callback) {
assert.strictEqual(typeof app, 'object');
assert(binds && typeof binds === 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER);
if (error) return callback(error);
error = validateBinds(binds);
if (error) return callback(error);
const task = {
args: {},
values: { binds }
};
addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, binds, taskId: result.taskId });
callback(null, { taskId: result.taskId });
});
}
function setEnvironment(app, env, auditSource, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof env, 'object');
+14
View File
@@ -188,6 +188,19 @@ function downloadImage(manifest, callback) {
}, callback);
}
function getBindsSync(app) {
assert.strictEqual(typeof app, 'object');
let binds = [];
for (let name of Object.keys(app.binds)) {
const bind = app.binds[name];
binds.push(`${bind.hostPath}:/media/${name}:${bind.readOnly ? 'ro' : 'rw'}`);
}
return binds;
}
function createSubcontainer(app, name, cmd, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
@@ -277,6 +290,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
},
HostConfig: {
Mounts: addons.getMountsSync(app, app.manifest.addons),
Binds: getBindsSync(app), // ideally, we have to use 'Mounts' but we have to create volumes then
LogConfig: {
Type: 'syslog',
Config: {
+21
View File
@@ -30,6 +30,7 @@ exports = module.exports = {
setMailbox: setMailbox,
setLocation: setLocation,
setDataDir: setDataDir,
setBinds: setBinds,
stop: stop,
start: start,
@@ -764,3 +765,23 @@ function downloadFile(req, res, next) {
stream.pipe(res);
});
}
function setBinds(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
if (!req.body.binds || typeof req.body.binds !== 'object') return next(new HttpError(400, 'binds should be an object'));
for (let name of Object.keys(req.body.binds)) {
if (!req.body.binds[name] || typeof req.body.binds[name] !== 'object') return next(new HttpError(400, 'each bind should be an object'));
if (typeof req.body.binds[name].hostPath !== 'string') return next(new HttpError(400, 'hostPath must be a string'));
if (typeof req.body.binds[name].readOnly !== 'boolean') return next(new HttpError(400, 'readOnly must be a boolean'));
}
apps.setBinds(req.resource, req.body.binds, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
+1
View File
@@ -212,6 +212,7 @@ function initializeExpressSync() {
router.post('/api/v1/apps/:id/configure/env', token, authorizeAdmin, routes.apps.load, routes.apps.setEnvironment);
router.post('/api/v1/apps/:id/configure/data_dir', token, authorizeAdmin, routes.apps.load, routes.apps.setDataDir);
router.post('/api/v1/apps/:id/configure/location', token, authorizeAdmin, routes.apps.load, routes.apps.setLocation);
router.post('/api/v1/apps/:id/configure/binds', token, authorizeAdmin, routes.apps.load, routes.apps.setBinds);
router.post('/api/v1/apps/:id/repair', token, authorizeAdmin, routes.apps.load, routes.apps.repair);
router.post('/api/v1/apps/:id/update', token, authorizeAdmin, routes.apps.load, routes.apps.update);
+3
View File
@@ -419,6 +419,7 @@ describe('database', function () {
tags: [],
label: null,
taskId: null,
binds: {},
serviceConfig: {}
};
@@ -891,6 +892,7 @@ describe('database', function () {
tags: [],
label: null,
taskId: null,
binds: {},
serviceConfig: {}
};
@@ -923,6 +925,7 @@ describe('database', function () {
tags: [],
label: null,
taskId: null,
binds: {},
serviceConfig: {}
};