From dc8aab1dc0ceb5181d1c8676fa54b161113a2aed Mon Sep 17 00:00:00 2001 From: muxator <a.mux@inwind.it> Date: Sun, 10 Mar 2019 02:11:45 +0100 Subject: [PATCH 1/7] settings.json.template: minor rewording of a comment --- settings.json.template | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/settings.json.template b/settings.json.template index a89cc4247cb..336d0ebc85f 100644 --- a/settings.json.template +++ b/settings.json.template @@ -3,8 +3,8 @@ * * Please edit settings.json, not settings.json.template * - * Please note that since Etherpad 1.6.0 you can store DB credentials in a - * separate file (credentials.json). + * Please note that starting from Etherpad 1.6.0 you can store DB credentials in + * a separate file (credentials.json). */ { /* From 5acc122e84fb7b6ae0f72109fd82dbcbd6b6621d Mon Sep 17 00:00:00 2001 From: muxator <a.mux@inwind.it> Date: Sun, 10 Mar 2019 01:46:55 +0100 Subject: [PATCH 2/7] Settings.js: trivial reformatting --- src/node/utils/Settings.js | 87 +++++++++++++++----------------------- 1 file changed, 35 insertions(+), 52 deletions(-) diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 508e6148d4c..91f4c67806e 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -287,29 +287,26 @@ exports.scrollWhenFocusLineIsOutOfViewport = { //checks if abiword is avaiable exports.abiwordAvailable = function() { - if(exports.abiword != null) - { + if (exports.abiword != null) { return os.type().indexOf("Windows") != -1 ? "withoutPDF" : "yes"; - } - else - { + } else { return "no"; } }; -exports.sofficeAvailable = function () { - if(exports.soffice != null) { +exports.sofficeAvailable = function() { + if (exports.soffice != null) { return os.type().indexOf("Windows") != -1 ? "withoutPDF": "yes"; } else { return "no"; } }; -exports.exportAvailable = function () { +exports.exportAvailable = function() { var abiword = exports.abiwordAvailable(); var soffice = exports.sofficeAvailable(); - if(abiword == "no" && soffice == "no") { + if (abiword == "no" && soffice == "no") { return "no"; } else if ((abiword == "withoutPDF" && soffice == "no") || (abiword == "no" && soffice == "withoutPDF")) { return "withoutPDF"; @@ -321,8 +318,7 @@ exports.exportAvailable = function () { // Provide git version if available exports.getGitCommit = function() { var version = ""; - try - { + try { var rootPath = path.resolve(npm.dir, '..'); if (fs.lstatSync(rootPath + '/.git').isFile()) { rootPath = fs.readFileSync(rootPath + '/.git', "utf8"); @@ -334,9 +330,7 @@ exports.getGitCommit = function() { var refPath = rootPath + "/" + ref.substring(5, ref.indexOf("\n")); version = fs.readFileSync(refPath, "utf-8"); version = version.substring(0, 7); - } - catch(e) - { + } catch(e) { console.warn("Can't get git version for server header\n" + e.message) } return version; @@ -350,24 +344,24 @@ exports.getEpVersion = function() { exports.reloadSettings = function reloadSettings() { // Discover where the settings file lives var settingsFilename = absolutePaths.makeAbsolute(argv.settings || "settings.json"); - + // Discover if a credential file exists var credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || "credentials.json"); var settingsStr, credentialsStr; - try{ + try { //read the settings sync settingsStr = fs.readFileSync(settingsFilename).toString(); console.info(`Settings loaded from: ${settingsFilename}`); - } catch(e){ + } catch(e) { console.warn(`No settings file found in ${settingsFilename}. Continuing using defaults!`); } - try{ + try { //read the credentials sync credentialsStr = fs.readFileSync(credentialsFilename).toString(); console.info(`Credentials file read from: ${credentialsFilename}`); - } catch(e){ + } catch(e) { // Doesn't matter if no credentials file found.. console.info(`No credentials file found in ${credentialsFilename}. Ignoring.`); } @@ -376,68 +370,58 @@ exports.reloadSettings = function reloadSettings() { var settings; var credentials; try { - if(settingsStr) { + if (settingsStr) { settingsStr = jsonminify(settingsStr).replace(",]","]").replace(",}","}"); settings = JSON.parse(settingsStr); } - }catch(e){ + } catch(e) { console.error(`There was an error processing your settings file from ${settingsFilename}:` + e.message); process.exit(1); } - if(credentialsStr) { + if (credentialsStr) { credentialsStr = jsonminify(credentialsStr).replace(",]","]").replace(",}","}"); credentials = JSON.parse(credentialsStr); } //loop trough the settings - for(var i in settings) - { + for (var i in settings) { //test if the setting start with a lowercase character - if(i.charAt(0).search("[a-z]") !== 0) - { + if (i.charAt(0).search("[a-z]") !== 0) { console.warn(`Settings should start with a lowercase character: '${i}'`); } //we know this setting, so we overwrite it //or it's a settings hash, specific to a plugin - if(exports[i] !== undefined || i.indexOf('ep_')==0) - { + if (exports[i] !== undefined || i.indexOf('ep_') == 0) { if (_.isObject(settings[i]) && !_.isArray(settings[i])) { exports[i] = _.defaults(settings[i], exports[i]); } else { exports[i] = settings[i]; } - } - //this setting is unkown, output a warning and throw it away - else - { + } else { + // this setting is unknown, output a warning and throw it away console.warn(`Unknown Setting: '${i}'. This setting doesn't exist or it was removed`); } } //loop trough the settings - for(var i in credentials) - { + for (var i in credentials) { //test if the setting start with a lowercase character - if(i.charAt(0).search("[a-z]") !== 0) - { + if (i.charAt(0).search("[a-z]") !== 0) { console.warn(`Settings should start with a lowercase character: '${i}'`); } //we know this setting, so we overwrite it //or it's a settings hash, specific to a plugin - if(exports[i] !== undefined || i.indexOf('ep_')==0) - { + if (exports[i] !== undefined || i.indexOf('ep_') == 0) { if (_.isObject(credentials[i]) && !_.isArray(credentials[i])) { exports[i] = _.defaults(credentials[i], exports[i]); } else { exports[i] = credentials[i]; } - } - //this setting is unkown, output a warning and throw it away - else - { + } else { + // this setting is unknown, output a warning and throw it away console.warn(`Unknown Setting: '${i}'. This setting doesn't exist or it was removed`); } } @@ -483,14 +467,13 @@ exports.reloadSettings = function reloadSettings() { console.info(`Using skin "${exports.skinName}" in dir: ${skinPath}`); } - if(exports.abiword){ + if (exports.abiword) { // Check abiword actually exists - if(exports.abiword != null) - { + if (exports.abiword != null) { fs.exists(exports.abiword, function(exists) { if (!exists) { var abiwordError = "Abiword does not exist at this path, check your settings file"; - if(!exports.suppressErrorsInPadText){ + if (!exports.suppressErrorsInPadText) { exports.defaultPadText = exports.defaultPadText + "\nError: " + abiwordError + suppressDisableMsg; } console.error(abiwordError); @@ -500,12 +483,12 @@ exports.reloadSettings = function reloadSettings() { } } - if(exports.soffice) { - fs.exists(exports.soffice, function (exists) { - if(!exists) { + if (exports.soffice) { + fs.exists(exports.soffice, function(exists) { + if (!exists) { var sofficeError = "SOffice does not exist at this path, check your settings file"; - if(!exports.suppressErrorsInPadText) { + if (!exports.suppressErrorsInPadText) { exports.defaultPadText = exports.defaultPadText + "\nError: " + sofficeError + suppressDisableMsg; } console.error(sofficeError); @@ -528,9 +511,9 @@ exports.reloadSettings = function reloadSettings() { console.warn("Declaring the sessionKey in the settings.json is deprecated. This value is auto-generated now. Please remove the setting from the file."); } - if(exports.dbType === "dirty"){ + if (exports.dbType === "dirty") { var dirtyWarning = "DirtyDB is used. This is fine for testing but not recommended for production."; - if(!exports.suppressErrorsInPadText){ + if (!exports.suppressErrorsInPadText) { exports.defaultPadText = exports.defaultPadText + "\nWarning: " + dirtyWarning + suppressDisableMsg; } From 68073c0e23e5085ea358b1e5bd58ec632d6dd682 Mon Sep 17 00:00:00 2001 From: muxator <a.mux@inwind.it> Date: Sat, 9 Mar 2019 08:59:39 +0100 Subject: [PATCH 3/7] Settings.js: trivial rewording of abiword and soffice (libreoffice) error messages --- src/node/utils/Settings.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 91f4c67806e..206413c8496 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -472,11 +472,11 @@ exports.reloadSettings = function reloadSettings() { if (exports.abiword != null) { fs.exists(exports.abiword, function(exists) { if (!exists) { - var abiwordError = "Abiword does not exist at this path, check your settings file"; + var abiwordError = "Abiword does not exist at this path, check your settings file."; if (!exports.suppressErrorsInPadText) { exports.defaultPadText = exports.defaultPadText + "\nError: " + abiwordError + suppressDisableMsg; } - console.error(abiwordError); + console.error(abiwordError + ` File location: ${exports.abiword}`); exports.abiword = null; } }); @@ -486,12 +486,12 @@ exports.reloadSettings = function reloadSettings() { if (exports.soffice) { fs.exists(exports.soffice, function(exists) { if (!exists) { - var sofficeError = "SOffice does not exist at this path, check your settings file"; + var sofficeError = "soffice (libreoffice) does not exist at this path, check your settings file."; if (!exports.suppressErrorsInPadText) { exports.defaultPadText = exports.defaultPadText + "\nError: " + sofficeError + suppressDisableMsg; } - console.error(sofficeError); + console.error(sofficeError + ` File location: ${exports.soffice}`); exports.soffice = null; } }); From 72c609f8dfcb4eb8c834fd5e681f6cf6d4cd9748 Mon Sep 17 00:00:00 2001 From: muxator <a.mux@inwind.it> Date: Sun, 10 Mar 2019 00:26:36 +0100 Subject: [PATCH 4/7] Settings.js: exit gracefully if an invalid credentials.json is passed. Before this commit, when passed a malformed credentials.json the application crashed with a stack dump. Now we catch the error and fail in a controlled way (like already done for settings.json). Example of exception we no longer throw: MALFORMEDJSON ^ SyntaxError: Unexpected token M in JSON at position 0 at JSON.parse (<anonymous>) at Object.reloadSettings (<BASEDIR>/src/node/utils/Settings.js:390:24) at Object.<anonymous> (<BASEDIR>/src/node/utils/Settings.js:543:9) at Module._compile (module.js:635:30) at Object.Module._extensions..js (module.js:646:10) at Module.load (module.js:554:32) at tryModuleLoad (module.js:497:12) at Function.Module._load (module.js:489:3) at Module.require (module.js:579:17) at require (internal/module.js:11:18) --- src/node/utils/Settings.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 206413c8496..98756c27f8c 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -379,9 +379,14 @@ exports.reloadSettings = function reloadSettings() { process.exit(1); } - if (credentialsStr) { - credentialsStr = jsonminify(credentialsStr).replace(",]","]").replace(",}","}"); - credentials = JSON.parse(credentialsStr); + try { + if (credentialsStr) { + credentialsStr = jsonminify(credentialsStr).replace(",]","]").replace(",}","}"); + credentials = JSON.parse(credentialsStr); + } + } catch(e) { + console.error(`There was an error processing your credentials file from ${credentialsFilename}:` + e.message); + process.exit(1); } //loop trough the settings From f696916f39eb469571a8cbac1c485cd0a64bfe8d Mon Sep 17 00:00:00 2001 From: muxator <a.mux@inwind.it> Date: Sat, 9 Mar 2019 10:06:51 +0100 Subject: [PATCH 5/7] Settings.js: factored out storeSettings() Grouped copied & pasted code into a single function. --- src/node/utils/Settings.js | 74 ++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 43 deletions(-) diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 98756c27f8c..4311bf3daf3 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -341,6 +341,35 @@ exports.getEpVersion = function() { return require('ep_etherpad-lite/package.json').version; } +/** + * Receives a settingsObj and, if the property name is a valid configuration + * item, stores it in the module's exported properties via a side effect. + * + * This code refactors a previous version that copied & pasted the same code for + * both "settings.json" and "credentials.json". + */ +function storeSettings(settingsObj) { + for (var i in settingsObj) { + // test if the setting starts with a lowercase character + if (i.charAt(0).search("[a-z]") !== 0) { + console.warn(`Settings should start with a lowercase character: '${i}'`); + } + + // we know this setting, so we overwrite it + // or it's a settings hash, specific to a plugin + if (exports[i] !== undefined || i.indexOf('ep_') == 0) { + if (_.isObject(settingsObj[i]) && !_.isArray(settingsObj[i])) { + exports[i] = _.defaults(settingsObj[i], exports[i]); + } else { + exports[i] = settingsObj[i]; + } + } else { + // this setting is unknown, output a warning and throw it away + console.warn(`Unknown Setting: '${i}'. This setting doesn't exist or it was removed`); + } + } +} + exports.reloadSettings = function reloadSettings() { // Discover where the settings file lives var settingsFilename = absolutePaths.makeAbsolute(argv.settings || "settings.json"); @@ -389,47 +418,8 @@ exports.reloadSettings = function reloadSettings() { process.exit(1); } - //loop trough the settings - for (var i in settings) { - //test if the setting start with a lowercase character - if (i.charAt(0).search("[a-z]") !== 0) { - console.warn(`Settings should start with a lowercase character: '${i}'`); - } - - //we know this setting, so we overwrite it - //or it's a settings hash, specific to a plugin - if (exports[i] !== undefined || i.indexOf('ep_') == 0) { - if (_.isObject(settings[i]) && !_.isArray(settings[i])) { - exports[i] = _.defaults(settings[i], exports[i]); - } else { - exports[i] = settings[i]; - } - } else { - // this setting is unknown, output a warning and throw it away - console.warn(`Unknown Setting: '${i}'. This setting doesn't exist or it was removed`); - } - } - - //loop trough the settings - for (var i in credentials) { - //test if the setting start with a lowercase character - if (i.charAt(0).search("[a-z]") !== 0) { - console.warn(`Settings should start with a lowercase character: '${i}'`); - } - - //we know this setting, so we overwrite it - //or it's a settings hash, specific to a plugin - if (exports[i] !== undefined || i.indexOf('ep_') == 0) { - if (_.isObject(credentials[i]) && !_.isArray(credentials[i])) { - exports[i] = _.defaults(credentials[i], exports[i]); - } else { - exports[i] = credentials[i]; - } - } else { - // this setting is unknown, output a warning and throw it away - console.warn(`Unknown Setting: '${i}'. This setting doesn't exist or it was removed`); - } - } + storeSettings(settings); + storeSettings(credentials); log4js.configure(exports.logconfig);//Configure the logging appenders log4js.setGlobalLogLevel(exports.loglevel);//set loglevel @@ -529,5 +519,3 @@ exports.reloadSettings = function reloadSettings() { // initially load settings exports.reloadSettings(); - - From 824d61aa142cf4ce7299add865fbb53c6205ccfd Mon Sep 17 00:00:00 2001 From: muxator <a.mux@inwind.it> Date: Sun, 10 Mar 2019 00:36:53 +0100 Subject: [PATCH 6/7] Settings.js: factored out parseSettings() No functional changes. --- src/node/utils/Settings.js | 80 ++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 4311bf3daf3..412eb7cd809 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -370,53 +370,65 @@ function storeSettings(settingsObj) { } } -exports.reloadSettings = function reloadSettings() { - // Discover where the settings file lives - var settingsFilename = absolutePaths.makeAbsolute(argv.settings || "settings.json"); +/** + * - reads the JSON configuration file settingsFilename from disk + * - strips the comments + * - returns a parsed Javascript object + * + * The isSettings variable only controls the error logging. + */ +function parseSettings(settingsFilename, isSettings) { + let settingsStr = ""; - // Discover if a credential file exists - var credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || "credentials.json"); + let settingsType, notFoundMessage, notFoundFunction; - var settingsStr, credentialsStr; - try { - //read the settings sync - settingsStr = fs.readFileSync(settingsFilename).toString(); - console.info(`Settings loaded from: ${settingsFilename}`); - } catch(e) { - console.warn(`No settings file found in ${settingsFilename}. Continuing using defaults!`); + if (isSettings) { + settingsType = "settings"; + notFoundMessage = "Continuing using defaults!"; + notFoundFunction = console.warn; + } else { + settingsType = "credentials"; + notFoundMessage = "Ignoring."; + notFoundFunction = console.info; } try { - //read the credentials sync - credentialsStr = fs.readFileSync(credentialsFilename).toString(); - console.info(`Credentials file read from: ${credentialsFilename}`); + //read the settings file + settingsStr = fs.readFileSync(settingsFilename).toString(); } catch(e) { - // Doesn't matter if no credentials file found.. - console.info(`No credentials file found in ${credentialsFilename}. Ignoring.`); - } + notFoundFunction(`No ${settingsType} file found in ${settingsFilename}. ${notFoundMessage}`); - // try to parse the settings - var settings; - var credentials; - try { - if (settingsStr) { - settingsStr = jsonminify(settingsStr).replace(",]","]").replace(",}","}"); - settings = JSON.parse(settingsStr); - } - } catch(e) { - console.error(`There was an error processing your settings file from ${settingsFilename}:` + e.message); - process.exit(1); + // or maybe undefined! + return null; } try { - if (credentialsStr) { - credentialsStr = jsonminify(credentialsStr).replace(",]","]").replace(",}","}"); - credentials = JSON.parse(credentialsStr); - } + settingsStr = jsonminify(settingsStr).replace(",]","]").replace(",}","}"); + + const settings = JSON.parse(settingsStr); + + console.info(`${settingsType} loaded from: ${settingsFilename}`); + + return settings; } catch(e) { - console.error(`There was an error processing your credentials file from ${credentialsFilename}:` + e.message); + console.error(`There was an error processing your ${settingsType} file from ${settingsFilename}: ${e.message}`); + process.exit(1); } +} + +exports.reloadSettings = function reloadSettings() { + // Discover where the settings file lives + var settingsFilename = absolutePaths.makeAbsolute(argv.settings || "settings.json"); + + // Discover if a credential file exists + var credentialsFilename = absolutePaths.makeAbsolute(argv.credentials || "credentials.json"); + + // try to parse the settings + var settings = parseSettings(settingsFilename, true); + + // try to parse the credentials + var credentials = parseSettings(credentialsFilename, false); storeSettings(settings); storeSettings(credentials); From 3b6b12e8482bb641252c2aa8fff225d61cd5225b Mon Sep 17 00:00:00 2001 From: muxator <a.mux@inwind.it> Date: Sat, 9 Mar 2019 23:01:21 +0100 Subject: [PATCH 7/7] 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 2c5ad028c93..87d6e81eaca 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 336d0ebc85f..50a095bac65 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 412eb7cd809..d013d422294 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}`);