Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Override config with environment variables #485

Merged
merged 3 commits into from
Jun 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ should show up in the network list on Riot and other clients.

* For the bot to appear online on Discord you need to run the bridge itself.
* ``npm start``
* Particular configuration keys can be overridden by defining corresponding environment variables. For instance, `auth.botToken` can be set with `APPSERVICE_DISCORD_AUTH_BOT_TOKEN`.

[Howto](./docs/howto.md)

Expand Down
48 changes: 41 additions & 7 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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
pacien marked this conversation as resolved.
Show resolved Hide resolved
* @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];
});
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/discordas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
35 changes: 29 additions & 6 deletions test/test_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -38,17 +38,40 @@ describe("DiscordBridgeConfig.ApplyConfig", () => {
console: "warn",
},
});
expect(config.bridge.homeserverUrl, "blah");
expect(config.bridge.homeserverUrl).to.equal("blah");
expect(config.bridge.disableTypingNotifications).to.be.true;
expect(config.bridge.disableDiscordMentions).to.be.false;
expect(config.bridge.disableDeletionForwarding).to.be.true;
expect(config.bridge.enableSelfServiceBridging).to.be.false;
expect(config.bridge.disableJoinLeaveNotifications).to.be.true;
expect(config.logging.console, "warn");
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: [
Expand All @@ -58,6 +81,6 @@ describe("DiscordBridgeConfig.ApplyConfig", () => {
],
},
});
expect(config.logging.files[0].file, "./bacon.log");
expect(config.logging.files[0].file).to.equal("./bacon.log");
});
});
4 changes: 2 additions & 2 deletions test/test_discordmessageprocessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ describe("DiscordMessageProcessor", () => {
msg.embeds = [];
msg.content = "Hello World!";
const result = await processor.FormatMessage(msg);
Chai.assert(result.body, "Hello World!");
Chai.assert(result.formattedBody, "Hello World!");
Chai.assert.equal(result.body, "Hello World!");
Chai.assert.equal(result.formattedBody, "Hello World!");
});
it("processes markdown messages correctly.", async () => {
const processor = new DiscordMessageProcessor(
Expand Down
3 changes: 2 additions & 1 deletion tools/chanfix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
3 changes: 2 additions & 1 deletion tools/ghostfix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down