diff --git a/.gitignore b/.gitignore index a88e694f..659b0e14 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,6 @@ venv.bak/ # mypy .mypy_cache/ + +# OSX specific files +.DS_Store diff --git a/dapr/aio/clients/grpc/client.py b/dapr/aio/clients/grpc/client.py index 2f49fdc7..58deeb3e 100644 --- a/dapr/aio/clients/grpc/client.py +++ b/dapr/aio/clients/grpc/client.py @@ -41,7 +41,7 @@ from dapr.clients.exceptions import DaprInternalError from dapr.clients.grpc._state import StateOptions, StateItem from dapr.clients.grpc._helpers import getWorkflowRuntimeStatus -from dapr.conf.helpers import parse_endpoint +from dapr.conf.helpers import GrpcEndpoint from dapr.conf import settings from dapr.proto import api_v1, api_service_v1, common_v1 from dapr.proto.runtime.v1.dapr_pb2 import UnsubscribeConfigurationResponse @@ -139,14 +139,18 @@ def __init__( address = settings.DAPR_GRPC_ENDPOINT or (f"{settings.DAPR_RUNTIME_HOST}:" f"{settings.DAPR_GRPC_PORT}") - self._scheme, self._hostname, self._port = parse_endpoint(address) + try: + self._uri = GrpcEndpoint(address) + except ValueError as error: + raise DaprInternalError(f'{error}') from error - if self._scheme == "https": - self._channel = grpc.aio.secure_channel(f"{self._hostname}:{self._port}", + if self._uri.tls: + self._channel = grpc.aio.secure_channel(self._uri.endpoint, credentials=self.get_credentials(), - options=options) + options=options) # type: ignore else: - self._channel = grpc.aio.insecure_channel(address, options) # type: ignore + self._channel = grpc.aio.insecure_channel(self._uri.endpoint, + options) # type: ignore if settings.DAPR_API_TOKEN: api_token_interceptor = DaprClientInterceptorAsync([ @@ -164,7 +168,7 @@ def get_credentials(self): async def close(self): """Closes Dapr runtime gRPC channel.""" - if self._channel: + if hasattr(self, '_channel') and self._channel: self._channel.close() async def __aenter__(self) -> Self: # type: ignore @@ -1442,7 +1446,7 @@ async def wait(self, timeout_s: float): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.settimeout(timeout_s) try: - s.connect((self._hostname, self._port)) + s.connect((self._uri.hostname, self._uri.port_as_int)) return except Exception as e: remaining = (start + timeout_s) - time.time() diff --git a/dapr/clients/grpc/client.py b/dapr/clients/grpc/client.py index 8149bc83..cccf5418 100644 --- a/dapr/clients/grpc/client.py +++ b/dapr/clients/grpc/client.py @@ -52,7 +52,7 @@ validateNotNone, validateNotBlankString, ) -from dapr.conf.helpers import parse_endpoint +from dapr.conf.helpers import GrpcEndpoint from dapr.clients.grpc._request import ( InvokeMethodRequest, BindingRequest, @@ -138,15 +138,18 @@ def __init__( address = settings.DAPR_GRPC_ENDPOINT or (f"{settings.DAPR_RUNTIME_HOST}:" f"{settings.DAPR_GRPC_PORT}") - self._scheme, self._hostname, self._port = parse_endpoint(address) + try: + self._uri = GrpcEndpoint(address) + except ValueError as error: + raise DaprInternalError(f'{error}') from error - if self._scheme == "https": - self._channel = grpc.secure_channel(f"{self._hostname}:{self._port}", # type: ignore + if self._uri.tls: + self._channel = grpc.secure_channel(self._uri.endpoint, # type: ignore self.get_credentials(), - options=options) else: - self._channel = grpc.insecure_channel(address, options=options) # type: ignore + self._channel = grpc.insecure_channel(self._uri.endpoint, # type: ignore + options=options) if settings.DAPR_API_TOKEN: api_token_interceptor = DaprClientInterceptor([ @@ -166,7 +169,7 @@ def get_credentials(self): def close(self): """Closes Dapr runtime gRPC channel.""" - if self._channel: + if hasattr(self, '_channel') and self._channel: self._channel.close() def __del__(self): @@ -805,8 +808,8 @@ def delete_state( :class:`DaprResponse` gRPC metadata returned from callee """ if metadata is not None: - warn('metadata argument is deprecated. Dapr already intercepts API token headers ' - 'and this is not needed.', DeprecationWarning, stacklevel=2) + warn('metadata argument is deprecated. Dapr already intercepts API token ' + 'headers and this is not needed.', DeprecationWarning, stacklevel=2) if not store_name or len(store_name) == 0 or len(store_name.strip()) == 0: raise ValueError("State store name cannot be empty") @@ -861,8 +864,8 @@ def get_secret( :class:`GetSecretResponse` object with the secret and metadata returned from callee """ if metadata is not None: - warn('metadata argument is deprecated. Dapr already intercepts API token headers ' - 'and this is not needed.', DeprecationWarning, stacklevel=2) + warn('metadata argument is deprecated. Dapr already intercepts API token ' + 'headers and this is not needed.', DeprecationWarning, stacklevel=2) req = api_v1.GetSecretRequest( store_name=store_name, @@ -908,8 +911,8 @@ def get_bulk_secret( :class:`GetBulkSecretResponse` object with secrets and metadata returned from callee """ if metadata is not None: - warn('metadata argument is deprecated. Dapr already intercepts API token headers ' - 'and this is not needed.', DeprecationWarning, stacklevel=2) + warn('metadata argument is deprecated. Dapr already intercepts API token ' + 'headers and this is not needed.', DeprecationWarning, stacklevel=2) req = api_v1.GetBulkSecretRequest( store_name=store_name, @@ -1431,7 +1434,7 @@ def wait(self, timeout_s: float): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.settimeout(timeout_s) try: - s.connect((self._hostname, self._port)) + s.connect((self._uri.hostname, self._uri.port_as_int)) return except Exception as e: remaining = (start + timeout_s) - time.time() diff --git a/dapr/conf/helpers.py b/dapr/conf/helpers.py index 3878f0aa..0342b751 100644 --- a/dapr/conf/helpers.py +++ b/dapr/conf/helpers.py @@ -1,56 +1,183 @@ -from typing import Tuple - - -def parse_endpoint(address: str) -> Tuple[str, str, int]: - scheme = "http" - fqdn = "localhost" - port = 80 - addr = address - - addr_list = address.split("://") - - if len(addr_list) == 2: - # A scheme was explicitly specified - scheme = addr_list[0] - if scheme == "https": - port = 443 - addr = addr_list[1] - - addr_list = addr.split(":") - if len(addr_list) == 2: - # A port was explicitly specified - if len(addr_list[0]) > 0: - fqdn = addr_list[0] - # Account for Endpoints of the type http://localhost:3500/v1.0/invoke - addr_list = addr_list[1].split("/") - port = addr_list[0] # type: ignore - elif len(addr_list) == 1: - # No port was specified - # Account for Endpoints of the type :3500/v1.0/invoke - addr_list = addr_list[0].split("/") - fqdn = addr_list[0] - else: - # IPv6 address - addr_list = addr.split("]:") - if len(addr_list) == 2: - # A port was explicitly specified - fqdn = addr_list[0] - fqdn = fqdn.replace("[", "") - - addr_list = addr_list[1].split("/") - port = addr_list[0] # type: ignore - elif len(addr_list) == 1: - # No port was specified - addr_list = addr_list[0].split("/") - fqdn = addr_list[0] - fqdn = fqdn.replace("[", "") - fqdn = fqdn.replace("]", "") +from warnings import warn +from urllib.parse import urlparse, parse_qs, ParseResult + + +class URIParseConfig: + DEFAULT_SCHEME = "dns" + DEFAULT_HOSTNAME = "localhost" + DEFAULT_PORT = 443 + DEFAULT_AUTHORITY = "" + ACCEPTED_SCHEMES = ["dns", "unix", "unix-abstract", "vsock", "http", "https", "grpc", "grpcs"] + + +class GrpcEndpoint: + _scheme: str + _hostname: str + _port: int + _tls: bool + _authority: str + _url: str + _parsed_url: ParseResult # from urllib.parse + _endpoint: str + + def __init__(self, url: str): + self._authority = URIParseConfig.DEFAULT_AUTHORITY + self._url = url + + self._parsed_url = urlparse(self._preprocess_uri(url)) + self._validate_path_and_query() + + self._set_tls() + self._set_hostname() + self._set_scheme() + self._set_port() + self._set_endpoint() + + def _set_scheme(self): + if len(self._parsed_url.scheme) == 0: + self._scheme = URIParseConfig.DEFAULT_SCHEME + return + + if self._parsed_url.scheme in ["http", "https"]: + self._scheme = URIParseConfig.DEFAULT_SCHEME + warn("http and https schemes are deprecated, use grpc or grpcs instead") + return + + if self._parsed_url.scheme not in URIParseConfig.ACCEPTED_SCHEMES: + raise ValueError(f"invalid scheme '{self._parsed_url.scheme}' in URL '{self._url}'") + + self._scheme = self._parsed_url.scheme + + @property + def scheme(self) -> str: + return self._scheme + + def _set_hostname(self): + if self._parsed_url.hostname is None: + self._hostname = URIParseConfig.DEFAULT_HOSTNAME + return + + if self._parsed_url.hostname.count(":") == 7: + # IPv6 address + self._hostname = f"[{self._parsed_url.hostname}]" + return + + self._hostname = self._parsed_url.hostname + + @property + def hostname(self) -> str: + return self._hostname + + def _set_port(self): + if self._parsed_url.scheme in ["unix", "unix-abstract"]: + self._port = 0 + return + + if self._parsed_url.port is None: + self._port = URIParseConfig.DEFAULT_PORT + return + + self._port = self._parsed_url.port + + @property + def port(self) -> str: + if self._port == 0: + return "" + + return str(self._port) + + @property + def port_as_int(self) -> int: + return self._port + + def _set_endpoint(self): + port = "" if not self._port else f":{self.port}" + + if self._scheme == "unix": + separator = "://" if self._url.startswith("unix://") else ":" + self._endpoint = f"{self._scheme}{separator}{self._hostname}" + return + + if self._scheme == "vsock": + self._endpoint = f"{self._scheme}:{self._hostname}:{self.port}" + return + + if self._scheme == "unix-abstract": + self._endpoint = f"{self._scheme}:{self._hostname}{port}" + return + + if self._scheme == "dns": + authority = f"//{self._authority}/" if self._authority else "" + self._endpoint = f"{self._scheme}:{authority}{self._hostname}{port}" + return + + self._endpoint = f"{self._scheme}:{self._hostname}{port}" + + @property + def endpoint(self) -> str: + return self._endpoint + + # Prepares the uri string in a specific format for parsing by the urlparse function + def _preprocess_uri(self, url: str) -> str: + url_list = url.split(":") + if len(url_list) == 3 and "://" not in url: + # A URI like dns:mydomain:5000 or vsock:mycid:5000 was used + url = url.replace(":", "://", 1) + elif len(url_list) >= 2 and "://" not in url and url_list[ + 0] in URIParseConfig.ACCEPTED_SCHEMES: + + # A URI like dns:mydomain or dns:[2001:db8:1f70::999:de8:7648:6e8]:mydomain was used + # Possibly a URI like dns:[2001:db8:1f70::999:de8:7648:6e8]:mydomain was used + url = url.replace(":", "://", 1) else: - raise ValueError(f"Invalid address: {address}") + url_list = url.split("://") + if len(url_list) == 1: + # If a scheme was not explicitly specified in the URL + # we need to add a default scheme, + # because of how urlparse works + url = f'{URIParseConfig.DEFAULT_SCHEME}://{url}' + else: + # If a scheme was explicitly specified in the URL + # we need to make sure it is a valid scheme + scheme = url_list[0] + if scheme not in URIParseConfig.ACCEPTED_SCHEMES: + raise ValueError(f"invalid scheme '{scheme}' in URL '{url}'") + + # We should do a special check if the scheme is dns, and it uses + # an authority in the format of dns:[//authority/]host[:port] + if scheme.lower() == "dns": + # A URI like dns://authority/mydomain was used + url_list = url.split("/") + if len(url_list) < 4: + raise ValueError(f"invalid dns authority '{url_list[2]}' in URL '{url}'") + self._authority = url_list[2] + url = f'dns://{url_list[3]}' + return url + + def _set_tls(self): + query_dict = parse_qs(self._parsed_url.query) + tls_str = query_dict.get('tls', [""])[0] + tls = tls_str.lower() == 'true' + if self._parsed_url.scheme == "https": + tls = True + + self._tls = tls - try: - port = int(port) - except ValueError: - raise ValueError(f"invalid port: {port}") + @property + def tls(self) -> bool: + return self._tls - return scheme, fqdn, port + def _validate_path_and_query(self) -> None: + if self._parsed_url.path: + raise ValueError(f"paths are not supported for gRPC endpoints:" + f" '{self._parsed_url.path}'") + if self._parsed_url.query: + query_dict = parse_qs(self._parsed_url.query) + if 'tls' in query_dict and self._parsed_url.scheme in ["http", "https"]: + raise ValueError( + f"the tls query parameter is not supported for http(s) endpoints: " + f"'{self._parsed_url.query}'") + query_dict.pop('tls', None) + if query_dict: + raise ValueError(f"query parameters are not supported for gRPC endpoints:" + f" '{self._parsed_url.query}'") diff --git a/daprdocs/content/en/python-sdk-docs/python-client.md b/daprdocs/content/en/python-sdk-docs/python-client.md index 0b5999a8..73a1cf70 100644 --- a/daprdocs/content/en/python-sdk-docs/python-client.md +++ b/daprdocs/content/en/python-sdk-docs/python-client.md @@ -39,27 +39,32 @@ with DaprClient() as d: ``` #### Specifying an endpoint on initialisation: -When passed as an argument in the constructor, the endpoint takes precedence over any configuration or environment variable. +When passed as an argument in the constructor, the gRPC endpoint takes precedence over any configuration or environment variable. ```python from dapr.clients import DaprClient -with DaprClient("https://mydomain:4443") as d: +with DaprClient("mydomain:50051?tls=true") as d: # use the client ``` -#### Specifying an endpoint in an environment variable: -You can use the standardised `DAPR_GRPC_ENDPOINT` and/or `DAPR_HTTP_ENDPOINT` environment variables to -specify the endpoint. When these environment variables are set, the client can be initialised -without any arguments. +#### Specifying the endpoint in an environment variable: +You can use the standardised `DAPR_GRPC_ENDPOINT` environment variable to +specify the gRPC endpoint. When this variable is set, the client can be initialised +without any arguments: ```bash -export DAPR_GRPC_ENDPOINT="https://mydomain:50051" -export DAPR_HTTP_ENDPOINT="https://mydomain:443" +export DAPR_GRPC_ENDPOINT="mydomain:50051?tls=true" ``` +```python +from dapr.clients import DaprClient + +with DaprClient() as d: + # the client will use the endpoint specified in the environment variables +``` The legacy environment variables `DAPR_RUNTIME_HOST`, `DAPR_HTTP_PORT` and `DAPR_GRPC_PORT` are -also supported, but `DAPR_GRPC_ENDPOINT` and `DAPR_HTTP_ENDPOINT` take precedence. +also supported, but `DAPR_GRPC_ENDPOINT` takes precedence. ## Building blocks diff --git a/tests/clients/test_secure_dapr_async_grpc_client.py b/tests/clients/test_secure_dapr_async_grpc_client.py index 14b25697..c9780e60 100644 --- a/tests/clients/test_secure_dapr_async_grpc_client.py +++ b/tests/clients/test_secure_dapr_async_grpc_client.py @@ -53,33 +53,25 @@ def tearDown(self): @patch.object(settings, "DAPR_GRPC_ENDPOINT", "https://domain1.com:5000") def test_init_with_DAPR_GRPC_ENDPOINT(self): dapr = DaprGrpcClientAsync() - self.assertEqual("domain1.com", dapr._hostname) - self.assertEqual(5000, dapr._port) - self.assertEqual("https", dapr._scheme) + self.assertEqual("dns:domain1.com:5000", dapr._uri.endpoint) @patch.object(settings, "DAPR_GRPC_ENDPOINT", "https://domain1.com:5000") def test_init_with_DAPR_GRPC_ENDPOINT_and_argument(self): dapr = DaprGrpcClientAsync("https://domain2.com:5002") - self.assertEqual("domain2.com", dapr._hostname) - self.assertEqual(5002, dapr._port) - self.assertEqual('https', dapr._scheme) + self.assertEqual("dns:domain2.com:5002", dapr._uri.endpoint) @patch.object(settings, "DAPR_GRPC_ENDPOINT", "https://domain1.com:5000") @patch.object(settings, "DAPR_RUNTIME_HOST", "domain2.com") @patch.object(settings, "DAPR_GRPC_PORT", "5002") def test_init_with_DAPR_GRPC_ENDPOINT_and_DAPR_RUNTIME_HOST(self): dapr = DaprGrpcClientAsync() - self.assertEqual("domain1.com", dapr._hostname) - self.assertEqual(5000, dapr._port) - self.assertEqual('https', dapr._scheme) + self.assertEqual("dns:domain1.com:5000", dapr._uri.endpoint) @patch.object(settings, "DAPR_RUNTIME_HOST", "domain1.com") @patch.object(settings, "DAPR_GRPC_PORT", "5000") def test_init_with_argument_and_DAPR_GRPC_ENDPOINT_and_DAPR_RUNTIME_HOST(self): dapr = DaprGrpcClientAsync("https://domain2.com:5002") - self.assertEqual("domain2.com", dapr._hostname) - self.assertEqual(5002, dapr._port) - self.assertEqual('https', dapr._scheme) + self.assertEqual("dns:domain2.com:5002", dapr._uri.endpoint) async def test_dapr_api_token_insertion(self): pass diff --git a/tests/clients/test_secure_dapr_grpc_client.py b/tests/clients/test_secure_dapr_grpc_client.py index 4ce346a4..e9f227ac 100644 --- a/tests/clients/test_secure_dapr_grpc_client.py +++ b/tests/clients/test_secure_dapr_grpc_client.py @@ -51,33 +51,25 @@ def tearDown(self): @patch.object(settings, "DAPR_GRPC_ENDPOINT", "https://domain1.com:5000") def test_init_with_DAPR_GRPC_ENDPOINT(self): dapr = DaprGrpcClient() - self.assertEqual("domain1.com", dapr._hostname) - self.assertEqual(5000, dapr._port) - self.assertEqual("https", dapr._scheme) + self.assertEqual("dns:domain1.com:5000", dapr._uri.endpoint) @patch.object(settings, "DAPR_GRPC_ENDPOINT", "https://domain1.com:5000") def test_init_with_DAPR_GRPC_ENDPOINT_and_argument(self): dapr = DaprGrpcClient("https://domain2.com:5002") - self.assertEqual("domain2.com", dapr._hostname) - self.assertEqual(5002, dapr._port) - self.assertEqual('https', dapr._scheme) + self.assertEqual("dns:domain2.com:5002", dapr._uri.endpoint) @patch.object(settings, "DAPR_GRPC_ENDPOINT", "https://domain1.com:5000") @patch.object(settings, "DAPR_RUNTIME_HOST", "domain2.com") @patch.object(settings, "DAPR_GRPC_PORT", "5002") def test_init_with_DAPR_GRPC_ENDPOINT_and_DAPR_RUNTIME_HOST(self): dapr = DaprGrpcClient() - self.assertEqual("domain1.com", dapr._hostname) - self.assertEqual(5000, dapr._port) - self.assertEqual('https', dapr._scheme) + self.assertEqual("dns:domain1.com:5000", dapr._uri.endpoint) @patch.object(settings, "DAPR_RUNTIME_HOST", "domain1.com") @patch.object(settings, "DAPR_GRPC_PORT", "5000") def test_init_with_argument_and_DAPR_GRPC_ENDPOINT_and_DAPR_RUNTIME_HOST(self): dapr = DaprGrpcClient("https://domain2.com:5002") - self.assertEqual("domain2.com", dapr._hostname) - self.assertEqual(5002, dapr._port) - self.assertEqual('https', dapr._scheme) + self.assertEqual("dns:domain2.com:5002", dapr._uri.endpoint) if __name__ == '__main__': diff --git a/tests/conf/helpers_test.py b/tests/conf/helpers_test.py index c57b3236..90aa8166 100644 --- a/tests/conf/helpers_test.py +++ b/tests/conf/helpers_test.py @@ -1,144 +1,157 @@ import unittest -from dapr.conf.helpers import parse_endpoint +from dapr.conf.helpers import GrpcEndpoint class DaprClientHelpersTests(unittest.TestCase): - def test_parse_endpoint(self): - testcases = [{"endpoint": ":5000", "scheme": "http", "host": "localhost", "port": 5000}, - {"endpoint": ":5000/v1/dapr", "scheme": "http", "host": "localhost", - "port": 5000}, - - {"endpoint": "localhost", "scheme": "http", "host": "localhost", "port": 80}, - {"endpoint": "localhost/v1/dapr", "scheme": "http", "host": "localhost", - "port": 80}, - {"endpoint": "localhost:5000", "scheme": "http", "host": "localhost", - "port": 5000}, - {"endpoint": "localhost:5000/v1/dapr", "scheme": "http", "host": "localhost", - "port": 5000}, - - {"endpoint": "http://localhost", "scheme": "http", "host": "localhost", - "port": 80}, - {"endpoint": "http://localhost/v1/dapr", "scheme": "http", "host": "localhost", - "port": 80}, - {"endpoint": "http://localhost:5000", "scheme": "http", "host": "localhost", - "port": 5000}, {"endpoint": "http://localhost:5000/v1/dapr", "scheme": "http", - "host": "localhost", "port": 5000}, - - {"endpoint": "https://localhost", "scheme": "https", "host": "localhost", - "port": 443}, {"endpoint": "https://localhost/v1/dapr", "scheme": "https", - "host": "localhost", "port": 443}, - {"endpoint": "https://localhost:5000", "scheme": "https", "host": "localhost", - "port": 5000}, - {"endpoint": "https://localhost:5000/v1/dapr", "scheme": "https", - "host": "localhost", "port": 5000}, - - {"endpoint": "127.0.0.1", "scheme": "http", "host": "127.0.0.1", "port": 80}, - {"endpoint": "127.0.0.1/v1/dapr", "scheme": "http", "host": "127.0.0.1", - "port": 80}, - {"endpoint": "127.0.0.1:5000", "scheme": "http", "host": "127.0.0.1", - "port": 5000}, - {"endpoint": "127.0.0.1:5000/v1/dapr", "scheme": "http", "host": "127.0.0.1", - "port": 5000}, - - {"endpoint": "http://127.0.0.1", "scheme": "http", "host": "127.0.0.1", - "port": 80}, - {"endpoint": "http://127.0.0.1/v1/dapr", "scheme": "http", "host": "127.0.0.1", - "port": 80}, - {"endpoint": "http://127.0.0.1:5000", "scheme": "http", "host": "127.0.0.1", - "port": 5000}, {"endpoint": "http://127.0.0.1:5000/v1/dapr", "scheme": "http", - "host": "127.0.0.1", "port": 5000}, - - {"endpoint": "https://127.0.0.1", "scheme": "https", "host": "127.0.0.1", - "port": 443}, {"endpoint": "https://127.0.0.1/v1/dapr", "scheme": "https", - "host": "127.0.0.1", "port": 443}, - {"endpoint": "https://127.0.0.1:5000", "scheme": "https", "host": "127.0.0.1", - "port": 5000}, - {"endpoint": "https://127.0.0.1:5000/v1/dapr", "scheme": "https", - "host": "127.0.0.1", "port": 5000}, - - {"endpoint": "[2001:db8:1f70::999:de8:7648:6e8]", "scheme": "http", - "host": "2001:db8:1f70::999:de8:7648:6e8", "port": 80}, - {"endpoint": "[2001:db8:1f70::999:de8:7648:6e8]/v1/dapr", "scheme": "http", - "host": "2001:db8:1f70::999:de8:7648:6e8", "port": 80}, - {"endpoint": "[2001:db8:1f70::999:de8:7648:6e8]:5000", "scheme": "http", - "host": "2001:db8:1f70::999:de8:7648:6e8", "port": 5000}, - {"endpoint": "[2001:db8:1f70::999:de8:7648:6e8]:5000/v1/dapr", - "scheme": "http", "host": "2001:db8:1f70::999:de8:7648:6e8", "port": 5000}, - - {"endpoint": "http://[2001:db8:1f70::999:de8:7648:6e8]", "scheme": "http", - "host": "2001:db8:1f70::999:de8:7648:6e8", "port": 80}, - {"endpoint": "http://[2001:db8:1f70::999:de8:7648:6e8]/v1/dapr", - "scheme": "http", "host": "2001:db8:1f70::999:de8:7648:6e8", "port": 80}, - {"endpoint": "http://[2001:db8:1f70::999:de8:7648:6e8]:5000", "scheme": "http", - "host": "2001:db8:1f70::999:de8:7648:6e8", "port": 5000}, - {"endpoint": "http://[2001:db8:1f70::999:de8:7648:6e8]:5000/v1/dapr", - "scheme": "http", "host": "2001:db8:1f70::999:de8:7648:6e8", "port": 5000}, - - {"endpoint": "https://[2001:db8:1f70::999:de8:7648:6e8]", "scheme": "https", - "host": "2001:db8:1f70::999:de8:7648:6e8", "port": 443}, - {"endpoint": "https://[2001:db8:1f70::999:de8:7648:6e8]/v1/dapr", - "scheme": "https", "host": "2001:db8:1f70::999:de8:7648:6e8", "port": 443}, - {"endpoint": "https://[2001:db8:1f70::999:de8:7648:6e8]:5000", - "scheme": "https", "host": "2001:db8:1f70::999:de8:7648:6e8", "port": 5000}, - {"endpoint": "https://[2001:db8:1f70::999:de8:7648:6e8]:5000/v1/dapr", - "scheme": "https", "host": "2001:db8:1f70::999:de8:7648:6e8", "port": 5000}, - - {"endpoint": "domain.com", "scheme": "http", "host": "domain.com", "port": 80}, - {"endpoint": "domain.com/v1/grpc", "scheme": "http", "host": "domain.com", - "port": 80}, - {"endpoint": "domain.com:5000", "scheme": "http", "host": "domain.com", - "port": 5000}, - {"endpoint": "domain.com:5000/v1/dapr", "scheme": "http", "host": "domain.com", - "port": 5000}, - - {"endpoint": "http://domain.com", "scheme": "http", "host": "domain.com", - "port": 80}, {"endpoint": "http://domain.com/v1/dapr", "scheme": "http", - "host": "domain.com", "port": 80}, - {"endpoint": "http://domain.com:5000", "scheme": "http", "host": "domain.com", - "port": 5000}, - {"endpoint": "http://domain.com:5000/v1/dapr", "scheme": "http", - "host": "domain.com", "port": 5000}, - - {"endpoint": "https://domain.com", "scheme": "https", "host": "domain.com", - "port": 443}, {"endpoint": "https://domain.com/v1/dapr", "scheme": "https", - "host": "domain.com", "port": 443}, - {"endpoint": "https://domain.com:5000", "scheme": "https", - "host": "domain.com", "port": 5000}, - {"endpoint": "https://domain.com:5000/v1/dapr", "scheme": "https", - "host": "domain.com", "port": 5000}, - - {"endpoint": "abc.domain.com", "scheme": "http", "host": "abc.domain.com", - "port": 80}, {"endpoint": "abc.domain.com/v1/grpc", "scheme": "http", - "host": "abc.domain.com", "port": 80}, - {"endpoint": "abc.domain.com:5000", "scheme": "http", "host": "abc.domain.com", - "port": 5000}, {"endpoint": "abc.domain.com:5000/v1/dapr", "scheme": "http", - "host": "abc.domain.com", "port": 5000}, - - {"endpoint": "http://abc.domain.com/v1/dapr", "scheme": "http", - "host": "abc.domain.com", "port": 80}, - {"endpoint": "http://abc.domain.com/v1/dapr", "scheme": "http", - "host": "abc.domain.com", "port": 80}, - {"endpoint": "http://abc.domain.com:5000/v1/dapr", "scheme": "http", - "host": "abc.domain.com", "port": 5000}, - {"endpoint": "http://abc.domain.com:5000/v1/dapr/v1/dapr", "scheme": "http", - "host": "abc.domain.com", "port": 5000}, - - {"endpoint": "https://abc.domain.com/v1/dapr", "scheme": "https", - "host": "abc.domain.com", "port": 443}, - {"endpoint": "https://abc.domain.com/v1/dapr", "scheme": "https", - "host": "abc.domain.com", "port": 443}, - {"endpoint": "https://abc.domain.com:5000/v1/dapr", "scheme": "https", - "host": "abc.domain.com", "port": 5000}, - {"endpoint": "https://abc.domain.com:5000/v1/dapr/v1/dapr", "scheme": "https", - "host": "abc.domain.com", "port": 5000}, - - ] + def test_parse_grpc_endpoint(self): + testcases = [ + # Port only + {"url": ":5000", "error": False, "secure": False, "scheme": "", "host": "localhost", + "port": 5000, "endpoint": "dns:localhost:5000"}, + {"url": ":5000?tls=false", "error": False, "secure": False, "scheme": "", + "host": "localhost", "port": 5000, "endpoint": "dns:localhost:5000"}, + {"url": ":5000?tls=true", "error": False, "secure": True, "scheme": "", + "host": "localhost", "port": 5000, "endpoint": "dns:localhost:5000"}, + + # Host only + {"url": "myhost", "error": False, "secure": False, "scheme": "", "host": "myhost", + "port": 443, "endpoint": "dns:myhost:443"}, + {"url": "myhost?tls=false", "error": False, "secure": False, "scheme": "", + "host": "myhost", "port": 443, "endpoint": "dns:myhost:443"}, + {"url": "myhost?tls=true", "error": False, "secure": True, "scheme": "", + "host": "myhost", "port": 443, "endpoint": "dns:myhost:443"}, + + # Host and port + {"url": "myhost:443", "error": False, "secure": False, "scheme": "", "host": "myhost", + "port": 443, "endpoint": "dns:myhost:443"}, + {"url": "myhost:443?tls=false", "error": False, "secure": False, "scheme": "", + "host": "myhost", "port": 443, "endpoint": "dns:myhost:443"}, + {"url": "myhost:443?tls=true", "error": False, "secure": True, "scheme": "", + "host": "myhost", "port": 443, "endpoint": "dns:myhost:443"}, + + # Scheme, host and port + {"url": "http://myhost", "error": False, "secure": False, "scheme": "", + "host": "myhost", "port": 443, "endpoint": "dns:myhost:443"}, + {"url": "http://myhost?tls=false", "error": True}, + # We can't have both http/https and the tls query parameter + {"url": "http://myhost?tls=true", "error": True}, + # We can't have both http/https and the tls query parameter + + {"url": "http://myhost:443", "error": False, "secure": False, "scheme": "", + "host": "myhost", "port": 443, "endpoint": "dns:myhost:443"}, + {"url": "http://myhost:443?tls=false", "error": True}, + # We can't have both http/https and the tls query parameter + {"url": "http://myhost:443?tls=true", "error": True}, + # We can't have both http/https and the tls query parameter + + {"url": "http://myhost:5000", "error": False, "secure": False, "scheme": "", + "host": "myhost", "port": 5000, "endpoint": "dns:myhost:5000"}, + {"url": "http://myhost:5000?tls=false", "error": True}, + # We can't have both http/https and the tls query parameter + {"url": "http://myhost:5000?tls=true", "error": True}, + # We can't have both http/https and the tls query parameter + + {"url": "https://myhost:443", "error": False, "secure": True, "scheme": "", + "host": "myhost", "port": 443, "endpoint": "dns:myhost:443"}, + {"url": "https://myhost:443?tls=false", "error": True}, + {"url": "https://myhost:443?tls=true", "error": True}, + + # Scheme = dns + {"url": "dns:myhost", "error": False, "secure": False, "scheme": "dns", + "host": "myhost", "port": 443, "endpoint": "dns:myhost:443"}, + {"url": "dns:myhost?tls=false", "error": False, "secure": False, "scheme": "dns", + "host": "myhost", "port": 443, "endpoint": "dns:myhost:443"}, + {"url": "dns:myhost?tls=true", "error": False, "secure": True, "scheme": "dns", + "host": "myhost", "port": 443, "endpoint": "dns:myhost:443"}, + + # Scheme = dns with authority + {"url": "dns://myauthority:53/myhost", "error": False, "secure": False, "scheme": "dns", + "host": "myhost", "port": 443, "endpoint": "dns://myauthority:53/myhost:443"}, + {"url": "dns://myauthority:53/myhost?tls=false", "error": False, "secure": False, + "scheme": "dns", "host": "myhost", "port": 443, + "endpoint": "dns://myauthority:53/myhost:443"}, + {"url": "dns://myauthority:53/myhost?tls=true", "error": False, "secure": True, + "scheme": "dns", "host": "myhost", "port": 443, + "endpoint": "dns://myauthority:53/myhost:443"}, {"url": "dns://myhost", "error": True}, + + # Unix sockets + {"url": "unix:my.sock", "error": False, "secure": False, "scheme": "unix", + "host": "my.sock", "port": "", "endpoint": "unix:my.sock"}, + {"url": "unix:my.sock?tls=true", "error": False, "secure": True, "scheme": "unix", + "host": "my.sock", "port": "", "endpoint": "unix:my.sock"}, + + # Unix sockets with absolute path + {"url": "unix://my.sock", "error": False, "secure": False, "scheme": "unix", + "host": "my.sock", "port": "", "endpoint": "unix://my.sock"}, + {"url": "unix://my.sock?tls=true", "error": False, "secure": True, "scheme": "unix", + "host": "my.sock", "port": "", "endpoint": "unix://my.sock"}, + + # Unix abstract sockets + {"url": "unix-abstract:my.sock", "error": False, "secure": False, "scheme": "unix", + "host": "my.sock", "port": "", "endpoint": "unix-abstract:my.sock"}, + {"url": "unix-abstract:my.sock?tls=true", "error": False, "secure": True, + "scheme": "unix", "host": "my.sock", "port": "", "endpoint": "unix-abstract:my.sock"}, + + # Vsock + {"url": "vsock:mycid", "error": False, "secure": False, "scheme": "vsock", + "host": "mycid", "port": "443", "endpoint": "vsock:mycid:443"}, + {"url": "vsock:mycid:5000", "error": False, "secure": False, "scheme": "vsock", + "host": "mycid", "port": 5000, "endpoint": "vsock:mycid:5000"}, + {"url": "vsock:mycid:5000?tls=true", "error": False, "secure": True, "scheme": "vsock", + "host": "mycid", "port": 5000, "endpoint": "vsock:mycid:5000"}, + + # IPv6 addresses with dns scheme + {"url": "[2001:db8:1f70::999:de8:7648:6e8]", "error": False, "secure": False, + "scheme": "", "host": "[2001:db8:1f70::999:de8:7648:6e8]", "port": 443, + "endpoint": "dns:[2001:db8:1f70::999:de8:7648:6e8]:443"}, + {"url": "dns:[2001:db8:1f70::999:de8:7648:6e8]", "error": False, "secure": False, + "scheme": "", "host": "[2001:db8:1f70::999:de8:7648:6e8]", "port": 443, + "endpoint": "dns:[2001:db8:1f70::999:de8:7648:6e8]:443"}, + {"url": "dns:[2001:db8:1f70::999:de8:7648:6e8]:5000", "error": False, "secure": False, + "scheme": "", "host": "[2001:db8:1f70::999:de8:7648:6e8]", "port": 5000, + "endpoint": "dns:[2001:db8:1f70::999:de8:7648:6e8]:5000"}, + {"url": "dns:[2001:db8:1f70::999:de8:7648:6e8]:5000?abc=[]", "error": True}, + + # IPv6 addresses with dns scheme and authority + {"url": "dns://myauthority:53/[2001:db8:1f70::999:de8:7648:6e8]", "error": False, + "secure": False, "scheme": "dns", "host": "[2001:db8:1f70::999:de8:7648:6e8]", + "port": 443, "endpoint": "dns://myauthority:53/[2001:db8:1f70::999:de8:7648:6e8]:443"}, + + # IPv6 addresses with https scheme + {"url": "https://[2001:db8:1f70::999:de8:7648:6e8]", "error": False, "secure": True, + "scheme": "", "host": "[2001:db8:1f70::999:de8:7648:6e8]", "port": 443, + "endpoint": "dns:[2001:db8:1f70::999:de8:7648:6e8]:443"}, + {"url": "https://[2001:db8:1f70::999:de8:7648:6e8]:5000", "error": False, + "secure": True, "scheme": "", "host": "[2001:db8:1f70::999:de8:7648:6e8]", + "port": 5000, "endpoint": "dns:[2001:db8:1f70::999:de8:7648:6e8]:5000"}, + + # Invalid addresses (with path and queries) + {"url": "host:5000/v1/dapr", "error": True}, # Paths are not allowed in grpc endpoints + {"url": "host:5000/?a=1", "error": True}, # Query params not allowed in grpc endpoints + + # Invalid scheme + {"url": "inv-scheme://myhost", "error": True}, + {"url": "inv-scheme:myhost:5000", "error": True}, + ] for testcase in testcases: - o = parse_endpoint(testcase["endpoint"]) - - self.assertEqual(testcase["scheme"], o[0]) - self.assertEqual(testcase["host"], o[1]) - self.assertEqual(testcase["port"], o[2]) + # if testcase["error"]: + # with self.assertRaises(ValueError): + # GrpcEndpoint(testcase["url"]) + # else: + # url = GrpcEndpoint(testcase["url"]) + # assert url.endpoint == testcase["endpoint"] + # assert url.tls == testcase["secure"] + # assert url.hostname == testcase["host"] + # assert url.port == str(testcase["port"]) + try: + url = GrpcEndpoint(testcase["url"]) + print(f'{testcase["url"]}\t {url.endpoint} \t{url.hostname}\t{url.port}\t{url.tls}') + assert url.endpoint == testcase["endpoint"] + assert url.tls == testcase["secure"] + assert url.hostname == testcase["host"] + assert url.port == str(testcase["port"]) + except ValueError as error: + print(f'{testcase["url"]}\t \t\t\t\t Error: {error}')