From 45d2bf8f45b923a13035efa10d996e7e22484e0e Mon Sep 17 00:00:00 2001
From: pacien <pacien.trangirard@pacien.net>
Date: Wed, 5 Jun 2019 00:29:30 +0200
Subject: [PATCH] add config override using env variables

Signed-off-by: pacien <pacien.trangirard@pacien.net>
---
 src/config.ts       | 48 ++++++++++++++++++++++++++++++++++++++-------
 src/discordas.ts    |  3 ++-
 test/test_config.ts | 29 ++++++++++++++++++++++++---
 tools/chanfix.ts    |  3 ++-
 tools/ghostfix.ts   |  3 ++-
 5 files changed, 73 insertions(+), 13 deletions(-)

diff --git a/src/config.ts b/src/config.ts
index 1666043..2f0eb76 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -14,6 +14,10 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+const ENV_PREFIX = "APPSERVICE_DISCORD";
+const ENV_KEY_SEPARATOR = "_";
+const ENV_VAL_SEPARATOR = ",";
+
 /** Type annotations for config/config.schema.yaml */
 export class DiscordBridgeConfig {
     public bridge: DiscordBridgeConfigBridge = new DiscordBridgeConfigBridge();
@@ -27,18 +31,48 @@ export class DiscordBridgeConfig {
 
     /**
      * Apply a set of keys and values over the default config.
-     * @param _config Config keys
+     * @param newConfig Config keys
      * @param configLayer Private parameter
      */
     // tslint:disable-next-line no-any
-    public ApplyConfig(newConfig: {[key: string]: any}, configLayer: any = this) {
+    public applyConfig(newConfig: {[key: string]: any}, configLayer: {[key: string]: any} = this) {
           Object.keys(newConfig).forEach((key) => {
-            if ( typeof(configLayer[key]) === "object" &&
-                    !Array.isArray(configLayer[key])) {
-                this.ApplyConfig(newConfig[key], this[key]);
-                return;
+            if (configLayer[key] instanceof Object && !(configLayer[key] instanceof Array)) {
+                this.applyConfig(newConfig[key], configLayer[key]);
+            } else {
+                configLayer[key] = newConfig[key];
+            }
+        });
+    }
+
+    /**
+     * Override configuration keys defined in the supplied environment dictionary.
+     * @param environment environment variable dictionary
+     * @param path private parameter: config layer path determining the environment key prefix
+     * @param configLayer private parameter: current layer of configuration to alter recursively
+     */
+    public applyEnvironmentOverrides(
+        // tslint:disable-next-line no-any
+        environment: {[key: string]: any},
+        path: string[] = [ENV_PREFIX],
+        // tslint:disable-next-line no-any
+        configLayer: {[key: string]: any} = this,
+    ) {
+        Object.keys(configLayer).forEach((key) => {
+            // camelCase to THICK_SNAKE
+            const attributeKey = key.replace(/[A-Z]/g, (prefix) => `${ENV_KEY_SEPARATOR}${prefix}`).toUpperCase();
+            const attributePath = path.concat([attributeKey]);
+
+            if (configLayer[key] instanceof Object && !(configLayer[key] instanceof Array)) {
+                this.applyEnvironmentOverrides(environment, attributePath, configLayer[key]);
+            } else {
+                const lookupKey = attributePath.join(ENV_KEY_SEPARATOR);
+                if (lookupKey in environment) {
+                    configLayer[key] = (configLayer[key] instanceof Array)
+                        ? environment[lookupKey].split(ENV_VAL_SEPARATOR)
+                        : environment[lookupKey];
+                }
             }
-            configLayer[key] = newConfig[key];
         });
     }
 }
diff --git a/src/discordas.ts b/src/discordas.ts
index 0f4b5e1..1f350c3 100644
--- a/src/discordas.ts
+++ b/src/discordas.ts
@@ -59,7 +59,8 @@ type callbackFn = (...args: any[]) => Promise<any>;
 
 async function run(port: number, fileConfig: DiscordBridgeConfig) {
     const config = new DiscordBridgeConfig();
-    config.ApplyConfig(fileConfig);
+    config.applyConfig(fileConfig);
+    config.applyEnvironmentOverrides(process.env);
     Log.Configure(config.logging);
     log.info("Starting Discord AS");
     const yamlConfig = yaml.safeLoad(fs.readFileSync(cli.opts.registrationPath, "utf8"));
diff --git a/test/test_config.ts b/test/test_config.ts
index 26b037e..badb662 100644
--- a/test/test_config.ts
+++ b/test/test_config.ts
@@ -22,10 +22,10 @@ import { DiscordBridgeConfig } from "../src/config";
 
 const expect = Chai.expect;
 
-describe("DiscordBridgeConfig.ApplyConfig", () => {
+describe("DiscordBridgeConfig.applyConfig", () => {
     it("should merge configs correctly", () => {
         const config = new DiscordBridgeConfig();
-        config.ApplyConfig({
+        config.applyConfig({
             bridge: {
                 disableDeletionForwarding: true,
                 disableDiscordMentions: false,
@@ -46,9 +46,32 @@ describe("DiscordBridgeConfig.ApplyConfig", () => {
         expect(config.bridge.disableJoinLeaveNotifications).to.be.true;
         expect(config.logging.console).to.equal("warn");
     });
+    it("should merge environment overrides correctly", () => {
+        const config = new DiscordBridgeConfig();
+        config.applyConfig({
+            bridge: {
+                disableDeletionForwarding: true,
+                disableDiscordMentions: false,
+                homeserverUrl: "blah",
+            },
+            logging: {
+                console: "warn",
+            },
+        });
+        config.applyEnvironmentOverrides({
+            APPSERVICE_DISCORD_BRIDGE_DISABLE_DELETION_FORWARDING: false,
+            APPSERVICE_DISCORD_BRIDGE_DISABLE_JOIN_LEAVE_NOTIFICATIONS: true,
+            APPSERVICE_DISCORD_LOGGING_CONSOLE: "debug",
+        });
+        expect(config.bridge.disableJoinLeaveNotifications).to.be.true;
+        expect(config.bridge.disableDeletionForwarding).to.be.false;
+        expect(config.bridge.disableDiscordMentions).to.be.false;
+        expect(config.bridge.homeserverUrl).to.equal("blah");
+        expect(config.logging.console).to.equal("debug");
+    });
     it("should merge logging.files correctly", () => {
         const config = new DiscordBridgeConfig();
-        config.ApplyConfig({
+        config.applyConfig({
             logging: {
                 console: "silent",
                 files: [
diff --git a/tools/chanfix.ts b/tools/chanfix.ts
index 8d4f4ab..a3f5095 100644
--- a/tools/chanfix.ts
+++ b/tools/chanfix.ts
@@ -67,7 +67,8 @@ if (options.help) {
 const yamlConfig = yaml.safeLoad(fs.readFileSync("./discord-registration.yaml", "utf8"));
 const registration = AppServiceRegistration.fromObject(yamlConfig);
 const config = new DiscordBridgeConfig();
-config.ApplyConfig(yaml.safeLoad(fs.readFileSync(options.config, "utf8")) as DiscordBridgeConfig);
+config.applyConfig(yaml.safeLoad(fs.readFileSync(options.config, "utf8")) as DiscordBridgeConfig);
+config.applyEnvironmentOverrides(process.env);
 
 if (registration === null) {
     throw new Error("Failed to parse registration file");
diff --git a/tools/ghostfix.ts b/tools/ghostfix.ts
index 9ce61d6..b4e99e9 100644
--- a/tools/ghostfix.ts
+++ b/tools/ghostfix.ts
@@ -76,7 +76,8 @@ if (options.help) {
 const yamlConfig = yaml.safeLoad(fs.readFileSync("./discord-registration.yaml", "utf8"));
 const registration = AppServiceRegistration.fromObject(yamlConfig);
 const config = new DiscordBridgeConfig();
-config.ApplyConfig(yaml.safeLoad(fs.readFileSync(options.config, "utf8")) as DiscordBridgeConfig);
+config.applyConfig(yaml.safeLoad(fs.readFileSync(options.config, "utf8")) as DiscordBridgeConfig);
+config.applyEnvironmentOverrides(process.env);
 
 if (registration === null) {
     throw new Error("Failed to parse registration file");
-- 
GitLab