Skip to content

Commit bcf183d

Browse files
zulip-botserver: Allow passing config via JSON formatted environment variable.
Fixes #485. Co-authored-by: Alex Vandiver <[email protected]>
1 parent 984d915 commit bcf183d

File tree

3 files changed

+78
-12
lines changed

3 files changed

+78
-12
lines changed

zulip_botserver/tests/test_server.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import unittest
55
from .server_test_lib import BotServerTestCase
66
import json
7+
from collections import OrderedDict
78
from importlib import import_module
89
from types import ModuleType
910

@@ -132,6 +133,34 @@ def test_argument_parsing_defaults(self) -> None:
132133
assert opts.hostname == '127.0.0.1'
133134
assert opts.port == 5002
134135

136+
def test_read_config_from_env_vars(self) -> None:
137+
# We use an OrderedDict so that the order of the entries in
138+
# the stringified environment variable is standard even on
139+
# Python 3.7 and earlier.
140+
bots_config = OrderedDict()
141+
bots_config['hello_world'] = {
142+
'email': '[email protected]',
143+
'key': 'value',
144+
'site': 'http://localhost',
145+
'token': 'abcd1234',
146+
}
147+
bots_config['giphy'] = {
148+
'email': '[email protected]',
149+
'key': 'value2',
150+
'site': 'http://localhost',
151+
'token': 'abcd1234',
152+
}
153+
os.environ['ZULIP_BOTSERVER_CONFIG'] = json.dumps(bots_config)
154+
155+
# No bot specified; should read all bot configs
156+
assert server.read_config_from_env_vars() == bots_config
157+
158+
# Specified bot exists; should read only that section.
159+
assert server.read_config_from_env_vars("giphy") == {'giphy': bots_config['giphy']}
160+
161+
# Specified bot doesn't exist; should read the first section of the config.
162+
assert server.read_config_from_env_vars("redefined_bot") == {'redefined_bot': bots_config['hello_world']}
163+
135164
def test_read_config_file(self) -> None:
136165
with self.assertRaises(IOError):
137166
server.read_config_file("nonexistentfile.conf")

zulip_botserver/zulip_botserver/input_parameters.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,19 @@ def parse_args() -> argparse.Namespace:
77
'''
88

99
parser = argparse.ArgumentParser(usage=usage)
10-
parser.add_argument(
10+
mutually_exclusive_args = parser.add_mutually_exclusive_group(required=True)
11+
# config-file or use-env-vars made mutually exclusive to prevent conflicts
12+
mutually_exclusive_args.add_argument(
1113
'--config-file', '-c',
1214
action='store',
13-
required=True,
1415
help='Config file for the Botserver. Use your `botserverrc` for multiple bots or'
1516
'`zuliprc` for a single bot.'
1617
)
18+
mutually_exclusive_args.add_argument(
19+
'--use-env-vars', '-e',
20+
action='store_true',
21+
help='Load configuration from JSON in ZULIP_BOTSERVER_CONFIG environment variable.'
22+
)
1723
parser.add_argument(
1824
'--bot-config-file',
1925
action='store',

zulip_botserver/zulip_botserver/server.py

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import sys
88
import importlib.util
99

10+
from collections import OrderedDict
1011
from configparser import MissingSectionHeaderError, NoOptionError
1112
from flask import Flask, request
1213
from importlib import import_module
@@ -28,6 +29,32 @@ def read_config_section(parser: configparser.ConfigParser, section: str) -> Dict
2829
}
2930
return section_info
3031

32+
def read_config_from_env_vars(bot_name: Optional[str] = None) -> Dict[str, Dict[str, str]]:
33+
bots_config = {} # type: Dict[str, Dict[str, str]]
34+
json_config = os.environ.get('ZULIP_BOTSERVER_CONFIG')
35+
36+
if json_config is None:
37+
raise OSError("Could not read environment variable 'ZULIP_BOTSERVER_CONFIG': Variable not set.")
38+
39+
# Load JSON-formatted environment variable; use OrderedDict to
40+
# preserve ordering on Python 3.6 and below.
41+
env_config = json.loads(json_config, object_pairs_hook=OrderedDict)
42+
if bot_name is not None:
43+
if bot_name in env_config:
44+
bots_config[bot_name] = env_config[bot_name]
45+
else:
46+
# If the bot name provided via the command line does not
47+
# exist in the configuration provided via the environment
48+
# variable, use the first bot in the environment variable,
49+
# with name updated to match, along with a warning.
50+
first_bot_name = list(env_config.keys())[0]
51+
bots_config[bot_name] = env_config[first_bot_name]
52+
logging.warning(
53+
"First bot name in the config list was changed from '{}' to '{}'".format(first_bot_name, bot_name)
54+
)
55+
else:
56+
bots_config = dict(env_config)
57+
return bots_config
3158

3259
def read_config_file(config_file_path: str, bot_name: Optional[str] = None) -> Dict[str, Dict[str, str]]:
3360
parser = parse_config_file(config_file_path)
@@ -178,16 +205,20 @@ def handle_bot() -> str:
178205
def main() -> None:
179206
options = parse_args()
180207
global bots_config
181-
try:
182-
bots_config = read_config_file(options.config_file, options.bot_name)
183-
except MissingSectionHeaderError:
184-
sys.exit("Error: Your Botserver config file `{0}` contains an empty section header!\n"
185-
"You need to write the names of the bots you want to run in the "
186-
"section headers of `{0}`.".format(options.config_file))
187-
except NoOptionError as e:
188-
sys.exit("Error: Your Botserver config file `{0}` has a missing option `{1}` in section `{2}`!\n"
189-
"You need to add option `{1}` with appropriate value in section `{2}` of `{0}`"
190-
.format(options.config_file, e.option, e.section))
208+
209+
if options.use_env_vars:
210+
bots_config = read_config_from_env_vars(options.bot_name)
211+
elif options.config_file:
212+
try:
213+
bots_config = read_config_file(options.config_file, options.bot_name)
214+
except MissingSectionHeaderError:
215+
sys.exit("Error: Your Botserver config file `{0}` contains an empty section header!\n"
216+
"You need to write the names of the bots you want to run in the "
217+
"section headers of `{0}`.".format(options.config_file))
218+
except NoOptionError as e:
219+
sys.exit("Error: Your Botserver config file `{0}` has a missing option `{1}` in section `{2}`!\n"
220+
"You need to add option `{1}` with appropriate value in section `{2}` of `{0}`"
221+
.format(options.config_file, e.option, e.section))
191222

192223
available_bots = list(bots_config.keys())
193224
bots_lib_modules = load_lib_modules(available_bots)

0 commit comments

Comments
 (0)