diff --git a/docs/references/api.md b/docs/references/api.md index f1e581455..2915c2c9a 100644 --- a/docs/references/api.md +++ b/docs/references/api.md @@ -118,8 +118,7 @@ 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 - readonlyRootfs: // whether the app's rootfs is readonly + xFrameOptions: // set X-Frame-Options header, to control which websites can embed this app } ``` @@ -154,9 +153,6 @@ If `altDomain` is set, the app can be accessed from `https://`. `memoryLimit` is the maximum memory this app can use (in bytes) including swap. If set to 0, the app uses the `memoryLimit` value set in the manifest. If set to -1, the app gets unlimited memory. -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): @@ -470,8 +466,7 @@ 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 - readonlyRootfs: // whether the app's rootfs is readonly + xFrameOptions: // set X-Frame-Options header, to control which websites can embed this app ``` All values are optional. See [Install app](/references/api.html#install-app) API for field descriptions. diff --git a/migrations/20170119185310-appdb-add-readonlyRootfs.js b/migrations/20170119234504-appdb-add-debugModeJson.js similarity index 63% rename from migrations/20170119185310-appdb-add-readonlyRootfs.js rename to migrations/20170119234504-appdb-add-debugModeJson.js index 6a38a9301..0021dc263 100644 --- a/migrations/20170119185310-appdb-add-readonlyRootfs.js +++ b/migrations/20170119234504-appdb-add-debugModeJson.js @@ -1,14 +1,14 @@ dbm = dbm || require('db-migrate'); exports.up = function(db, callback) { - db.runSql('ALTER TABLE apps ADD COLUMN readonlyRootfs BOOLEAN DEFAULT 1', function (error) { + db.runSql('ALTER TABLE apps ADD COLUMN debugModeJson TEXT', function (error) { if (error) console.error(error); callback(error); }); }; exports.down = function(db, callback) { - db.runSql('ALTER TABLE apps DROP COLUMN readonlyRootfs', function (error) { + db.runSql('ALTER TABLE apps DROP COLUMN debugModeJson ', function (error) { if (error) console.error(error); callback(error); }); diff --git a/migrations/20170119234504-appdb-add-developmentMode.js b/migrations/20170119234504-appdb-add-developmentMode.js deleted file mode 100644 index a7e7fb570..000000000 --- a/migrations/20170119234504-appdb-add-developmentMode.js +++ /dev/null @@ -1,15 +0,0 @@ -dbm = dbm || require('db-migrate'); - -exports.up = function(db, callback) { - db.runSql('ALTER TABLE apps ADD COLUMN developmentMode BOOLEAN DEFAULT 0', function (error) { - if (error) console.error(error); - callback(error); - }); -}; - -exports.down = function(db, callback) { - db.runSql('ALTER TABLE apps DROP COLUMN developmentMode', function (error) { - if (error) console.error(error); - callback(error); - }); -}; diff --git a/migrations/schema.sql b/migrations/schema.sql index 389aba146..1b7ceb99f 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -67,7 +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 + debugModeJson TEXT, // options for development mode lastBackupId VARCHAR(128), // tracks last valid backup, can be removed diff --git a/src/appdb.js b/src/appdb.js index bae05f57d..f5c8b46f6 100644 --- a/src/appdb.js +++ b/src/appdb.js @@ -58,7 +58,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.readonlyRootfs', 'apps.developmentMode' ].join(','); + 'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson' ].join(','); var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(','); @@ -96,8 +96,10 @@ function postProcess(result) { result.xFrameOptions = result.xFrameOptions || 'SAMEORIGIN'; result.sso = !!result.sso; // make it bool - result.readonlyRootfs = !!result.readonlyRootfs; // make it bool - result.developmentMode = !!result.developmentMode; // make it bool + + assert(result.debugModeJson === null || typeof result.debugModeJson === 'string'); + result.debugMode = safe.JSON.parse(result.debugModeJson); + delete result.debugModeJson; } function get(id, callback) { @@ -185,13 +187,12 @@ function add(id, appStoreId, manifest, location, portBindings, data, callback) { var installationState = data.installationState || exports.ISTATE_PENDING_INSTALL; var lastBackupId = data.lastBackupId || null; // used when cloning var sso = 'sso' in data ? data.sso : null; - var readonlyRootfs = 'readonlyRootfs' in data ? data.readonlyRootfs : true; - var developmentMode = 'developmentMode' in data ? data.developmentMode : false; + var debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null; var queries = [ ]; queries.push({ - query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId, sso, readonlyRootfs, developmentMode) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', - args: [ id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId, sso, readonlyRootfs, developmentMode ] + query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId, sso, debugModeJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + args: [ id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId, sso, debugModeJson ] }); Object.keys(portBindings).forEach(function (env) { diff --git a/src/apps.js b/src/apps.js index ec3a36204..93e5cc290 100644 --- a/src/apps.js +++ b/src/apps.js @@ -230,6 +230,16 @@ function validateXFrameOptions(xFrameOptions) { return (uri.protocol === 'http:' || uri.protocol === 'https:') ? null : new AppsError(AppsError.BAD_FIELD, 'xFrameOptions ALLOW-FROM uri must be a valid http[s] uri' ); } +function validateDebugMode(debugMode) { + assert.strictEqual(typeof debugMode, 'object'); + + if (debugMode === null) return null; + if ('cmd' in debugMode && debugMode.cmd !== null && !Array.isArray(debugMode.cmd)) return new AppsError(AppsError.BAD_FIELD, 'debugMode.cmd must be an array or null' ); + if ('readonlyRootfs' in debugMode && typeof debugMode.readonlyRootfs !== 'boolean') return new AppsError(AppsError.BAD_FIELD, 'debugMode.readonlyRootfs must be a boolean' ); + + return null; +} + function getDuplicateErrorDetails(location, portBindings, error) { assert.strictEqual(typeof location, 'string'); assert.strictEqual(typeof portBindings, 'object'); @@ -480,8 +490,7 @@ function install(data, auditSource, callback) { altDomain = data.altDomain || null, xFrameOptions = data.xFrameOptions || 'SAMEORIGIN', sso = 'sso' in data ? data.sso : null, - readonlyRootfs = 'readonlyRootfs' in data ? data.readonlyRootfs : true, - developmentMode = 'developmentMode' in data ? data.developmentMode : false; + debugMode = data.debugMode || null; assert(data.appStoreId || data.manifest); // atleast one of them is required @@ -509,6 +518,9 @@ function install(data, auditSource, callback) { error = validateXFrameOptions(xFrameOptions); if (error) return callback(error); + error = validateDebugMode(debugMode); + if (error) return callback(error); + if ('sso' in data && !('optionalSso' in manifest)) return callback(new AppsError(AppsError.BAD_FIELD, 'sso can only be specified for apps with optionalSso')); // if sso was unspecified, enable it by default if possible if (sso === null) sso = !!manifest.addons['simpleauth'] || !!manifest.addons['ldap'] || !!manifest.addons['oauth']; @@ -539,8 +551,7 @@ function install(data, auditSource, callback) { altDomain: altDomain, xFrameOptions: xFrameOptions, sso: sso, - readonlyRootfs: readonlyRootfs, - developmentMode: developmentMode + debugMode: debugMode }; var from = (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app'; @@ -619,16 +630,10 @@ function configure(appId, data, auditSource, callback) { if (error) return callback(error); } - if ('readonlyRootfs' in data) { - values.readonlyRootfs = data.readonlyRootfs; - } else { - values.readonlyRootfs = app.readonlyRootfs; - } - - if ('developmentMode' in data) { - values.developmentMode = data.developmentMode; - } else { - values.developmentMode = app.developmentMode; + if ('debugMode' in data) { + values.debugMode = data.debugMode; + error = validateDebugMode(values.debugMode); + if (error) return callback(error); } // save cert to data/box/certs. TODO: move this to apptask when we have a real task queue @@ -722,7 +727,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')); + // do not update apps in debug mode + if (app.debugMode && !data.force) return callback(new AppsError(AppsError.BAD_STATE, 'debug mode enabled. force to override')); // Ensure we update the memory limit in case the new app requires more memory as a minimum // 0 and -1 are special values for memory limit indicating unset and unlimited diff --git a/src/docker.js b/src/docker.js index d75b42ff0..d2a26adf9 100644 --- a/src/docker.js +++ b/src/docker.js @@ -177,7 +177,7 @@ function createSubcontainer(app, name, cmd, options, callback) { name: name, // used for filtering logs Tty: isAppContainer, Image: app.manifest.dockerImage, - Cmd: (isAppContainer && app.developmentMode) ? [ '/bin/bash', '-c', 'echo "Development mode. Use cloudron exec to debug. Sleeping" && sleep infinity' ] : cmd, + Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd, Env: stdEnv.concat(addonEnv).concat(portEnv), ExposedPorts: isAppContainer ? exposedPorts : { }, Volumes: { // see also ReadonlyRootfs @@ -195,7 +195,7 @@ function createSubcontainer(app, name, cmd, options, callback) { MemorySwap: memoryLimit, // Memory + Swap PortBindings: isAppContainer ? dockerPortBindings : { }, PublishAllPorts: false, - ReadonlyRootfs: app.readonlyRootfs, + ReadonlyRootfs: app.debugMode ? !!app.debugMode.readonlyRootfs : true, RestartPolicy: { "Name": isAppContainer ? "always" : "no", "MaximumRetryCount": 0 diff --git a/src/routes/apps.js b/src/routes/apps.js index 59c96e7ad..9a11a439b 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -56,8 +56,7 @@ function removeInternalAppFields(app) { altDomain: app.altDomain, xFrameOptions: app.xFrameOptions, sso: app.sso, - readonlyRootfs: app.readonlyRootfs, - developmentMode: app.developmentMode + debugMode: app.debugMode }; } @@ -128,8 +127,7 @@ 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')); - if ('developmentMode' in data && typeof data.developmentMode !== 'boolean') return next(new HttpError(400, 'developmentMode is not a boolean')); + if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object')); debug('Installing app :%j', data); @@ -167,8 +165,7 @@ 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')); - if ('developmentMode' in data && typeof data.developmentMode !== 'boolean') return next(new HttpError(400, 'developmentMode is not a boolean')); + if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object')); debug('Configuring app id:%s data:%j', req.params.id, data); diff --git a/src/test/database-test.js b/src/test/database-test.js index 18d655d6e..f05cf969d 100644 --- a/src/test/database-test.js +++ b/src/test/database-test.js @@ -542,8 +542,7 @@ describe('database', function () { altDomain: null, xFrameOptions: 'DENY', sso: true, - readonlyRootfs: true, - developmentMode: false + debugMode: null }; var APP_1 = { id: 'appid-1', @@ -565,8 +564,7 @@ describe('database', function () { altDomain: null, xFrameOptions: 'SAMEORIGIN', sso: true, - readonlyRootfs: true, - developmentMode: false + debugMode: null }; it('add fails due to missing arguments', function () {