Skip to content

Commit 3fd2eed

Browse files
authored
Use SSL by default (#67)
This PR enable SSL by default and makes the default SSL option configurable for `parse_grpc_uri()`.
2 parents 2c55d40 + 47738a4 commit 3fd2eed

File tree

3 files changed

+62
-17
lines changed

3 files changed

+62
-17
lines changed

RELEASE_NOTES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
## Upgrading
88

9-
<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->
9+
- The `parse_grpc_uri` function (and `BaseApiClient` constructor) now enables SSL by default (`ssl=false` should be passed to disable it).
10+
- The `parse_grpc_uri` function now accepts an optional `default_ssl` parameter to set the default value for the `ssl` parameter when not present in the URI.
1011

1112
## New Features
1213

src/frequenz/client/base/channel.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,19 @@ def _to_bool(value: str) -> bool:
2323

2424

2525
def parse_grpc_uri(
26-
uri: str, channel_type: type[ChannelT], /, *, default_port: int = 9090
26+
uri: str,
27+
channel_type: type[ChannelT],
28+
/,
29+
*,
30+
default_port: int = 9090,
31+
default_ssl: bool = True,
2732
) -> ChannelT:
2833
"""Create a grpclib client channel from a URI.
2934
3035
The URI must have the following format:
3136
3237
```
33-
grpc://hostname[:port][?ssl=false]
38+
grpc://hostname[:port][?ssl=<bool>]
3439
```
3540
3641
A few things to consider about URI components:
@@ -39,14 +44,15 @@ def parse_grpc_uri(
3944
- If the port is omitted, the `default_port` is used.
4045
- If a query parameter is passed many times, the last value is used.
4146
- The only supported query parameter is `ssl`, which must be a boolean value and
42-
defaults to `false`.
47+
defaults to the `default_ssl` argument if not present.
4348
- Boolean query parameters can be specified with the following values
4449
(case-insensitive): `true`, `1`, `on`, `false`, `0`, `off`.
4550
4651
Args:
4752
uri: The gRPC URI specifying the connection parameters.
4853
channel_type: The type of channel to create.
4954
default_port: The default port number to use if the URI does not specify one.
55+
default_ssl: The default SSL setting to use if the URI does not specify one.
5056
5157
Returns:
5258
A grpclib client channel object.
@@ -69,7 +75,8 @@ def parse_grpc_uri(
6975
)
7076

7177
options = {k: v[-1] for k, v in parse_qs(parsed_uri.query).items()}
72-
ssl = _to_bool(options.pop("ssl", "false"))
78+
ssl_option = options.pop("ssl", None)
79+
ssl = _to_bool(ssl_option) if ssl_option is not None else default_ssl
7380
if options:
7481
raise ValueError(
7582
f"Unexpected query parameters {options!r} in the URI '{uri}'",

tests/test_channel.py

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""Test cases for the channel module."""
55

66
from dataclasses import dataclass
7+
from typing import NotRequired, TypedDict
78
from unittest import mock
89

910
import pytest
@@ -12,8 +13,8 @@
1213
from frequenz.client.base.channel import parse_grpc_uri
1314

1415
VALID_URLS = [
15-
("grpc://localhost", "localhost", 9090, False),
16-
("grpc://localhost:1234", "localhost", 1234, False),
16+
("grpc://localhost", "localhost", 9090, True),
17+
("grpc://localhost:1234", "localhost", 1234, True),
1718
("grpc://localhost:1234?ssl=true", "localhost", 1234, True),
1819
("grpc://localhost:1234?ssl=false", "localhost", 1234, False),
1920
("grpc://localhost:1234?ssl=1", "localhost", 1234, True),
@@ -29,12 +30,25 @@
2930
]
3031

3132

33+
class _CreateChannelKwargs(TypedDict):
34+
default_port: NotRequired[int]
35+
default_ssl: NotRequired[bool]
36+
37+
3238
@pytest.mark.parametrize("uri, host, port, ssl", VALID_URLS)
33-
def test_grpclib_parse_uri_ok(
39+
@pytest.mark.parametrize(
40+
"default_port", [None, 9090, 1234], ids=lambda x: f"default_port={x}"
41+
)
42+
@pytest.mark.parametrize(
43+
"default_ssl", [None, True, False], ids=lambda x: f"default_ssl={x}"
44+
)
45+
def test_grpclib_parse_uri_ok( # pylint: disable=too-many-arguments
3446
uri: str,
3547
host: str,
3648
port: int,
3749
ssl: bool,
50+
default_port: int | None,
51+
default_ssl: bool | None,
3852
) -> None:
3953
"""Test successful parsing of gRPC URIs using grpclib."""
4054

@@ -44,24 +58,39 @@ class _FakeChannel:
4458
port: int
4559
ssl: bool
4660

61+
kwargs = _CreateChannelKwargs()
62+
if default_port is not None:
63+
kwargs["default_port"] = default_port
64+
if default_ssl is not None:
65+
kwargs["default_ssl"] = default_ssl
66+
67+
expected_port = port if f":{port}" in uri or default_port is None else default_port
68+
expected_ssl = ssl if "ssl" in uri or default_ssl is None else default_ssl
69+
4770
with mock.patch(
4871
"frequenz.client.base.channel._grpchacks.grpclib_create_channel",
4972
return_value=_FakeChannel(host, port, ssl),
50-
):
51-
channel = parse_grpc_uri(uri, _grpchacks.GrpclibChannel)
73+
) as create_channel_mock:
74+
channel = parse_grpc_uri(uri, _grpchacks.GrpclibChannel, **kwargs)
5275

5376
assert isinstance(channel, _FakeChannel)
54-
assert channel.host == host
55-
assert channel.port == port
56-
assert channel.ssl == ssl
77+
create_channel_mock.assert_called_once_with(host, expected_port, expected_ssl)
5778

5879

5980
@pytest.mark.parametrize("uri, host, port, ssl", VALID_URLS)
60-
def test_grpcio_parse_uri_ok(
81+
@pytest.mark.parametrize(
82+
"default_port", [None, 9090, 1234], ids=lambda x: f"default_port={x}"
83+
)
84+
@pytest.mark.parametrize(
85+
"default_ssl", [None, True, False], ids=lambda x: f"default_ssl={x}"
86+
)
87+
def test_grpcio_parse_uri_ok( # pylint: disable=too-many-arguments,too-many-locals
6188
uri: str,
6289
host: str,
6390
port: int,
6491
ssl: bool,
92+
default_port: int | None,
93+
default_ssl: bool | None,
6594
) -> None:
6695
"""Test successful parsing of gRPC URIs using grpcio."""
6796
expected_channel = mock.MagicMock(
@@ -70,6 +99,14 @@ def test_grpcio_parse_uri_ok(
7099
expected_credentials = mock.MagicMock(
71100
name="mock_credentials", spec=_grpchacks.GrpcioChannel
72101
)
102+
expected_port = port if f":{port}" in uri or default_port is None else default_port
103+
expected_ssl = ssl if "ssl" in uri or default_ssl is None else default_ssl
104+
105+
kwargs = _CreateChannelKwargs()
106+
if default_port is not None:
107+
kwargs["default_port"] = default_port
108+
if default_ssl is not None:
109+
kwargs["default_ssl"] = default_ssl
73110

74111
with (
75112
mock.patch(
@@ -85,11 +122,11 @@ def test_grpcio_parse_uri_ok(
85122
return_value=expected_credentials,
86123
) as ssl_channel_credentials_mock,
87124
):
88-
channel = parse_grpc_uri(uri, _grpchacks.GrpcioChannel)
125+
channel = parse_grpc_uri(uri, _grpchacks.GrpcioChannel, **kwargs)
89126

90127
assert channel == expected_channel
91-
expected_target = f"{host}:{port}"
92-
if ssl:
128+
expected_target = f"{host}:{expected_port}"
129+
if expected_ssl:
93130
ssl_channel_credentials_mock.assert_called_once_with()
94131
secure_channel_mock.assert_called_once_with(
95132
expected_target, expected_credentials

0 commit comments

Comments
 (0)