Repository: zalando/patroni (python)

Issue:
Accept *:port as a valid entry for postgresql.listen in patroni --validate-config
**Describe the bug**
If `postgresql.listen` is set to  `*:port`, `patroni --validate-config` complains.
I understand that security wise choosing the interface explicitly is better and that patroni's documentation
doesn't state that it's possible but it's a valid (and wide spread) postgresql.conf value.

If you're ok with allowing this, we can do the PR.

**To Reproduce**
1 ) configure `postgresql.listen` to `*:5432` in the yaml
2 ) run `patroni --validate-config` on the config file

**Expected behavior**
No errors.

**Screenshots**
```
postgresql.listen *:5432 didn't pass validation: gaierror(-2, 'Name or service not known')
```

**Environment**
- Patroni version: 2.1.4
- PostgreSQL version: 14.5
- DCS (and its version): 3.4.20

**Patroni configuration file**
```
scope: pgdemo
name: pg1
bootstrap:
  dcs:
    postgresql:
      parameters:
        cluster_name: pg1
        lc_messages: C
        lc_monetary: C
        lc_numeric: C
        lc_time: C
        listen_addresses: '*'
        log_destination: stderr
        logging_collector: true
        port: 5432
        unix_socket_directories: /run/user/1000/pglift/postgresql
  initdb:
  - locale: C
  - encoding: UTF8
  pg_hba:
  - local   all             postgres                                trust
  - local   all             all                                     trust
  - host    all             all             127.0.0.1/32            trust
  - host    all             all             ::1/128                 trust
  - local   replication     replication                              trust
  - host    replication     replication      127.0.0.1/32            trust
  - host    replication     replication      ::1/128                 trust
  pg_ident:
  - '# MAPNAME       SYSTEM-USERNAME         PG-USERNAME'
restapi:
  connect_address: 127.0.1.1:8008
  listen: 127.0.1.1:8008
etcd3:
  host: 10.20.60.99:2379
postgresql:
  authentication:
    superuser:
      username: postgres
    replication:
      username: replication
    rewind:
      username: postgres
  connect_address: 127.0.1.1:5432
  data_dir: /home/vagrant/.local/share/pglift/srv/pgsql/14/pg1/data
  bin_dir: /usr/pgsql-14/bin
  listen: 127.0.1.1:5432
  use_unix_socket: true
  use_unix_socket_repl: true
  pgpass: /home/vagrant/.pgpass
  parameters:
    cluster_name: pg1
    lc_messages: C
    lc_monetary: C
    lc_numeric: C
    lc_time: C
    listen_addresses: '*'
    log_destination: stderr
    logging_collector: true
    port: 5432
    unix_socket_directories: /run/user/1000/pglift/postgresql
  pg_hba:
  - local   all             postgres                                trust
  - local   all             all                                     trust
  - host    all             all             127.0.0.1/32            trust
  - host    all             all             ::1/128                 trust
  - local   replication     replication                              trust
  - host    replication     replication      127.0.0.1/32            trust
  - host    replication     replication      ::1/128                 trust
  pg_ident:
  - '# MAPNAME       SYSTEM-USERNAME         PG-USERNAME'
```

**patronictl show-config**
```
(.venv) [vagrant@pat1 pglift]$ patronictl show-config
postgresql:
  parameters:
    cluster_name: pg1
    lc_messages: C
    lc_monetary: C
    lc_numeric: C
    lc_time: C
    listen_addresses: '*'
    log_destination: stderr
    logging_collector: true
    port: 5432
    unix_socket_directories: /run/user/1000/pglift/postgresql
```

**Have you checked Patroni logs?**
N/A

**Have you checked PostgreSQL logs?**
N/A

**Have you tried to use GitHub issue search?**
I didn't find one.

**Additional context**
N/A

Pull request description:
Accept '*:<port>' for postgresql.listen
We catch this special value when validating configuration and check that
it's alone in the hosts list.

Fixes #2397.

Patch:
diff --git a/patroni/validator.py b/patroni/validator.py
index 595d4355..839a9016 100644
--- a/patroni/validator.py
+++ b/patroni/validator.py
@@ -37,6 +37,10 @@ def validate_host_port(host_port, listen=False, multiple_hosts=False):
             hosts = hosts.split(",")
         else:
             hosts = [hosts]
+        if "*" in hosts:
+            if len(hosts) != 1:
+                raise ConfigParseError("expecting '*' alone")
+            hosts = [p[-1][0] for p in socket.getaddrinfo(None, port, 0, socket.SOCK_STREAM, 0, socket.AI_PASSIVE)]
         for host in hosts:
             proto = socket.getaddrinfo(host, "", 0, socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
             s = socket.socket(proto[0][0], socket.SOCK_STREAM)


Test patch:
diff --git a/tests/test_validator.py b/tests/test_validator.py
index 830b415e..9ccc6a7d 100644
--- a/tests/test_validator.py
+++ b/tests/test_validator.py
@@ -156,6 +156,7 @@ class TestValidator(unittest.TestCase):
         files.append(config["postgresql"]["bin_dir"])
         c = copy.deepcopy(config)
         c["restapi"]["connect_address"] = 'False:blabla'
+        c["postgresql"]["listen"] = '*:543'
         c["etcd"]["hosts"] = ["127.0.0.1:2379", "1244.0.0.1:2379", "127.0.0.1:invalidport"]
         c["kubernetes"]["pod_ip"] = "127.0.0.1111"
         errors = schema(c)
@@ -208,11 +209,12 @@ class TestValidator(unittest.TestCase):
         files.append(os.path.join(config["postgresql"]["data_dir"], "PG_VERSION"))
         c = copy.deepcopy(config)
         c["etcd"]["hosts"] = []
+        c["postgresql"]["listen"] = '127.0.0.2,*:543'
         del c["postgresql"]["bin_dir"]
         with patch('patroni.validator.open', mock_open(read_data='11')):
             errors = schema(c)
         output = "\n".join(errors)
-        self.assertEqual(['etcd.hosts', 'postgresql.data_dir',
+        self.assertEqual(['etcd.hosts', 'postgresql.data_dir', 'postgresql.listen',
                           'raft.bind_addr', 'raft.self_addr'], parse_output(output))
 
     @patch('subprocess.check_output', Mock(return_value=b"postgres (PostgreSQL) 12.1"))