diff --git a/README.rst b/README.rst index 9696f8a..bb88dbe 100644 --- a/README.rst +++ b/README.rst @@ -89,11 +89,31 @@ Available tools * ``qb session outgoing`` - List outgoing sessions from the server. -Environment variables ---------------------- +Configuration & Environment variables +------------------------------------- +Several options exist to configure Qpid Bow. In order of preference: -``AMQP_SERVERS`` - comma-separated list of main and failover servers to connect to +**Pass in arguments** +One can always override the used server URL using arguments: -``AMQP_TEST_SERVERS`` - Same as ``AMQP_SERVERS``, used solely for unittests +* For the CLI tools, use the ``--broker-url`` command line argument. +* For the library pass in the keyword argument ``server_url``. + +**Configure using a dict** +When using Qpid Bow as a library, one can pass in config using a dict to: +``qpid_bow.config.configure`` + +The dict can contain the following entries: + +* ``amqp_url`` - Comma-separated list of main and failover servers to connect to. +* ``username`` - Username to use when no username is provided in the URL. +* ``password`` - Password to use when no password is provided in the URL. + +**Environment variables** +The easiest way to configure Qpid Bow's tools and library is to use environment variables. +These variables can be added to your shell's profile and will automatically get picked up. + +* ``AMQP_SERVERS`` - Comma-separated list of main and failover servers to connect to. +* ``AMQP_TEST_SERVERS`` - Same as ``AMQP_SERVERS``, used solely for unittests. example: ``AMQP_SERVERS=amqp://user:pass@192.168.1.1:5672,amqp://user:pass@192.168.1.2:5672`` diff --git a/VERSION b/VERSION index 6d7de6e..9084fa2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.2 +1.1.0 diff --git a/docs/source/conf.py b/docs/source/conf.py index be262f9..e870b61 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,8 +14,8 @@ copyright = '2018, Bynder B.V.' author = 'Bynder B.V.' -version = '1.0' # The short X.Y version. -release = '1.0.2' # The full version, including alpha/beta/rc tags. +version = '1.1' # The short X.Y version. +release = '1.1.0' # The full version, including alpha/beta/rc tags. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] diff --git a/qpid_bow/config.py b/qpid_bow/config.py index b75a27c..6977e2b 100644 --- a/qpid_bow/config.py +++ b/qpid_bow/config.py @@ -7,6 +7,8 @@ Optional, ) +from urllib.parse import urlsplit, urlunsplit + config: dict = {} @@ -19,30 +21,50 @@ def configure(new_config: Mapping): config.update(new_config) -def get_urls(urls: Optional[str] = None) -> List[str]: - """Retrieves server urls from one of the sources. +def process_url(url: str) -> str: + """Processes a URL for usage with Qpid Proton. - The sources priority comes in the following order: passed arguments, - global config, AMQP_SERVERS environment variable. + - ActiveMQ amqp+ssl scheme is replaced with amqps. + - Adds username and password from config. Args: - urls: Comma-separated urls. + url: Input URL. Returns: - List[str]: Returns list of urls to connect to. + str: Processed URL. """ - if urls: - return [url.strip() for url in urls.split(',')] + split_url = urlsplit(url.strip()) + if split_url.scheme == 'amqp+ssl': + split_url = split_url._replace(scheme='amqps') + + if ((not split_url.username or not split_url.password) and + 'username' in config and 'password' in config): + user_pass = f"{config['username']}:{config['password']}@" + new_netloc = user_pass + split_url.netloc + split_url = split_url._replace(netloc=new_netloc) + + return urlunsplit(split_url) + - if config.get('amqp_url'): - return [url.strip() for url in config['amqp_url'].split(',')] +def get_urls(argument_urls: Optional[str] = None) -> List[str]: + """Retrieves server argument_urls from one of the sources. - amqp_servers = environ.get('AMQP_SERVERS') + The sources priority comes in the following order: passed arguments, + global config, AMQP_SERVERS environment variable. + + Args: + argument_urls: Comma-separated argument_urls. - if amqp_servers: - environ_urls = [] - for server in amqp_servers.split(','): - environ_urls.append(server.strip()) - return environ_urls + Returns: + List[str]: Returns list of argument_urls to connect to. + """ + if argument_urls: + raw_urls = argument_urls + elif 'amqp_url' in config: + raw_urls = config['amqp_url'] + elif 'AMQP_SERVERS' in environ: + raw_urls = environ['AMQP_SERVERS'] + else: + raise ValueError('AMQP server url is not configured') - raise ValueError('AMQP server url is not configured') + return [process_url(url) for url in raw_urls.split(',')] diff --git a/test/test_config.py b/test/test_config.py new file mode 100644 index 0000000..fc09fe5 --- /dev/null +++ b/test/test_config.py @@ -0,0 +1,85 @@ +from contextlib import suppress +from os import environ +from unittest import TestCase + +from qpid_bow.config import configure, config, get_urls, process_url + +class TestGetURLs(TestCase): + def setUp(self): + self.old_config = config + self.old_env = environ.get('AMQP_SERVERS') + + def tearDown(self): + config.clear() + config.update(self.old_config) + if self.old_env: + environ['AMQP_SERVERS'] = self.old_env + else: + with suppress(KeyError): + del environ['AMQP_SERVERS'] + + def test_no_config(self): + with suppress(KeyError): + del environ['AMQP_SERVERS'] + + with self.assertRaises(ValueError): + get_urls() + + def test_get_urls_priority_from_environ(self): + environ['AMQP_SERVERS'] = 'amqps://environ.example' + self.assertEqual(get_urls(), ['amqps://environ.example']) + + def test_get_urls_priority_from_config(self): + environ['AMQP_SERVERS'] = 'amqps://environ.example' + configure({'amqp_url': 'amqps://config.example'}) + + self.assertEqual(get_urls(), ['amqps://config.example']) + + def test_get_urls_priority_from_args(self): + environ['AMQP_SERVERS'] = 'amqps://environ.example' + configure({'amqp_url': 'amqps://config.example'}) + + self.assertEqual(get_urls('amqps://args.example'), + ['amqps://args.example']) + + def test_get_urls_comma_seperated(self): + self.assertEqual( + get_urls('amqps://args1.example, amqps://args2.example'), + ['amqps://args1.example', 'amqps://args2.example']) + + def test_get_urls_activemq_format(self): + self.assertEqual(get_urls('amqp+ssl://args.example'), + ['amqps://args.example']) + + def test_get_urls_activemq_format_comma_seperated(self): + self.assertEqual( + get_urls('amqp+ssl://args1.example, amqp+ssl://args2.example'), + ['amqps://args1.example', 'amqps://args2.example']) + + def test_get_urls_user_passwd_config_mixed(self): + config['username'] = 'otheruser' + config['password'] = 'otherpass' + self.assertEqual( + get_urls('amqps://args1.example,amqps://user:pass@args2.example'), + ['amqps://otheruser:otherpass@args1.example', + 'amqps://user:pass@args2.example']) + + def test_process_url_noop(self): + valid_url = 'amqp://some.example' + self.assertEqual(process_url(valid_url), valid_url) + + def test_process_url_activemq(self): + self.assertEqual(process_url('amqp+ssl://some.example'), + 'amqps://some.example') + + def test_process_url_user_passwd_config(self): + config['username'] = 'user' + config['password'] = 'pass' + self.assertEqual(process_url('amqps://some.example'), + 'amqps://user:pass@some.example') + + def test_process_url_user_passwd_no_override(self): + config['username'] = 'otheruser' + config['password'] = 'otherpass' + valid_url = 'amqps://user:pass@some.example' + self.assertEqual(process_url(valid_url), valid_url)