Add binds support to containers
This commit is contained in:
@@ -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);
|
||||
});
|
||||
};
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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
@@ -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');
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 }));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user