1313# limitations under the License.
1414
1515"""Helpers for :mod:`grpc`."""
16- from typing import Generic , TypeVar , Iterator
16+ from typing import Generic , Iterator , Optional , TypeVar
1717
1818import collections
1919import functools
20- import logging
2120import warnings
2221
2322import grpc
5352# The list of gRPC Callable interfaces that return iterators.
5453_STREAM_WRAP_CLASSES = (grpc .UnaryStreamMultiCallable , grpc .StreamStreamMultiCallable )
5554
56- _LOGGER = logging .getLogger (__name__ )
57-
5855# denotes the proto response type for grpc calls
5956P = TypeVar ("P" )
6057
@@ -271,11 +268,24 @@ def _create_composite_credentials(
271268 # Create a set of grpc.CallCredentials using the metadata plugin.
272269 google_auth_credentials = grpc .metadata_call_credentials (metadata_plugin )
273270
274- if ssl_credentials is None :
275- ssl_credentials = grpc .ssl_channel_credentials ()
276-
277- # Combine the ssl credentials and the authorization credentials.
278- return grpc .composite_channel_credentials (ssl_credentials , google_auth_credentials )
271+ # if `ssl_credentials` is set, use `grpc.composite_channel_credentials` instead of
272+ # `grpc.compute_engine_channel_credentials` as the former supports passing
273+ # `ssl_credentials` via `channel_credentials` which is needed for mTLS.
274+ if ssl_credentials :
275+ # Combine the ssl credentials and the authorization credentials.
276+ # See https://grpc.github.io/grpc/python/grpc.html#grpc.composite_channel_credentials
277+ return grpc .composite_channel_credentials (
278+ ssl_credentials , google_auth_credentials
279+ )
280+ else :
281+ # Use grpc.compute_engine_channel_credentials in order to support Direct Path.
282+ # See https://grpc.github.io/grpc/python/grpc.html#grpc.compute_engine_channel_credentials
283+ # TODO(https://github.com/googleapis/python-api-core/issues/598):
284+ # Although `grpc.compute_engine_channel_credentials` returns channel credentials
285+ # outside of a Google Compute Engine environment (GCE), we should determine if
286+ # there is a way to reliably detect a GCE environment so that
287+ # `grpc.compute_engine_channel_credentials` is not called outside of GCE.
288+ return grpc .compute_engine_channel_credentials (google_auth_credentials )
279289
280290
281291def create_channel (
@@ -288,6 +298,7 @@ def create_channel(
288298 default_scopes = None ,
289299 default_host = None ,
290300 compression = None ,
301+ attempt_direct_path : Optional [bool ] = False ,
291302 ** kwargs ,
292303):
293304 """Create a secure channel with credentials.
@@ -311,6 +322,22 @@ def create_channel(
311322 default_host (str): The default endpoint. e.g., "pubsub.googleapis.com".
312323 compression (grpc.Compression): An optional value indicating the
313324 compression method to be used over the lifetime of the channel.
325+ attempt_direct_path (Optional[bool]): If set, Direct Path will be attempted
326+ when the request is made. Direct Path is only available within a Google
327+ Compute Engine (GCE) environment and provides a proxyless connection
328+ which increases the available throughput, reduces latency, and increases
329+ reliability. Note:
330+
331+ - This argument should only be set in a GCE environment and for Services
332+ that are known to support Direct Path.
333+ - If this argument is set outside of GCE, then this request will fail
334+ unless the back-end service happens to have configured fall-back to DNS.
335+ - If the request causes a `ServiceUnavailable` response, it is recommended
336+ that the client repeat the request with `attempt_direct_path` set to
337+ `False` as the Service may not support Direct Path.
338+ - Using `ssl_credentials` with `attempt_direct_path` set to `True` will
339+ result in `ValueError` as this combination is not yet supported.
340+
314341 kwargs: Additional key-word args passed to
315342 :func:`grpc_gcp.secure_channel` or :func:`grpc.secure_channel`.
316343 Note: `grpc_gcp` is only supported in environments with protobuf < 4.0.0.
@@ -320,8 +347,15 @@ def create_channel(
320347
321348 Raises:
322349 google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed.
350+ ValueError: If `ssl_credentials` is set and `attempt_direct_path` is set to `True`.
323351 """
324352
353+ # If `ssl_credentials` is set and `attempt_direct_path` is set to `True`,
354+ # raise ValueError as this is not yet supported.
355+ # See https://github.com/googleapis/python-api-core/issues/590
356+ if ssl_credentials and attempt_direct_path :
357+ raise ValueError ("Using ssl_credentials with Direct Path is not supported" )
358+
325359 composite_credentials = _create_composite_credentials (
326360 credentials = credentials ,
327361 credentials_file = credentials_file ,
@@ -332,17 +366,58 @@ def create_channel(
332366 default_host = default_host ,
333367 )
334368
369+ # Note that grpcio-gcp is deprecated
335370 if HAS_GRPC_GCP : # pragma: NO COVER
336371 if compression is not None and compression != grpc .Compression .NoCompression :
337- _LOGGER .debug (
338- "Compression argument is being ignored for grpc_gcp.secure_channel creation."
372+ warnings .warn (
373+ "The `compression` argument is ignored for grpc_gcp.secure_channel creation." ,
374+ DeprecationWarning ,
375+ )
376+ if attempt_direct_path :
377+ warnings .warn (
378+ """The `attempt_direct_path` argument is ignored for grpc_gcp.secure_channel creation.""" ,
379+ DeprecationWarning ,
339380 )
340381 return grpc_gcp .secure_channel (target , composite_credentials , ** kwargs )
382+
383+ if attempt_direct_path :
384+ target = _modify_target_for_direct_path (target )
385+
341386 return grpc .secure_channel (
342387 target , composite_credentials , compression = compression , ** kwargs
343388 )
344389
345390
391+ def _modify_target_for_direct_path (target : str ) -> str :
392+ """
393+ Given a target, return a modified version which is compatible with Direct Path.
394+
395+ Args:
396+ target (str): The target service address in the format 'hostname[:port]' or
397+ 'dns://hostname[:port]'.
398+
399+ Returns:
400+ target (str): The target service address which is converted into a format compatible with Direct Path.
401+ If the target contains `dns:///` or does not contain `:///`, the target will be converted in
402+ a format compatible with Direct Path; otherwise the original target will be returned as the
403+ original target may already denote Direct Path.
404+ """
405+
406+ # A DNS prefix may be included with the target to indicate the endpoint is living in the Internet,
407+ # outside of Google Cloud Platform.
408+ dns_prefix = "dns:///"
409+ # Remove "dns:///" if `attempt_direct_path` is set to True as
410+ # the Direct Path prefix `google-c2p:///` will be used instead.
411+ target = target .replace (dns_prefix , "" )
412+
413+ direct_path_separator = ":///"
414+ if direct_path_separator not in target :
415+ target_without_port = target .split (":" )[0 ]
416+ # Modify the target to use Direct Path by adding the `google-c2p:///` prefix
417+ target = f"google-c2p{ direct_path_separator } { target_without_port } "
418+ return target
419+
420+
346421_MethodCall = collections .namedtuple (
347422 "_MethodCall" , ("request" , "timeout" , "metadata" , "credentials" , "compression" )
348423)
0 commit comments