From 6d400050a337551e38072c135bbb86800dba92be Mon Sep 17 00:00:00 2001 From: muxator Date: Sat, 9 Mar 2019 23:01:21 +0100 Subject: [PATCH] Settings.js: support configuration via environment variables. All the configuration values can be read from environment variables using the syntax "${ENV_VAR_NAME}". This is useful, for example, when running in a Docker container. EXAMPLE: "port": "${PORT}" "minify": "${MINIFY}" "skinName": "${SKIN_NAME}" Would read the configuration values for those items from the environment variables PORT, MINIFY and SKIN_NAME. REMARKS: Please note that a variable substitution always needs to be quoted. "port": 9001, <-- Literal values. When not using substitution, "minify": false only strings must be quoted: booleans and "skin": "colibris" numbers must not. "port": ${PORT} <-- ERROR: this is not valid json "minify": ${MINIFY} "skin": ${SKIN_NAME} "port": "${PORT}" <-- CORRECT: if you want to use a variable "minify": "${MINIFY}" substitution, put quotes around its name, "skin": "${SKIN_NAME}" even if the required value is a number or a boolean. Etherpad will take care of rewriting it to the proper type if necessary. Resolves #3543 --- docker/README.md | 2 + settings.json.template | 33 +++++++++++ src/node/utils/Settings.js | 113 ++++++++++++++++++++++++++++++++++++- 3 files changed, 147 insertions(+), 1 deletion(-) diff --git a/docker/README.md b/docker/README.md index 2c5ad028c..87d6e81ea 100644 --- a/docker/README.md +++ b/docker/README.md @@ -14,6 +14,8 @@ cp ../settings.json.template settings.json [ further edit your settings.json as needed] ``` +**Each configuration parameter can also be set via an environment variable**, using the syntax `"${ENV_VAR_NAME}"`. For details, refer to `settings.json.template`. + Build the version you prefer: ```bash # builds latest development version diff --git a/settings.json.template b/settings.json.template index 336d0ebc8..50a095bac 100644 --- a/settings.json.template +++ b/settings.json.template @@ -5,6 +5,39 @@ * * Please note that starting from Etherpad 1.6.0 you can store DB credentials in * a separate file (credentials.json). + * + * + * ENVIRONMENT VARIABLE SUBSTITUTION + * ================================= + * + * All the configuration values can be read from environment variables using the + * syntax "${ENV_VAR_NAME}". + * This is useful, for example, when running in a Docker container. + * + * EXAMPLE: + * "port": "${PORT}" + * "minify": "${MINIFY}" + * "skinName": "${SKIN_NAME}" + * + * Would read the configuration values for those items from the environment + * variables PORT, MINIFY and SKIN_NAME. + * + * REMARKS: + * Please note that a variable substitution always needs to be quoted. + * "port": 9001, <-- Literal values. When not using substitution, + * "minify": false only strings must be quoted: booleans and + * "skin": "colibris" numbers must not. + * + * "port": ${PORT} <-- ERROR: this is not valid json + * "minify": ${MINIFY} + * "skin": ${SKIN_NAME} + * + * "port": "${PORT}" <-- CORRECT: if you want to use a variable + * "minify": "${MINIFY}" substitution, put quotes around its name, + * "skin": "${SKIN_NAME}" even if the required value is a number or a + * boolean. + * Etherpad will take care of rewriting it to + * the proper type if necessary. */ { /* diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 412eb7cd8..d013d4222 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -370,9 +370,118 @@ function storeSettings(settingsObj) { } } +/** + * Takes a javascript object containing Etherpad's configuration, and returns + * another object, in which all the string properties whose name is of the form + * "${ENV_VAR}", got their value replaced with the value of the given + * environment variable. + * + * An environment variable's value is always a string. However, the code base + * makes use of the various json types. To maintain compatiblity, some + * heuristics is applied: + * + * - if ENV_VAR does not exist in the environment, null is returned; + * - if ENV_VAR's value is "true" or "false", it is converted to the js boolean + * values true or false; + * - if ENV_VAR's value looks like a number, it is converted to a js number + * (details in the code). + * + * Variable substitution is performed doing a round trip conversion to/from + * json, using a custom replacer parameter in JSON.stringify(), and parsing the + * JSON back again. This ensures that environment variable replacement is + * performed even on nested objects. + * + * see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter + */ +function lookupEnvironmentVariables(obj) { + const stringifiedAndReplaced = JSON.stringify(obj, (key, value) => { + /* + * the first invocation of replacer() is with an empty key. Just go on, or + * we would zap the entire object. + */ + if (key === '') { + return value; + } + + /* + * If we received from the configuration file a number, a boolean or + * something that is not a string, we can be sure that it was a literal + * value. No need to perform any variable substitution. + * + * The environment variable expansion syntax "${ENV_VAR}" is just a string + * of specific form, after all. + */ + if (typeof value !== 'string') { + return value; + } + + /* + * Let's check if the string value looks like a variable expansion (e.g.: + * "${ENV_VAR}") + */ + const match = value.match(/^\$\{(.*)\}$/); + + if (match === null) { + // no match: use the value literally, without any substitution + + return value; + } + + // we found the name of an environment variable. Let's read its value. + const envVarName = match[1]; + const envVarValue = process.env[envVarName]; + + if (envVarValue === undefined) { + console.warn(`Configuration key ${key} tried to read its value from environment variable ${envVarName}, but no value was found. Returning null. Please check your configuration and environment settings.`); + + /* + * We have to return null, because if we just returned undefined, the + * configuration item "key" would be stripped from the returned object. + */ + return null; + } + + // envVarName contained some value. + + /* + * For numeric and boolean strings let's convert it to proper types before + * returning it, in order to maintain backward compatibility. + */ + + // cooked from https://stackoverflow.com/questions/175739/built-in-way-in-javascript-to-check-if-a-string-is-a-valid-number + const isNumeric = !isNaN(envVarValue) && !isNaN(parseFloat(envVarValue) && isFinite(envVarValue)); + + if (isNumeric) { + console.debug(`Configuration key "${key}" will be read from environment variable ${envVarName}. Detected numeric string, that will be coerced to a number`); + + return +envVarValue; + } + + // the boolean literal case is easy. + if (envVarValue === "true" || envVarValue === "false") { + console.debug(`Configuration key "${key}" will be read from environment variable ${envVarName}. Detected boolean string, that will be coerced to a boolean`); + + return (envVarValue === "true"); + } + + /* + * The only remaining case is that envVarValue is a string with no special + * meaning, and we just return it as-is. + */ + console.debug(`Configuration key "${key}" will be read from environment variable ${envVarName}`); + + return envVarValue; + }); + + const newSettings = JSON.parse(stringifiedAndReplaced); + + return newSettings; +} + /** * - reads the JSON configuration file settingsFilename from disk * - strips the comments + * - replaces environment variables calling lookupEnvironmentVariables() * - returns a parsed Javascript object * * The isSettings variable only controls the error logging. @@ -409,7 +518,9 @@ function parseSettings(settingsFilename, isSettings) { console.info(`${settingsType} loaded from: ${settingsFilename}`); - return settings; + const replacedSettings = lookupEnvironmentVariables(settings); + + return replacedSettings; } catch(e) { console.error(`There was an error processing your ${settingsType} file from ${settingsFilename}: ${e.message}`);