diff --git a/docs/references/api.md b/docs/references/api.md index 3997833e4..8fed6cd95 100644 --- a/docs/references/api.md +++ b/docs/references/api.md @@ -118,7 +118,8 @@ Request: key: , // pem encoded TLS key memoryLimit: , // memory constraint in bytes altDomain: , // alternate domain from which this app can be reached - xFrameOptions: // set X-Frame-Options header, to control which websites can embed this app + xFrameOptions: , // set X-Frame-Options header, to control which websites can embed this app + readonlyRootfs: // whether the app's rootfs is readonly } ``` @@ -151,6 +152,9 @@ If `altDomain` is set, the app can be accessed from `https://`. * `SAMEORIGIN` - allows embedding from the same domain as the app. This is the default. * `ALLOW-FROM https://example.com/` - allows this app to be embedded from example.com +If `readonlyRootfs` is false, then the app's rootfs can be modified post installation. This is useful for debugging as it allows the app's code to be modified post installation. Apps that have a readonly rootfs cannot +be updated to a newer version (because it is not safe to update them). + Read more about the options at [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options). Response (200): @@ -464,7 +468,8 @@ Request: key: , // pem encoded TLS key memoryLimit: , // memory constraint in bytes altDomain: , // alternate domain from which this app can be reached - xFrameOptions: // set X-Frame-Options header, to control which websites can embed this app + xFrameOptions: , // set X-Frame-Options header, to control which websites can embed this app + readonlyRootfs: // whether the app's rootfs is readonly ``` All values are optional. See [Install app](/references/api.html#install-app) API for field descriptions. diff --git a/migrations/20170119185310-appdb-add-readonly.js b/migrations/20170119185310-appdb-add-readonlyRootfs.js similarity index 60% rename from migrations/20170119185310-appdb-add-readonly.js rename to migrations/20170119185310-appdb-add-readonlyRootfs.js index 472e0a6b1..6a38a9301 100644 --- a/migrations/20170119185310-appdb-add-readonly.js +++ b/migrations/20170119185310-appdb-add-readonlyRootfs.js @@ -1,14 +1,14 @@ dbm = dbm || require('db-migrate'); exports.up = function(db, callback) { - db.runSql('ALTER TABLE apps ADD COLUMN readonly BOOLEAN DEFAULT 1', function (error) { + db.runSql('ALTER TABLE apps ADD COLUMN readonlyRootfs BOOLEAN DEFAULT 1', function (error) { if (error) console.error(error); callback(error); }); }; exports.down = function(db, callback) { - db.runSql('ALTER TABLE apps DROP COLUMN readonly', function (error) { + db.runSql('ALTER TABLE apps DROP COLUMN readonlyRootfs', function (error) { if (error) console.error(error); callback(error); }); diff --git a/migrations/schema.sql b/migrations/schema.sql index 9e879fcbd..389aba146 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -67,6 +67,7 @@ CREATE TABLE IF NOT EXISTS apps( altDomain VARCHAR(256), xFrameOptions VARCHAR(512), sso BOOLEAN DEFAULT 1, // whether user chose to enable SSO + readonlyRootfs BOOLEAN DEFAULT 1, // readonly rootfs lastBackupId VARCHAR(128), // tracks last valid backup, can be removed diff --git a/src/appdb.js b/src/appdb.js index c715a6907..b2c2e4050 100644 --- a/src/appdb.js +++ b/src/appdb.js @@ -60,7 +60,7 @@ var assert = require('assert'), var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState', 'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.dnsRecordId', 'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.oldConfigJson', 'apps.memoryLimit', 'apps.altDomain', - 'apps.xFrameOptions', 'apps.sso', 'apps.readonly' ].join(','); + 'apps.xFrameOptions', 'apps.sso', 'apps.readonlyRootfs' ].join(','); var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(','); @@ -98,6 +98,7 @@ function postProcess(result) { result.xFrameOptions = result.xFrameOptions || 'SAMEORIGIN'; result.sso = !!result.sso; // make it bool + result.readonlyRootfs = !!result.readonlyRootfs; // make it bool } function get(id, callback) { diff --git a/src/apps.js b/src/apps.js index 6a06e9e15..b14011479 100644 --- a/src/apps.js +++ b/src/apps.js @@ -476,7 +476,8 @@ function install(data, auditSource, callback) { memoryLimit = data.memoryLimit || 0, altDomain = data.altDomain || null, xFrameOptions = data.xFrameOptions || 'SAMEORIGIN', - sso = 'sso' in data ? data.sso : null; + sso = 'sso' in data ? data.sso : null, + readonlyRootfs = 'readonlyRootfs' in data ? data.readonlyRootfs : true; assert(data.appStoreId || data.manifest); // atleast one of them is required @@ -533,7 +534,8 @@ function install(data, auditSource, callback) { memoryLimit: memoryLimit, altDomain: altDomain, xFrameOptions: xFrameOptions, - sso: sso + sso: sso, + readonlyRootfs: readonlyRootfs }; var from = (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app'; @@ -612,6 +614,12 @@ function configure(appId, data, auditSource, callback) { if (error) return callback(error); } + if ('readonlyRootfs' in data) { + values.readonlyRootfs = data.readonlyRootfs; + } else { + values.readonlyRootfs = app.readonlyRootfs; + } + // save cert to data/box/certs. TODO: move this to apptask when we have a real task queue if ('cert' in data && 'key' in data) { if (data.cert && data.key) { @@ -703,6 +711,8 @@ function update(appId, data, auditSource, callback) { values.appStoreId = ''; } + if (!app.readonlyRootfs && !data.force) return callback(new AppsError(AppsError.BAD_STATE, 'rootfs is not readonly')); + // Ensure we update the memory limit in case the new app requires more memory as a minimum if (values.manifest.memoryLimit && app.memoryLimit < values.manifest.memoryLimit) { values.memoryLimit = values.manifest.memoryLimit; diff --git a/src/docker.js b/src/docker.js index 4d6462147..a6ab2b80e 100644 --- a/src/docker.js +++ b/src/docker.js @@ -197,7 +197,7 @@ function createSubcontainer(app, name, cmd, options, callback) { MemorySwap: memoryLimit, // Memory + Swap PortBindings: isAppContainer ? dockerPortBindings : { }, PublishAllPorts: false, - ReadonlyRootfs: !developmentMode, // see also Volumes in startContainer + ReadonlyRootfs: app.readonlyRootfs, RestartPolicy: { "Name": isAppContainer ? "always" : "no", "MaximumRetryCount": 0 diff --git a/src/routes/apps.js b/src/routes/apps.js index 88a3ee4ad..68bdf1479 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -126,6 +126,8 @@ function installApp(req, res, next) { if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean')); + if ('readonlyRootfs' in data && typeof data.readonlyRootfs !== 'boolean') return next(new HttpError(400, 'readonlyRootfs is not a boolean')); + debug('Installing app :%j', data); apps.install(data, auditSource(req), function (error, app) { @@ -162,6 +164,8 @@ function configureApp(req, res, next) { if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string')); if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string')); + if ('readonlyRootfs' in data && typeof data.readonlyRootfs !== 'boolean') return next(new HttpError(400, 'readonlyRootfs is not a boolean')); + debug('Configuring app id:%s data:%j', req.params.id, data); apps.configure(req.params.id, data, auditSource(req), function (error) {