zulip-botserver: Allow passing config via JSON formatted environment variable.

Fixes #485.

Co-authored-by: Alex Vandiver <alexmv@zulip.com>
This commit is contained in:
LoopThrough-i-j 2021-01-05 14:39:48 +05:30 committed by Alex Vandiver
parent 984d9151d5
commit bcf183d2b1
3 changed files with 78 additions and 12 deletions

View file

@ -4,6 +4,7 @@ from typing import Any, Dict
import unittest import unittest
from .server_test_lib import BotServerTestCase from .server_test_lib import BotServerTestCase
import json import json
from collections import OrderedDict
from importlib import import_module from importlib import import_module
from types import ModuleType from types import ModuleType
@ -132,6 +133,34 @@ class BotServerTests(BotServerTestCase):
assert opts.hostname == '127.0.0.1' assert opts.hostname == '127.0.0.1'
assert opts.port == 5002 assert opts.port == 5002
def test_read_config_from_env_vars(self) -> None:
# We use an OrderedDict so that the order of the entries in
# the stringified environment variable is standard even on
# Python 3.7 and earlier.
bots_config = OrderedDict()
bots_config['hello_world'] = {
'email': 'helloworld-bot@zulip.com',
'key': 'value',
'site': 'http://localhost',
'token': 'abcd1234',
}
bots_config['giphy'] = {
'email': 'giphy-bot@zulip.com',
'key': 'value2',
'site': 'http://localhost',
'token': 'abcd1234',
}
os.environ['ZULIP_BOTSERVER_CONFIG'] = json.dumps(bots_config)
# No bot specified; should read all bot configs
assert server.read_config_from_env_vars() == bots_config
# Specified bot exists; should read only that section.
assert server.read_config_from_env_vars("giphy") == {'giphy': bots_config['giphy']}
# Specified bot doesn't exist; should read the first section of the config.
assert server.read_config_from_env_vars("redefined_bot") == {'redefined_bot': bots_config['hello_world']}
def test_read_config_file(self) -> None: def test_read_config_file(self) -> None:
with self.assertRaises(IOError): with self.assertRaises(IOError):
server.read_config_file("nonexistentfile.conf") server.read_config_file("nonexistentfile.conf")

View file

@ -7,13 +7,19 @@ def parse_args() -> argparse.Namespace:
''' '''
parser = argparse.ArgumentParser(usage=usage) parser = argparse.ArgumentParser(usage=usage)
parser.add_argument( mutually_exclusive_args = parser.add_mutually_exclusive_group(required=True)
# config-file or use-env-vars made mutually exclusive to prevent conflicts
mutually_exclusive_args.add_argument(
'--config-file', '-c', '--config-file', '-c',
action='store', action='store',
required=True,
help='Config file for the Botserver. Use your `botserverrc` for multiple bots or' help='Config file for the Botserver. Use your `botserverrc` for multiple bots or'
'`zuliprc` for a single bot.' '`zuliprc` for a single bot.'
) )
mutually_exclusive_args.add_argument(
'--use-env-vars', '-e',
action='store_true',
help='Load configuration from JSON in ZULIP_BOTSERVER_CONFIG environment variable.'
)
parser.add_argument( parser.add_argument(
'--bot-config-file', '--bot-config-file',
action='store', action='store',

View file

@ -7,6 +7,7 @@ import os
import sys import sys
import importlib.util import importlib.util
from collections import OrderedDict
from configparser import MissingSectionHeaderError, NoOptionError from configparser import MissingSectionHeaderError, NoOptionError
from flask import Flask, request from flask import Flask, request
from importlib import import_module from importlib import import_module
@ -28,6 +29,32 @@ def read_config_section(parser: configparser.ConfigParser, section: str) -> Dict
} }
return section_info return section_info
def read_config_from_env_vars(bot_name: Optional[str] = None) -> Dict[str, Dict[str, str]]:
bots_config = {} # type: Dict[str, Dict[str, str]]
json_config = os.environ.get('ZULIP_BOTSERVER_CONFIG')
if json_config is None:
raise OSError("Could not read environment variable 'ZULIP_BOTSERVER_CONFIG': Variable not set.")
# Load JSON-formatted environment variable; use OrderedDict to
# preserve ordering on Python 3.6 and below.
env_config = json.loads(json_config, object_pairs_hook=OrderedDict)
if bot_name is not None:
if bot_name in env_config:
bots_config[bot_name] = env_config[bot_name]
else:
# If the bot name provided via the command line does not
# exist in the configuration provided via the environment
# variable, use the first bot in the environment variable,
# with name updated to match, along with a warning.
first_bot_name = list(env_config.keys())[0]
bots_config[bot_name] = env_config[first_bot_name]
logging.warning(
"First bot name in the config list was changed from '{}' to '{}'".format(first_bot_name, bot_name)
)
else:
bots_config = dict(env_config)
return bots_config
def read_config_file(config_file_path: str, bot_name: Optional[str] = None) -> Dict[str, Dict[str, str]]: def read_config_file(config_file_path: str, bot_name: Optional[str] = None) -> Dict[str, Dict[str, str]]:
parser = parse_config_file(config_file_path) parser = parse_config_file(config_file_path)
@ -178,16 +205,20 @@ def handle_bot() -> str:
def main() -> None: def main() -> None:
options = parse_args() options = parse_args()
global bots_config global bots_config
try:
bots_config = read_config_file(options.config_file, options.bot_name) if options.use_env_vars:
except MissingSectionHeaderError: bots_config = read_config_from_env_vars(options.bot_name)
sys.exit("Error: Your Botserver config file `{0}` contains an empty section header!\n" elif options.config_file:
"You need to write the names of the bots you want to run in the " try:
"section headers of `{0}`.".format(options.config_file)) bots_config = read_config_file(options.config_file, options.bot_name)
except NoOptionError as e: except MissingSectionHeaderError:
sys.exit("Error: Your Botserver config file `{0}` has a missing option `{1}` in section `{2}`!\n" sys.exit("Error: Your Botserver config file `{0}` contains an empty section header!\n"
"You need to add option `{1}` with appropriate value in section `{2}` of `{0}`" "You need to write the names of the bots you want to run in the "
.format(options.config_file, e.option, e.section)) "section headers of `{0}`.".format(options.config_file))
except NoOptionError as e:
sys.exit("Error: Your Botserver config file `{0}` has a missing option `{1}` in section `{2}`!\n"
"You need to add option `{1}` with appropriate value in section `{2}` of `{0}`"
.format(options.config_file, e.option, e.section))
available_bots = list(bots_config.keys()) available_bots = list(bots_config.keys())
bots_lib_modules = load_lib_modules(available_bots) bots_lib_modules = load_lib_modules(available_bots)