feat(backend): new config handling based on file

* Config should be provided as Yaml file now
* Other way to provide configuration are now deprectaed
* The config format is checked agaist a schema with the json-schema standard
* Tests are are added to the project (in the backend for config parsing)
This commit is contained in:
Florent Chehab 2020-05-10 22:19:32 +02:00
parent 4476ce3284
commit 0b1d1943ec
No known key found for this signature in database
GPG Key ID: 9A0CE018889EA246
9 changed files with 4280 additions and 37 deletions

17
config.default.yml Normal file
View File

@ -0,0 +1,17 @@
backend:
# TODO
accessToken: ""
# TODO
webdav: false
performance:
# Whiteboard information broadcasting frequency (in /s)
# => diminishing this will result in more latency
whiteboardInfoBroadcastFreq: 1
frontend:
# When an editable whiteboard is loading in a client,
# should it be started in read-only mode.
setReadOnlyOnWhiteboardLoad: false
# Show smallest screen indicator
showSmallestScreenIndicator: true
# performance:

4033
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@
"build": "webpack --config config/webpack.build.js",
"start:dev": "node scripts/server.js --mode=development",
"start:prod": "npm run build && node scripts/server.js --mode=production",
"test": "echo \"No tests needed!\" && exit 1",
"test": "jest",
"pretty-quick": "pretty-quick",
"format": "prettier --write .",
"style": "prettier --check ."
@ -28,10 +28,12 @@
}
},
"dependencies": {
"ajv": "6.12.2",
"dompurify": "^2.0.7",
"express": "4.*",
"formidable": "1.*",
"fs-extra": "7.*",
"js-yaml": "3.13.1",
"jsdom": "^14.0.0",
"pdfjs-dist": "^2.3.200",
"socket.io": "2.*",
@ -54,6 +56,7 @@
"css-loader": "^3.5.2",
"html-webpack-plugin": "^4.2.0",
"husky": "^4.2.5",
"jest": "26.0.1",
"jquery": "^3.2.1",
"jquery-ui": "^1.12.1",
"keymage": "^1.1.3",

View File

@ -85,7 +85,7 @@ class WhiteboardServerSideInfo {
nbConnectedUsers: this._nbConnectedUsers,
};
if (!config.disableSmallestScreen) {
if (config.frontend.showSmallestScreenIndicator) {
out.smallestScreenResolution = this.getSmallestScreenResolution();
}

View File

@ -0,0 +1,46 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Whiteboard config",
"type": "object",
"properties": {
"backend": {
"type": "object",
"required": ["accessToken", "performance", "webdav"],
"additionalProperties": false,
"properties": {
"accessToken": {
"type": "string"
},
"webdav": {
"type": "boolean"
},
"performance": {
"additionalProperties": false,
"type": "object",
"required": ["whiteboardInfoBroadcastFreq"],
"properties": {
"whiteboardInfoBroadcastFreq": {
"type": "number",
"minimum": 0
}
}
}
}
},
"frontend": {
"type": "object",
"additionalProperties": false,
"required": ["setReadOnlyOnWhiteboardLoad", "showSmallestScreenIndicator"],
"properties": {
"setReadOnlyOnWhiteboardLoad": {
"type": "boolean"
},
"showSmallestScreenIndicator": {
"type": "boolean"
}
}
}
},
"required": ["backend", "frontend"],
"additionalProperties": false
}

View File

@ -1,45 +1,84 @@
const { getArgs } = require("./utils");
const util = require("util");
const config = {
accessToken: "",
disableSmallestScreen: false,
webdav: false,
const {
getArgs,
getDefaultConfig,
getConfig,
deepMergeConfigs,
isConfigValid,
} = require("./utils");
whiteboardInfoBroadcastFreq: 1, // once per second
};
const defaultConfig = getDefaultConfig();
const cliArgs = getArgs();
let userConfig = {};
if (cliArgs["config"]) {
userConfig = getConfig(cliArgs["config"]);
}
const config = deepMergeConfigs(defaultConfig, userConfig);
/**
* Update the config based on the CLI args
* @param {object} startArgs
*/
function updateConfigFromStartArgs(startArgs) {
if (startArgs["accesstoken"]) {
config.accessToken = startArgs["accesstoken"];
}
if (startArgs["disablesmallestscreen"]) {
config.disableSmallestScreen = true;
}
if (startArgs["webdav"]) {
config.webdav = true;
function deprecateCliArg(key, callback) {
const val = startArgs[key];
if (val) {
console.warn(
"\x1b[33m\x1b[1m",
`Setting config values (${key}) from the CLI is deprecated. ` +
"This ability will be removed in the next major version. " +
"You should use the config file. "
);
callback(val);
}
}
deprecateCliArg("accesstoken", (val) => (config.backend.accessToken = val));
deprecateCliArg(
"disablesmallestscreen",
() => (config.backend.showSmallestScreenIndicator = false)
);
deprecateCliArg("webdav", () => (config.backend.webdav = true));
}
/**
* Update the config based on the env variables
*/
function updateConfigFromEnv() {
if (process.env.accesstoken) {
config.accessToken = process.env.accesstoken;
}
if (process.env.disablesmallestscreen) {
config.disablesmallestscreen = true;
}
if (process.env.webdav) {
config.webdav = true;
function deprecateEnv(key, callback) {
const val = process.env[key];
if (val) {
console.warn(
"\x1b[33m\x1b[1m",
`Setting config values (${key}) from the environment is deprecated. ` +
"This ability will be removed in the next major version. " +
"You should use the config file. "
);
callback(val);
}
}
deprecateEnv("accesstoken", (val) => (config.backend.accessToken = val));
deprecateEnv(
"disablesmallestscreen",
() => (config.backend.showSmallestScreenIndicator = false)
);
deprecateEnv("webdav", () => (config.backend.webdav = true));
}
// compatibility layer
// FIXME: remove this in next major
updateConfigFromEnv();
updateConfigFromStartArgs(getArgs());
// FIXME: remove this in next major
updateConfigFromStartArgs(cliArgs);
if (!isConfigValid(config, true)) {
throw new Error("Config is not valid. Check logs for details");
}
console.info(util.inspect(config, { showHidden: false, depth: null, colors: true }));
module.exports = config;

View File

@ -4,8 +4,6 @@ const config = require("./config");
const WhiteboardServerSideInfo = require("./WhiteboardServerSideInfo");
function startBackendServer(port) {
console.info("Starting backend server with config", config);
var fs = require("fs-extra");
var express = require("express");
var formidable = require("formidable"); //form upload processing
@ -27,7 +25,7 @@ function startBackendServer(port) {
var io = require("socket.io")(server, { path: "/ws-api" });
console.log("Webserver & socketserver running on port:" + port);
const { accessToken, webdav } = config;
const { accessToken, webdav } = config.backend;
app.get("/api/loadwhiteboard", function (req, res) {
var wid = req["query"]["wid"];
@ -191,7 +189,7 @@ function startBackendServer(port) {
info.infoWasSent();
}
});
}, (1 / config.whiteboardInfoBroadcastFreq) * 1000);
}, (1 / config.backend.performance.whiteboardInfoBroadcastFreq) * 1000);
io.on("connection", function (socket) {
var whiteboardId = null;

View File

@ -1,3 +1,12 @@
const path = require("path");
const fs = require("fs");
const yaml = require("js-yaml");
const Ajv = require("ajv");
const ajv = new Ajv({ allErrors: true });
const configSchema = require("./config-schema.json");
function getArgs() {
const args = {};
process.argv.slice(2, process.argv.length).forEach((arg) => {
@ -17,4 +26,68 @@ function getArgs() {
return args;
}
/**
* TODO
*
* @param path
* @return {any}
*/
function getConfig(path) {
return yaml.safeLoad(fs.readFileSync(path, "utf8"));
}
/**
* TODO
* @param config
* @param warn
* @return {*}
*/
function isConfigValid(config, warn = true) {
const validate = ajv.compile(configSchema);
const isValid = validate(config);
if (!isValid && warn) console.warn(validate.errors);
return isValid;
}
/**
* TODO
* @return {*}
*/
function getDefaultConfig() {
const defaultConfigPath = path.join(__dirname, "..", "config.default.yml");
return getConfig(defaultConfigPath);
}
/**
* TODO
* Deep merges objects, not arrays.
*
* @param baseConfig
* @param overrideConfig
* @return {{}}
*/
function deepMergeConfigs(baseConfig, overrideConfig) {
const out = {};
Object.entries(baseConfig).forEach(([key, val]) => {
out[key] = val;
if (overrideConfig.hasOwnProperty(key)) {
const overrideVal = overrideConfig[key];
if (typeof val === "object" && !Array.isArray(val) && val !== null) {
out[key] = deepMergeConfigs(val, overrideVal);
} else {
out[key] = overrideVal;
}
}
});
return out;
}
module.exports.getArgs = getArgs;
module.exports.getConfig = getConfig;
module.exports.getDefaultConfig = getDefaultConfig;
module.exports.deepMergeConfigs = deepMergeConfigs;
module.exports.isConfigValid = isConfigValid;

48
scripts/utils.test.js Normal file
View File

@ -0,0 +1,48 @@
const { getDefaultConfig, deepMergeConfigs, isConfigValid } = require("./utils");
test("Load default config", () => {
const defaultConfig = getDefaultConfig();
expect(typeof defaultConfig).toBe("object");
});
test("Full config override", () => {
const defaultConfig = getDefaultConfig();
expect(deepMergeConfigs(defaultConfig, defaultConfig)).toEqual(defaultConfig);
});
test("Simple partial config override", () => {
expect(deepMergeConfigs({ test: true }, { test: false }).test).toBe(false);
expect(deepMergeConfigs({ test: false }, { test: true }).test).toBe(true);
});
test("Simple deep config override", () => {
expect(deepMergeConfigs({ stage1: { stage2: true } }, { stage1: { stage2: false } })).toEqual({
stage1: { stage2: false },
});
});
test("Complex object config override", () => {
expect(
deepMergeConfigs({ stage1: { stage2: true, stage2b: true } }, { stage1: { stage2: false } })
).toEqual({
stage1: { stage2: false, stage2b: true },
});
});
test("Override default config", () => {
const defaultConfig = getDefaultConfig();
const overrideConfig1 = { frontend: { setReadOnlyOnWhiteboardLoad: true } };
expect(
deepMergeConfigs(defaultConfig, overrideConfig1).frontend.setReadOnlyOnWhiteboardLoad
).toBe(true);
});
test("Dumb config is not valid", () => {
expect(isConfigValid({}, false)).toBe(false);
});
test("Default config is valid", () => {
const defaultConfig = getDefaultConfig();
expect(isConfigValid(defaultConfig)).toBe(true);
});