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
This commit is contained in:
parent
f96e139b17
commit
6d400050a3
3 changed files with 147 additions and 1 deletions
|
@ -14,6 +14,8 @@ cp ../settings.json.template settings.json
|
||||||
[ further edit your settings.json as needed]
|
[ 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:
|
Build the version you prefer:
|
||||||
```bash
|
```bash
|
||||||
# builds latest development version
|
# builds latest development version
|
||||||
|
|
|
@ -5,6 +5,39 @@
|
||||||
*
|
*
|
||||||
* Please note that starting from Etherpad 1.6.0 you can store DB credentials in
|
* Please note that starting from Etherpad 1.6.0 you can store DB credentials in
|
||||||
* a separate file (credentials.json).
|
* 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.
|
||||||
*/
|
*/
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -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
|
* - reads the JSON configuration file settingsFilename from disk
|
||||||
* - strips the comments
|
* - strips the comments
|
||||||
|
* - replaces environment variables calling lookupEnvironmentVariables()
|
||||||
* - returns a parsed Javascript object
|
* - returns a parsed Javascript object
|
||||||
*
|
*
|
||||||
* The isSettings variable only controls the error logging.
|
* The isSettings variable only controls the error logging.
|
||||||
|
@ -409,7 +518,9 @@ function parseSettings(settingsFilename, isSettings) {
|
||||||
|
|
||||||
console.info(`${settingsType} loaded from: ${settingsFilename}`);
|
console.info(`${settingsType} loaded from: ${settingsFilename}`);
|
||||||
|
|
||||||
return settings;
|
const replacedSettings = lookupEnvironmentVariables(settings);
|
||||||
|
|
||||||
|
return replacedSettings;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(`There was an error processing your ${settingsType} file from ${settingsFilename}: ${e.message}`);
|
console.error(`There was an error processing your ${settingsType} file from ${settingsFilename}: ${e.message}`);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue