Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/docket/_redis_sentinel.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ def parse_sentinel_url(url: str) -> SentinelConfiguration:
All other query parameters are standard redis-py connection options
(``max_connections``, ``socket_timeout``, ``health_check_interval``, ...)
and apply to the data-node connections with exactly the same parsing as a
standalone ``redis://`` URL.
standalone ``redis://`` URL. On the ``rediss+sentinel`` scheme the ``ssl_*``
options additionally apply to the connections to the Sentinel daemons, so a
single private CA (``?ssl_ca_certs=...``) covers the whole topology.

Raises:
ValueError: for a missing host, malformed port, missing service name,
Expand Down Expand Up @@ -194,6 +196,19 @@ def parse_sentinel_url(url: str) -> SentinelConfiguration:
for governed in ("db", "username", "password"):
funneled.pop(governed, None)
connection_kwargs.update(funneled)
if tls:
# The Sentinel daemons share the data nodes' TLS profile: without
# this, a private CA passed as ?ssl_ca_certs= (or ?ssl_cert_reqs=none
# for unverified setups) would apply to the master connections only,
# and every connection to a Sentinel daemon would fail certificate
# verification during discovery.
sentinel_kwargs.update(
{
name: value
for name, value in funneled.items()
if name.startswith("ssl_")
}
)

if parsed.username:
connection_kwargs["username"] = unquote(parsed.username)
Expand Down
31 changes: 31 additions & 0 deletions tests/test_sentinel_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,37 @@ def test_parse_sentinel_url_passes_through_pool_options():
assert config.sentinel_kwargs == {}


def test_parse_sentinel_url_tls_shares_ssl_options_with_sentinels():
"""On the rediss+sentinel scheme the ssl_* options apply to the Sentinel
daemon connections too, so one private CA verifies the whole topology
(discovery would otherwise fail certificate verification)."""
config = parse_sentinel_url(
"rediss+sentinel://sentinel-a/mymaster"
"?ssl_ca_certs=/etc/redis/ca.pem&ssl_check_hostname=false&socket_timeout=5"
)
assert config.connection_kwargs == {
"ssl": True,
"ssl_ca_certs": "/etc/redis/ca.pem",
"ssl_check_hostname": False,
"socket_timeout": 5.0,
}
assert config.sentinel_kwargs == {
"ssl": True,
"ssl_ca_certs": "/etc/redis/ca.pem",
"ssl_check_hostname": False,
}


def test_parse_sentinel_url_plaintext_sentinels_get_no_ssl_options():
"""Without TLS the daemons stay plaintext: ssl_* options pass through to
the data-node kwargs only, like any other redis-py option."""
config = parse_sentinel_url(
"redis+sentinel://sentinel-a/mymaster?ssl_ca_certs=/etc/redis/ca.pem"
)
assert config.connection_kwargs == {"ssl_ca_certs": "/etc/redis/ca.pem"}
assert config.sentinel_kwargs == {}


def test_parse_sentinel_url_db_comes_from_path_only():
"""The database is taken from the path; a stray ?db= is not an alternate
source, and credentials stay sourced from the URL userinfo."""
Expand Down