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
@@ -271,11 +271,24 @@ def _create_composite_credentials(
271271 # Create a set of grpc.CallCredentials using the metadata plugin.
272272 google_auth_credentials = grpc .metadata_call_credentials (metadata_plugin )
273273
274- if ssl_credentials is None :
275- ssl_credentials = grpc .ssl_channel_credentials ()
274+ # if `ssl_credentials` is set, use `grpc.composite_channel_credentials` instead of
275+ # `grpc.compute_engine_channel_credentials` as the former supports passing
276+ # `ssl_credentials` via `channel_credentials` which is needed for mTLS.
277+ if ssl_credentials :
278+ # Combine the ssl credentials and the authorization credentials.
279+ # See https://grpc.github.io/grpc/python/grpc.html#grpc.composite_channel_credentials
280+ return grpc .composite_channel_credentials (
281+ ssl_credentials , google_auth_credentials
282+ )
283+ else :
284+ # Use grpc.compute_engine_channel_credentials in order to support Direct Path.
285+ # See https://grpc.github.io/grpc/python/grpc.html#grpc.compute_engine_channel_credentials
276286
277- # Combine the ssl credentials and the authorization credentials.
278- return grpc .composite_channel_credentials (ssl_credentials , google_auth_credentials )
287+ # TODO(<insert bug to github issue>): Although `grpc.compute_engine_channel_credentials`
288+ # returns channel credentials outside of GCE, we should determine if there is a way to
289+ # reliably detect when the client is in a GCE environment so that
290+ # `grpc.compute_engine_channel_credentials` is not called outside of GCE.
291+ return grpc .compute_engine_channel_credentials (google_auth_credentials )
279292
280293
281294def create_channel (
@@ -288,6 +301,7 @@ def create_channel(
288301 default_scopes = None ,
289302 default_host = None ,
290303 compression = None ,
304+ attempt_direct_path : Optional [bool ] = None ,
291305 ** kwargs ,
292306):
293307 """Create a secure channel with credentials.
@@ -311,6 +325,16 @@ def create_channel(
311325 default_host (str): The default endpoint. e.g., "pubsub.googleapis.com".
312326 compression (grpc.Compression): An optional value indicating the
313327 compression method to be used over the lifetime of the channel.
328+ attempt_direct_path (Optional[bool]): If set, Direct Path will be attempted when
329+ the request is made. Direct Path provides a proxyless connection which
330+ increases the available throughput, reduces latency, and increases
331+ reliability. Outside of GCE, the direct path request may fallback
332+ to DNS if this is configured by the Service. This argument should only
333+ be set in a GCE environment and for Services that are known to support Direct Path.
334+ If a `ServiceUnavailable` response is received when the request is sent, it is
335+ recommended that the client repeat the request with `attempt_direct_path` set to `False`
336+ as the Service may not support Direct Path. Using `ssl_credentials` with `attempt_direct_path`
337+ set to `True` will result in `ValueError` as it is not yet supported.
314338 kwargs: Additional key-word args passed to
315339 :func:`grpc_gcp.secure_channel` or :func:`grpc.secure_channel`.
316340 Note: `grpc_gcp` is only supported in environments with protobuf < 4.0.0.
@@ -320,8 +344,15 @@ def create_channel(
320344
321345 Raises:
322346 google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed.
347+ ValueError: If `ssl_credentials` is set and `attempt_direct_path` is set to `True`.
323348 """
324349
350+ # If `ssl_credentials` is set and `attempt_direct_path` is set to `True`,
351+ # raise ValueError as this is not yet supported.
352+ # TODO(<insert bug to github issue>): Add link to Github Issue
353+ if ssl_credentials is not None and attempt_direct_path :
354+ raise ValueError ("Using ssl_credentials with Direct Path is not supported" )
355+
325356 composite_credentials = _create_composite_credentials (
326357 credentials = credentials ,
327358 credentials_file = credentials_file ,
@@ -332,17 +363,53 @@ def create_channel(
332363 default_host = default_host ,
333364 )
334365
366+ # Note that grpcio-gcp is deprecated
335367 if HAS_GRPC_GCP : # pragma: NO COVER
336368 if compression is not None and compression != grpc .Compression .NoCompression :
337369 _LOGGER .debug (
338370 "Compression argument is being ignored for grpc_gcp.secure_channel creation."
339371 )
372+ if attempt_direct_path :
373+ warnings .warn (
374+ """The `attempt_direct_path` argument is ignored for grpc_gcp.secure_channel creation.""" ,
375+ DeprecationWarning ,
376+ )
340377 return grpc_gcp .secure_channel (target , composite_credentials , ** kwargs )
378+
379+ if attempt_direct_path :
380+ target = _modify_target_for_direct_path (target )
381+
341382 return grpc .secure_channel (
342383 target , composite_credentials , compression = compression , ** kwargs
343384 )
344385
345386
387+ def _modify_target_for_direct_path (target : str ) -> str :
388+ """Create a secure channel with credentials.
389+
390+ Args:
391+ target (str): The target service address in the format 'hostname:port', 'dns://hostname' or other
392+ compatible format.
393+
394+ Returns:
395+ target (str): The target service address which is converted into a format compatible with Direct Path.
396+ If the target contains `dns:///` or does not have contain `:///`, the target will be converted in
397+ a format compatible with Direct Path, otherwise the original target will be returned.
398+ """
399+
400+ dns_prefix = "dns:///"
401+ # Remove "dns:///" if `attempt_direct_path` is set to True as
402+ # the Direct Path prefix `google-c2p:///` will be used instead.
403+ target = target .replace (dns_prefix , "" )
404+
405+ direct_path_prefix = ":///"
406+ if direct_path_prefix not in target :
407+ target_without_port = target .split (":" )[0 ]
408+ # Modify the target to use Direct Path by adding the `google-c2p:///` prefix
409+ target = f"google-c2p{ direct_path_prefix } { target_without_port } "
410+ return target
411+
412+
346413_MethodCall = collections .namedtuple (
347414 "_MethodCall" , ("request" , "timeout" , "metadata" , "credentials" , "compression" )
348415)
0 commit comments