From 6ffc73946a3ed2bf6fa7e50a14270529dcec6b7b Mon Sep 17 00:00:00 2001 From: Israel Date: Wed, 12 Apr 2023 04:02:32 -0300 Subject: [PATCH] Cover `etcd3` in `parse_dcs` function (#2639) Previous to this commit the `parse_dcs` function would fail with a `PatroniCtlException` if the user ever passed an `etcd3` URL through `--dcs-url` command-line option. As a consequence, the only way of using `etcd3` in `patronictl` was by using a configuration file passed through `-c` command-line option. This commit fixes that issue and allows `etcd3` to be used in `--dcs-url`. References: PAT-91 Close #2638 --- patroni/ctl.py | 38 ++++++++++++++++++++++++++++++++++++-- tests/test_ctl.py | 1 + 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/patroni/ctl.py b/patroni/ctl.py index fb6aa2b4..e78c0f27 100644 --- a/patroni/ctl.py +++ b/patroni/ctl.py @@ -21,6 +21,8 @@ import tempfile import time import yaml +from typing import Any, Dict, Union + from click import ClickException from collections import defaultdict from contextlib import contextmanager @@ -44,7 +46,8 @@ CONFIG_FILE_PATH = os.path.join(CONFIG_DIR_PATH, 'patronictl.yaml') DCS_DEFAULTS = {'zookeeper': {'port': 2181, 'template': "zookeeper:\n hosts: ['{host}:{port}']"}, 'exhibitor': {'port': 8181, 'template': "exhibitor:\n hosts: [{host}]\n port: {port}"}, 'consul': {'port': 8500, 'template': "consul:\n host: '{host}:{port}'"}, - 'etcd': {'port': 2379, 'template': "etcd:\n host: '{host}:{port}'"}} + 'etcd': {'port': 2379, 'template': "etcd:\n host: '{host}:{port}'"}, + 'etcd3': {'port': 2379, 'template': "etcd3:\n host: '{host}:{port}'"}} class PatroniCtlException(ClickException): @@ -90,7 +93,38 @@ class PatronictlPrettyTable(PrettyTable): _hrule = property(_get_hline, _set_hline) -def parse_dcs(dcs): +def parse_dcs(dcs: str) -> Union[Dict[str, Any], None]: + """Parse a DCS URL. + + :param dcs: the DCS URL in the format ``DCS://HOST:PORT``. ``DCS`` can be one among + * ``consul`` + * ``etcd`` + * ``etcd3`` + * ``exhibitor`` + * ``zookeeper`` + + If ``DCS`` is not specified, it assumes ``etcd`` by default. If ``HOST`` is not specified, it assumes + ``localhost`` by default. If ``PORT`` is not specified, it assumes the default port of the given ``DCS``. + + :returns: ``None`` if *dcs* is ``None``, otherwise a dictionary. The dictionary represents *dcs* as if it were + parsed from the Patroni configuration file. + + :raises PatroniCtlException: if the DCS name in *dcs* is not valid. + + :Example: + + >>> parse_dcs('') + {'etcd': {'host': 'localhost:2379'}} + + >>> parse_dcs('etcd://:2399') + {'etcd': {'host': 'localhost:2399'}} + + >>> parse_dcs('etcd://test') + {'etcd': {'host': 'test:2379'}} + + >>> parse_dcs('etcd3://random.com:2399') + {'etcd3': {'host': 'random.com:2399'}} + """ if dcs is None: return None elif '//' not in dcs: diff --git a/tests/test_ctl.py b/tests/test_ctl.py index 65f9d444..eba4d1c1 100644 --- a/tests/test_ctl.py +++ b/tests/test_ctl.py @@ -76,6 +76,7 @@ class TestCtl(unittest.TestCase): assert parse_dcs('zookeeper://localhost') == {'zookeeper': {'hosts': ['localhost:2181']}} assert parse_dcs('exhibitor://dummy') == {'exhibitor': {'hosts': ['dummy'], 'port': 8181}} assert parse_dcs('consul://localhost') == {'consul': {'host': 'localhost:8500'}} + assert parse_dcs('etcd3://random.com:2399') == {'etcd3': {'host': 'random.com:2399'}} self.assertRaises(PatroniCtlException, parse_dcs, 'invalid://test') def test_output_members(self):