diff --git a/docs/conf.py b/docs/conf.py index d70a658b..b7057ab2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -38,6 +38,7 @@ "sphinx.ext.napoleon", "sphinx.ext.todo", "sphinx.ext.viewcode", + "recommonmark", ] # autodoc/autosummary flags @@ -49,10 +50,6 @@ # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] -# Allow markdown includes (so releases.md can include CHANGLEOG.md) -# http://www.sphinx-doc.org/en/master/markdown.html -source_parsers = {".md": "recommonmark.parser.CommonMarkParser"} - # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] diff --git a/google/cloud/gaming_v1/services/game_server_clusters_service/client.py b/google/cloud/gaming_v1/services/game_server_clusters_service/client.py index 7a4b15e8..73dea99c 100644 --- a/google/cloud/gaming_v1/services/game_server_clusters_service/client.py +++ b/google/cloud/gaming_v1/services/game_server_clusters_service/client.py @@ -16,7 +16,8 @@ # from collections import OrderedDict -from typing import Dict, Sequence, Tuple, Type, Union +import re +from typing import Callable, Dict, Sequence, Tuple, Type, Union import pkg_resources import google.api_core.client_options as ClientOptions # type: ignore @@ -76,8 +77,38 @@ class GameServerClustersServiceClient(metaclass=GameServerClustersServiceClientM Agones and is used to manage fleets within clusters. """ - DEFAULT_OPTIONS = ClientOptions.ClientOptions( - api_endpoint="gameservices.googleapis.com" + @staticmethod + def _get_default_mtls_endpoint(api_endpoint): + """Convert api endpoint to mTLS endpoint. + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + str: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + + DEFAULT_ENDPOINT = "gameservices.googleapis.com" + DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_ENDPOINT ) @classmethod @@ -109,12 +140,21 @@ def game_server_cluster_path( project=project, location=location, realm=realm, cluster=cluster ) + @staticmethod + def parse_game_server_cluster_path(path: str) -> Dict[str, str]: + """Parse a game_server_cluster path into its component segments.""" + m = re.match( + r"^projects/(?P.+?)/locations/(?P.+?)/realms/(?P.+?)/gameServerClusters/(?P.+?)$", + path, + ) + return m.groupdict() if m else {} + def __init__( self, *, credentials: credentials.Credentials = None, transport: Union[str, GameServerClustersServiceTransport] = None, - client_options: ClientOptions = DEFAULT_OPTIONS, + client_options: ClientOptions = None, ) -> None: """Instantiate the game server clusters service client. @@ -128,6 +168,17 @@ def __init__( transport to use. If set to None, a transport is chosen automatically. client_options (ClientOptions): Custom options for the client. + (1) The ``api_endpoint`` property can be used to override the + default endpoint provided by the client. + (2) If ``transport`` argument is None, ``client_options`` can be + used to create a mutual TLS transport. If ``client_cert_source`` + is provided, mutual TLS transport will be created with the given + ``api_endpoint`` or the default mTLS endpoint, and the client + SSL credentials obtained from ``client_cert_source``. + + Raises: + google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport + creation failed for any reason. """ if isinstance(client_options, dict): client_options = ClientOptions.from_dict(client_options) @@ -136,17 +187,46 @@ def __init__( # Ordinarily, we provide the transport, but allowing a custom transport # instance provides an extensibility point for unusual situations. if isinstance(transport, GameServerClustersServiceTransport): + # transport is a GameServerClustersServiceTransport instance. if credentials: raise ValueError( "When providing a transport instance, " "provide its credentials directly." ) self._transport = transport - else: + elif client_options is None or ( + client_options.api_endpoint is None + and client_options.client_cert_source is None + ): + # Don't trigger mTLS if we get an empty ClientOptions. Transport = type(self).get_transport_class(transport) self._transport = Transport( + credentials=credentials, host=self.DEFAULT_ENDPOINT + ) + else: + # We have a non-empty ClientOptions. If client_cert_source is + # provided, trigger mTLS with user provided endpoint or the default + # mTLS endpoint. + if client_options.client_cert_source: + api_mtls_endpoint = ( + client_options.api_endpoint + if client_options.api_endpoint + else self.DEFAULT_MTLS_ENDPOINT + ) + else: + api_mtls_endpoint = None + + api_endpoint = ( + client_options.api_endpoint + if client_options.api_endpoint + else self.DEFAULT_ENDPOINT + ) + + self._transport = GameServerClustersServiceGrpcTransport( credentials=credentials, - host=client_options.api_endpoint or "gameservices.googleapis.com", + host=api_endpoint, + api_mtls_endpoint=api_mtls_endpoint, + client_cert_source=client_options.client_cert_source, ) def list_game_server_clusters( diff --git a/google/cloud/gaming_v1/services/game_server_clusters_service/transports/grpc.py b/google/cloud/gaming_v1/services/game_server_clusters_service/transports/grpc.py index e68da218..5b9ab2a9 100644 --- a/google/cloud/gaming_v1/services/game_server_clusters_service/transports/grpc.py +++ b/google/cloud/gaming_v1/services/game_server_clusters_service/transports/grpc.py @@ -15,11 +15,13 @@ # limitations under the License. # -from typing import Callable, Dict +from typing import Callable, Dict, Tuple from google.api_core import grpc_helpers # type: ignore from google.api_core import operations_v1 # type: ignore from google.auth import credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore + import grpc # type: ignore @@ -48,7 +50,9 @@ def __init__( *, host: str = "gameservices.googleapis.com", credentials: credentials.Credentials = None, - channel: grpc.Channel = None + channel: grpc.Channel = None, + api_mtls_endpoint: str = None, + client_cert_source: Callable[[], Tuple[bytes, bytes]] = None ) -> None: """Instantiate the transport. @@ -62,20 +66,55 @@ def __init__( This argument is ignored if ``channel`` is provided. channel (Optional[grpc.Channel]): A ``Channel`` instance through which to make calls. + api_mtls_endpoint (Optional[str]): The mutual TLS endpoint. If + provided, it overrides the ``host`` argument and tries to create + a mutual TLS channel with client SSL credentials from + ``client_cert_source`` or applicatin default SSL credentials. + client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): A + callback to provide client SSL certificate bytes and private key + bytes, both in PEM format. It is ignored if ``api_mtls_endpoint`` + is None. + + Raises: + google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport + creation failed for any reason. """ - # Sanity check: Ensure that channel and credentials are not both - # provided. if channel: + # Sanity check: Ensure that channel and credentials are not both + # provided. credentials = False + # If a channel was explicitly provided, set it. + self._grpc_channel = channel + elif api_mtls_endpoint: + host = ( + api_mtls_endpoint + if ":" in api_mtls_endpoint + else api_mtls_endpoint + ":443" + ) + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + ssl_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + else: + ssl_credentials = SslCredentials().ssl_credentials + + # create a new channel. The provided one is ignored. + self._grpc_channel = grpc_helpers.create_channel( + host, + credentials=credentials, + ssl_credentials=ssl_credentials, + scopes=self.AUTH_SCOPES, + ) + # Run the base constructor. super().__init__(host=host, credentials=credentials) self._stubs = {} # type: Dict[str, Callable] - # If a channel was explicitly provided, set it. - if channel: - self._grpc_channel = channel - @classmethod def create_channel( cls, diff --git a/google/cloud/gaming_v1/services/game_server_configs_service/client.py b/google/cloud/gaming_v1/services/game_server_configs_service/client.py index 0241b2fe..92c31ef5 100644 --- a/google/cloud/gaming_v1/services/game_server_configs_service/client.py +++ b/google/cloud/gaming_v1/services/game_server_configs_service/client.py @@ -16,7 +16,8 @@ # from collections import OrderedDict -from typing import Dict, Sequence, Tuple, Type, Union +import re +from typing import Callable, Dict, Sequence, Tuple, Type, Union import pkg_resources import google.api_core.client_options as ClientOptions # type: ignore @@ -75,8 +76,38 @@ class GameServerConfigsServiceClient(metaclass=GameServerConfigsServiceClientMet Agones fleet. """ - DEFAULT_OPTIONS = ClientOptions.ClientOptions( - api_endpoint="gameservices.googleapis.com" + @staticmethod + def _get_default_mtls_endpoint(api_endpoint): + """Convert api endpoint to mTLS endpoint. + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + str: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + + DEFAULT_ENDPOINT = "gameservices.googleapis.com" + DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_ENDPOINT ) @classmethod @@ -108,12 +139,21 @@ def game_server_config_path( project=project, location=location, deployment=deployment, config=config ) + @staticmethod + def parse_game_server_config_path(path: str) -> Dict[str, str]: + """Parse a game_server_config path into its component segments.""" + m = re.match( + r"^projects/(?P.+?)/locations/(?P.+?)/gameServerDeployments/(?P.+?)/configs/(?P.+?)$", + path, + ) + return m.groupdict() if m else {} + def __init__( self, *, credentials: credentials.Credentials = None, transport: Union[str, GameServerConfigsServiceTransport] = None, - client_options: ClientOptions = DEFAULT_OPTIONS, + client_options: ClientOptions = None, ) -> None: """Instantiate the game server configs service client. @@ -127,6 +167,17 @@ def __init__( transport to use. If set to None, a transport is chosen automatically. client_options (ClientOptions): Custom options for the client. + (1) The ``api_endpoint`` property can be used to override the + default endpoint provided by the client. + (2) If ``transport`` argument is None, ``client_options`` can be + used to create a mutual TLS transport. If ``client_cert_source`` + is provided, mutual TLS transport will be created with the given + ``api_endpoint`` or the default mTLS endpoint, and the client + SSL credentials obtained from ``client_cert_source``. + + Raises: + google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport + creation failed for any reason. """ if isinstance(client_options, dict): client_options = ClientOptions.from_dict(client_options) @@ -135,17 +186,46 @@ def __init__( # Ordinarily, we provide the transport, but allowing a custom transport # instance provides an extensibility point for unusual situations. if isinstance(transport, GameServerConfigsServiceTransport): + # transport is a GameServerConfigsServiceTransport instance. if credentials: raise ValueError( "When providing a transport instance, " "provide its credentials directly." ) self._transport = transport - else: + elif client_options is None or ( + client_options.api_endpoint is None + and client_options.client_cert_source is None + ): + # Don't trigger mTLS if we get an empty ClientOptions. Transport = type(self).get_transport_class(transport) self._transport = Transport( + credentials=credentials, host=self.DEFAULT_ENDPOINT + ) + else: + # We have a non-empty ClientOptions. If client_cert_source is + # provided, trigger mTLS with user provided endpoint or the default + # mTLS endpoint. + if client_options.client_cert_source: + api_mtls_endpoint = ( + client_options.api_endpoint + if client_options.api_endpoint + else self.DEFAULT_MTLS_ENDPOINT + ) + else: + api_mtls_endpoint = None + + api_endpoint = ( + client_options.api_endpoint + if client_options.api_endpoint + else self.DEFAULT_ENDPOINT + ) + + self._transport = GameServerConfigsServiceGrpcTransport( credentials=credentials, - host=client_options.api_endpoint or "gameservices.googleapis.com", + host=api_endpoint, + api_mtls_endpoint=api_mtls_endpoint, + client_cert_source=client_options.client_cert_source, ) def list_game_server_configs( diff --git a/google/cloud/gaming_v1/services/game_server_configs_service/transports/grpc.py b/google/cloud/gaming_v1/services/game_server_configs_service/transports/grpc.py index 32c676b9..7caf2d34 100644 --- a/google/cloud/gaming_v1/services/game_server_configs_service/transports/grpc.py +++ b/google/cloud/gaming_v1/services/game_server_configs_service/transports/grpc.py @@ -15,11 +15,13 @@ # limitations under the License. # -from typing import Callable, Dict +from typing import Callable, Dict, Tuple from google.api_core import grpc_helpers # type: ignore from google.api_core import operations_v1 # type: ignore from google.auth import credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore + import grpc # type: ignore @@ -48,7 +50,9 @@ def __init__( *, host: str = "gameservices.googleapis.com", credentials: credentials.Credentials = None, - channel: grpc.Channel = None + channel: grpc.Channel = None, + api_mtls_endpoint: str = None, + client_cert_source: Callable[[], Tuple[bytes, bytes]] = None ) -> None: """Instantiate the transport. @@ -62,20 +66,55 @@ def __init__( This argument is ignored if ``channel`` is provided. channel (Optional[grpc.Channel]): A ``Channel`` instance through which to make calls. + api_mtls_endpoint (Optional[str]): The mutual TLS endpoint. If + provided, it overrides the ``host`` argument and tries to create + a mutual TLS channel with client SSL credentials from + ``client_cert_source`` or applicatin default SSL credentials. + client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): A + callback to provide client SSL certificate bytes and private key + bytes, both in PEM format. It is ignored if ``api_mtls_endpoint`` + is None. + + Raises: + google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport + creation failed for any reason. """ - # Sanity check: Ensure that channel and credentials are not both - # provided. if channel: + # Sanity check: Ensure that channel and credentials are not both + # provided. credentials = False + # If a channel was explicitly provided, set it. + self._grpc_channel = channel + elif api_mtls_endpoint: + host = ( + api_mtls_endpoint + if ":" in api_mtls_endpoint + else api_mtls_endpoint + ":443" + ) + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + ssl_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + else: + ssl_credentials = SslCredentials().ssl_credentials + + # create a new channel. The provided one is ignored. + self._grpc_channel = grpc_helpers.create_channel( + host, + credentials=credentials, + ssl_credentials=ssl_credentials, + scopes=self.AUTH_SCOPES, + ) + # Run the base constructor. super().__init__(host=host, credentials=credentials) self._stubs = {} # type: Dict[str, Callable] - # If a channel was explicitly provided, set it. - if channel: - self._grpc_channel = channel - @classmethod def create_channel( cls, diff --git a/google/cloud/gaming_v1/services/game_server_deployments_service/client.py b/google/cloud/gaming_v1/services/game_server_deployments_service/client.py index 1b033a76..3419bb0f 100644 --- a/google/cloud/gaming_v1/services/game_server_deployments_service/client.py +++ b/google/cloud/gaming_v1/services/game_server_deployments_service/client.py @@ -16,7 +16,8 @@ # from collections import OrderedDict -from typing import Dict, Sequence, Tuple, Type, Union +import re +from typing import Callable, Dict, Sequence, Tuple, Type, Union import pkg_resources import google.api_core.client_options as ClientOptions # type: ignore @@ -78,8 +79,38 @@ class GameServerDeploymentsServiceClient( of Agones fleets. """ - DEFAULT_OPTIONS = ClientOptions.ClientOptions( - api_endpoint="gameservices.googleapis.com" + @staticmethod + def _get_default_mtls_endpoint(api_endpoint): + """Convert api endpoint to mTLS endpoint. + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + str: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + + DEFAULT_ENDPOINT = "gameservices.googleapis.com" + DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_ENDPOINT ) @classmethod @@ -103,29 +134,47 @@ def from_service_account_file(cls, filename: str, *args, **kwargs): from_service_account_json = from_service_account_file @staticmethod - def game_server_deployment_path( + def game_server_deployment_rollout_path( project: str, location: str, deployment: str ) -> str: - """Return a fully-qualified game_server_deployment string.""" - return "projects/{project}/locations/{location}/gameServerDeployments/{deployment}".format( + """Return a fully-qualified game_server_deployment_rollout string.""" + return "projects/{project}/locations/{location}/gameServerDeployments/{deployment}/rollout".format( project=project, location=location, deployment=deployment ) @staticmethod - def game_server_deployment_rollout_path( + def parse_game_server_deployment_rollout_path(path: str) -> Dict[str, str]: + """Parse a game_server_deployment_rollout path into its component segments.""" + m = re.match( + r"^projects/(?P.+?)/locations/(?P.+?)/gameServerDeployments/(?P.+?)/rollout$", + path, + ) + return m.groupdict() if m else {} + + @staticmethod + def game_server_deployment_path( project: str, location: str, deployment: str ) -> str: - """Return a fully-qualified game_server_deployment_rollout string.""" - return "projects/{project}/locations/{location}/gameServerDeployments/{deployment}/rollout".format( + """Return a fully-qualified game_server_deployment string.""" + return "projects/{project}/locations/{location}/gameServerDeployments/{deployment}".format( project=project, location=location, deployment=deployment ) + @staticmethod + def parse_game_server_deployment_path(path: str) -> Dict[str, str]: + """Parse a game_server_deployment path into its component segments.""" + m = re.match( + r"^projects/(?P.+?)/locations/(?P.+?)/gameServerDeployments/(?P.+?)$", + path, + ) + return m.groupdict() if m else {} + def __init__( self, *, credentials: credentials.Credentials = None, transport: Union[str, GameServerDeploymentsServiceTransport] = None, - client_options: ClientOptions = DEFAULT_OPTIONS, + client_options: ClientOptions = None, ) -> None: """Instantiate the game server deployments service client. @@ -139,6 +188,17 @@ def __init__( transport to use. If set to None, a transport is chosen automatically. client_options (ClientOptions): Custom options for the client. + (1) The ``api_endpoint`` property can be used to override the + default endpoint provided by the client. + (2) If ``transport`` argument is None, ``client_options`` can be + used to create a mutual TLS transport. If ``client_cert_source`` + is provided, mutual TLS transport will be created with the given + ``api_endpoint`` or the default mTLS endpoint, and the client + SSL credentials obtained from ``client_cert_source``. + + Raises: + google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport + creation failed for any reason. """ if isinstance(client_options, dict): client_options = ClientOptions.from_dict(client_options) @@ -147,17 +207,46 @@ def __init__( # Ordinarily, we provide the transport, but allowing a custom transport # instance provides an extensibility point for unusual situations. if isinstance(transport, GameServerDeploymentsServiceTransport): + # transport is a GameServerDeploymentsServiceTransport instance. if credentials: raise ValueError( "When providing a transport instance, " "provide its credentials directly." ) self._transport = transport - else: + elif client_options is None or ( + client_options.api_endpoint is None + and client_options.client_cert_source is None + ): + # Don't trigger mTLS if we get an empty ClientOptions. Transport = type(self).get_transport_class(transport) self._transport = Transport( + credentials=credentials, host=self.DEFAULT_ENDPOINT + ) + else: + # We have a non-empty ClientOptions. If client_cert_source is + # provided, trigger mTLS with user provided endpoint or the default + # mTLS endpoint. + if client_options.client_cert_source: + api_mtls_endpoint = ( + client_options.api_endpoint + if client_options.api_endpoint + else self.DEFAULT_MTLS_ENDPOINT + ) + else: + api_mtls_endpoint = None + + api_endpoint = ( + client_options.api_endpoint + if client_options.api_endpoint + else self.DEFAULT_ENDPOINT + ) + + self._transport = GameServerDeploymentsServiceGrpcTransport( credentials=credentials, - host=client_options.api_endpoint or "gameservices.googleapis.com", + host=api_endpoint, + api_mtls_endpoint=api_mtls_endpoint, + client_cert_source=client_options.client_cert_source, ) def list_game_server_deployments( diff --git a/google/cloud/gaming_v1/services/game_server_deployments_service/transports/grpc.py b/google/cloud/gaming_v1/services/game_server_deployments_service/transports/grpc.py index e64c8b65..2ea472d8 100644 --- a/google/cloud/gaming_v1/services/game_server_deployments_service/transports/grpc.py +++ b/google/cloud/gaming_v1/services/game_server_deployments_service/transports/grpc.py @@ -15,11 +15,13 @@ # limitations under the License. # -from typing import Callable, Dict +from typing import Callable, Dict, Tuple from google.api_core import grpc_helpers # type: ignore from google.api_core import operations_v1 # type: ignore from google.auth import credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore + import grpc # type: ignore @@ -48,7 +50,9 @@ def __init__( *, host: str = "gameservices.googleapis.com", credentials: credentials.Credentials = None, - channel: grpc.Channel = None + channel: grpc.Channel = None, + api_mtls_endpoint: str = None, + client_cert_source: Callable[[], Tuple[bytes, bytes]] = None ) -> None: """Instantiate the transport. @@ -62,20 +66,55 @@ def __init__( This argument is ignored if ``channel`` is provided. channel (Optional[grpc.Channel]): A ``Channel`` instance through which to make calls. + api_mtls_endpoint (Optional[str]): The mutual TLS endpoint. If + provided, it overrides the ``host`` argument and tries to create + a mutual TLS channel with client SSL credentials from + ``client_cert_source`` or applicatin default SSL credentials. + client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): A + callback to provide client SSL certificate bytes and private key + bytes, both in PEM format. It is ignored if ``api_mtls_endpoint`` + is None. + + Raises: + google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport + creation failed for any reason. """ - # Sanity check: Ensure that channel and credentials are not both - # provided. if channel: + # Sanity check: Ensure that channel and credentials are not both + # provided. credentials = False + # If a channel was explicitly provided, set it. + self._grpc_channel = channel + elif api_mtls_endpoint: + host = ( + api_mtls_endpoint + if ":" in api_mtls_endpoint + else api_mtls_endpoint + ":443" + ) + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + ssl_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + else: + ssl_credentials = SslCredentials().ssl_credentials + + # create a new channel. The provided one is ignored. + self._grpc_channel = grpc_helpers.create_channel( + host, + credentials=credentials, + ssl_credentials=ssl_credentials, + scopes=self.AUTH_SCOPES, + ) + # Run the base constructor. super().__init__(host=host, credentials=credentials) self._stubs = {} # type: Dict[str, Callable] - # If a channel was explicitly provided, set it. - if channel: - self._grpc_channel = channel - @classmethod def create_channel( cls, diff --git a/google/cloud/gaming_v1/services/realms_service/client.py b/google/cloud/gaming_v1/services/realms_service/client.py index 036270f0..17bc445d 100644 --- a/google/cloud/gaming_v1/services/realms_service/client.py +++ b/google/cloud/gaming_v1/services/realms_service/client.py @@ -16,7 +16,8 @@ # from collections import OrderedDict -from typing import Dict, Sequence, Tuple, Type, Union +import re +from typing import Callable, Dict, Sequence, Tuple, Type, Union import pkg_resources import google.api_core.client_options as ClientOptions # type: ignore @@ -72,8 +73,38 @@ class RealmsServiceClient(metaclass=RealmsServiceClientMeta): considered interchangeable. """ - DEFAULT_OPTIONS = ClientOptions.ClientOptions( - api_endpoint="gameservices.googleapis.com" + @staticmethod + def _get_default_mtls_endpoint(api_endpoint): + """Convert api endpoint to mTLS endpoint. + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + str: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + + DEFAULT_ENDPOINT = "gameservices.googleapis.com" + DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_ENDPOINT ) @classmethod @@ -103,12 +134,21 @@ def realm_path(project: str, location: str, realm: str) -> str: project=project, location=location, realm=realm ) + @staticmethod + def parse_realm_path(path: str) -> Dict[str, str]: + """Parse a realm path into its component segments.""" + m = re.match( + r"^projects/(?P.+?)/locations/(?P.+?)/realms/(?P.+?)$", + path, + ) + return m.groupdict() if m else {} + def __init__( self, *, credentials: credentials.Credentials = None, transport: Union[str, RealmsServiceTransport] = None, - client_options: ClientOptions = DEFAULT_OPTIONS, + client_options: ClientOptions = None, ) -> None: """Instantiate the realms service client. @@ -122,6 +162,17 @@ def __init__( transport to use. If set to None, a transport is chosen automatically. client_options (ClientOptions): Custom options for the client. + (1) The ``api_endpoint`` property can be used to override the + default endpoint provided by the client. + (2) If ``transport`` argument is None, ``client_options`` can be + used to create a mutual TLS transport. If ``client_cert_source`` + is provided, mutual TLS transport will be created with the given + ``api_endpoint`` or the default mTLS endpoint, and the client + SSL credentials obtained from ``client_cert_source``. + + Raises: + google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport + creation failed for any reason. """ if isinstance(client_options, dict): client_options = ClientOptions.from_dict(client_options) @@ -130,17 +181,46 @@ def __init__( # Ordinarily, we provide the transport, but allowing a custom transport # instance provides an extensibility point for unusual situations. if isinstance(transport, RealmsServiceTransport): + # transport is a RealmsServiceTransport instance. if credentials: raise ValueError( "When providing a transport instance, " "provide its credentials directly." ) self._transport = transport - else: + elif client_options is None or ( + client_options.api_endpoint is None + and client_options.client_cert_source is None + ): + # Don't trigger mTLS if we get an empty ClientOptions. Transport = type(self).get_transport_class(transport) self._transport = Transport( + credentials=credentials, host=self.DEFAULT_ENDPOINT + ) + else: + # We have a non-empty ClientOptions. If client_cert_source is + # provided, trigger mTLS with user provided endpoint or the default + # mTLS endpoint. + if client_options.client_cert_source: + api_mtls_endpoint = ( + client_options.api_endpoint + if client_options.api_endpoint + else self.DEFAULT_MTLS_ENDPOINT + ) + else: + api_mtls_endpoint = None + + api_endpoint = ( + client_options.api_endpoint + if client_options.api_endpoint + else self.DEFAULT_ENDPOINT + ) + + self._transport = RealmsServiceGrpcTransport( credentials=credentials, - host=client_options.api_endpoint or "gameservices.googleapis.com", + host=api_endpoint, + api_mtls_endpoint=api_mtls_endpoint, + client_cert_source=client_options.client_cert_source, ) def list_realms( diff --git a/google/cloud/gaming_v1/services/realms_service/transports/grpc.py b/google/cloud/gaming_v1/services/realms_service/transports/grpc.py index ae783002..2774b4bd 100644 --- a/google/cloud/gaming_v1/services/realms_service/transports/grpc.py +++ b/google/cloud/gaming_v1/services/realms_service/transports/grpc.py @@ -15,11 +15,13 @@ # limitations under the License. # -from typing import Callable, Dict +from typing import Callable, Dict, Tuple from google.api_core import grpc_helpers # type: ignore from google.api_core import operations_v1 # type: ignore from google.auth import credentials # type: ignore +from google.auth.transport.grpc import SslCredentials # type: ignore + import grpc # type: ignore @@ -48,7 +50,9 @@ def __init__( *, host: str = "gameservices.googleapis.com", credentials: credentials.Credentials = None, - channel: grpc.Channel = None + channel: grpc.Channel = None, + api_mtls_endpoint: str = None, + client_cert_source: Callable[[], Tuple[bytes, bytes]] = None ) -> None: """Instantiate the transport. @@ -62,20 +66,55 @@ def __init__( This argument is ignored if ``channel`` is provided. channel (Optional[grpc.Channel]): A ``Channel`` instance through which to make calls. + api_mtls_endpoint (Optional[str]): The mutual TLS endpoint. If + provided, it overrides the ``host`` argument and tries to create + a mutual TLS channel with client SSL credentials from + ``client_cert_source`` or applicatin default SSL credentials. + client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): A + callback to provide client SSL certificate bytes and private key + bytes, both in PEM format. It is ignored if ``api_mtls_endpoint`` + is None. + + Raises: + google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport + creation failed for any reason. """ - # Sanity check: Ensure that channel and credentials are not both - # provided. if channel: + # Sanity check: Ensure that channel and credentials are not both + # provided. credentials = False + # If a channel was explicitly provided, set it. + self._grpc_channel = channel + elif api_mtls_endpoint: + host = ( + api_mtls_endpoint + if ":" in api_mtls_endpoint + else api_mtls_endpoint + ":443" + ) + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + ssl_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + else: + ssl_credentials = SslCredentials().ssl_credentials + + # create a new channel. The provided one is ignored. + self._grpc_channel = grpc_helpers.create_channel( + host, + credentials=credentials, + ssl_credentials=ssl_credentials, + scopes=self.AUTH_SCOPES, + ) + # Run the base constructor. super().__init__(host=host, credentials=credentials) self._stubs = {} # type: Dict[str, Callable] - # If a channel was explicitly provided, set it. - if channel: - self._grpc_channel = channel - @classmethod def create_channel( cls, diff --git a/mypy.ini b/mypy.ini index f23e6b53..4505b485 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,3 +1,3 @@ [mypy] -python_version = 3.5 +python_version = 3.6 namespace_packages = True diff --git a/synth.metadata b/synth.metadata index 8c16daaa..34de6caf 100644 --- a/synth.metadata +++ b/synth.metadata @@ -1,21 +1,25 @@ { - "updateTime": "2020-03-25T12:13:27.483737Z", "sources": [ + { + "git": { + "name": ".", + "remote": "https://github.com/googleapis/python-game-servers.git", + "sha": "bdadd8e6afa749a776ee9018b5cd64e543c69f53" + } + }, { "git": { "name": "googleapis", "remote": "https://github.com/googleapis/googleapis.git", "sha": "551cf1e6e3addcc63740427c4f9b40dedd3dac27", - "internalRef": "302792195", - "log": "551cf1e6e3addcc63740427c4f9b40dedd3dac27\nfeat: Add OS Config AgentEndpointService v1 PatchJobs and Tasks APIs.\n\nPiperOrigin-RevId: 302792195\n\n1df117114c73299b614dfd3ba3632bf246669336\nSynchronize new proto/yaml changes.\n\nPiperOrigin-RevId: 302753982\n\n71d6c56a14bb433beb1237dccb48dabcd9597924\nRefresh monitoring client libraries.\nRename to Cloud Monitoring API.\nAdded support for TimeSeriesQueryLanguageCondition condition type in alert policies.\n\nPiperOrigin-RevId: 302735422\n\n25a1781c096974df99d556cc5888fefa82bc6425\nbazel: migrate all go_gapic_library targets to microgenerator implementation\n\n* update rules_go and gazelle bazel dependencies\n* update gapic-generator bazel dependency (with build file generator changes)\n\nPiperOrigin-RevId: 302730217\n\n" + "internalRef": "302792195" } }, { "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "ee4330a0e5f4b93978e8683fbda8e6d4148326b7", - "log": "ee4330a0e5f4b93978e8683fbda8e6d4148326b7\nchore(java_templates): mark version bumps of current library as a chore (#452)\n\nWith the samples/install-without-bom/pom.xml referencing the latest released library, we want to mark updates of this version as a chore for renovate bot.\na0d3133a5d45544a66345059eebf76933265c099\nfix(java): run mvn install with retry (#453)\n\n* fix(java): run mvn install with retry\n\n* fix invocation of command\n" + "sha": "ee4330a0e5f4b93978e8683fbda8e6d4148326b7" } } ], diff --git a/tests/unit/gaming_v1/test_game_server_clusters_service.py b/tests/unit/gaming_v1/test_game_server_clusters_service.py index 3602252e..a1cecd09 100644 --- a/tests/unit/gaming_v1/test_game_server_clusters_service.py +++ b/tests/unit/gaming_v1/test_game_server_clusters_service.py @@ -24,6 +24,7 @@ from google import auth from google.api_core import client_options from google.api_core import future +from google.api_core import grpc_helpers from google.api_core import operations_v1 from google.auth import credentials from google.cloud.gaming_v1.services.game_server_clusters_service import ( @@ -39,6 +40,42 @@ from google.protobuf import timestamp_pb2 as timestamp # type: ignore +def client_cert_source_callback(): + return b"cert bytes", b"key bytes" + + +def test__get_default_mtls_endpoint(): + api_endpoint = "example.googleapis.com" + api_mtls_endpoint = "example.mtls.googleapis.com" + sandbox_endpoint = "example.sandbox.googleapis.com" + sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" + non_googleapi = "api.example.com" + + assert GameServerClustersServiceClient._get_default_mtls_endpoint(None) is None + assert ( + GameServerClustersServiceClient._get_default_mtls_endpoint(api_endpoint) + == api_mtls_endpoint + ) + assert ( + GameServerClustersServiceClient._get_default_mtls_endpoint(api_mtls_endpoint) + == api_mtls_endpoint + ) + assert ( + GameServerClustersServiceClient._get_default_mtls_endpoint(sandbox_endpoint) + == sandbox_mtls_endpoint + ) + assert ( + GameServerClustersServiceClient._get_default_mtls_endpoint( + sandbox_mtls_endpoint + ) + == sandbox_mtls_endpoint + ) + assert ( + GameServerClustersServiceClient._get_default_mtls_endpoint(non_googleapi) + == non_googleapi + ) + + def test_game_server_clusters_service_client_from_service_account_file(): creds = credentials.AnonymousCredentials() with mock.patch.object( @@ -59,31 +96,89 @@ def test_game_server_clusters_service_client_from_service_account_file(): def test_game_server_clusters_service_client_client_options(): - # Check the default options have their expected values. - assert ( - GameServerClustersServiceClient.DEFAULT_OPTIONS.api_endpoint - == "gameservices.googleapis.com" - ) + # Check that if channel is provided we won't create a new one. + with mock.patch( + "google.cloud.gaming_v1.services.game_server_clusters_service.GameServerClustersServiceClient.get_transport_class" + ) as gtc: + transport = transports.GameServerClustersServiceGrpcTransport( + credentials=credentials.AnonymousCredentials() + ) + client = GameServerClustersServiceClient(transport=transport) + gtc.assert_not_called() - # Check that options can be customized. - options = client_options.ClientOptions(api_endpoint="squid.clam.whelk") + # Check mTLS is not triggered with empty client options. + options = client_options.ClientOptions() with mock.patch( "google.cloud.gaming_v1.services.game_server_clusters_service.GameServerClustersServiceClient.get_transport_class" ) as gtc: transport = gtc.return_value = mock.MagicMock() client = GameServerClustersServiceClient(client_options=options) - transport.assert_called_once_with(credentials=None, host="squid.clam.whelk") + transport.assert_called_once_with( + credentials=None, host=client.DEFAULT_ENDPOINT + ) + + # Check mTLS is not triggered if api_endpoint is provided but + # client_cert_source is None. + options = client_options.ClientOptions(api_endpoint="squid.clam.whelk") + with mock.patch( + "google.cloud.gaming_v1.services.game_server_clusters_service.transports.GameServerClustersServiceGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None + client = GameServerClustersServiceClient(client_options=options) + grpc_transport.assert_called_once_with( + api_mtls_endpoint=None, + client_cert_source=None, + credentials=None, + host="squid.clam.whelk", + ) + + # Check mTLS is triggered if client_cert_source is provided. + options = client_options.ClientOptions( + client_cert_source=client_cert_source_callback + ) + with mock.patch( + "google.cloud.gaming_v1.services.game_server_clusters_service.transports.GameServerClustersServiceGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None + client = GameServerClustersServiceClient(client_options=options) + grpc_transport.assert_called_once_with( + api_mtls_endpoint=client.DEFAULT_MTLS_ENDPOINT, + client_cert_source=client_cert_source_callback, + credentials=None, + host=client.DEFAULT_ENDPOINT, + ) + + # Check mTLS is triggered if api_endpoint and client_cert_source are provided. + options = client_options.ClientOptions( + api_endpoint="squid.clam.whelk", client_cert_source=client_cert_source_callback + ) + with mock.patch( + "google.cloud.gaming_v1.services.game_server_clusters_service.transports.GameServerClustersServiceGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None + client = GameServerClustersServiceClient(client_options=options) + grpc_transport.assert_called_once_with( + api_mtls_endpoint="squid.clam.whelk", + client_cert_source=client_cert_source_callback, + credentials=None, + host="squid.clam.whelk", + ) def test_game_server_clusters_service_client_client_options_from_dict(): with mock.patch( - "google.cloud.gaming_v1.services.game_server_clusters_service.GameServerClustersServiceClient.get_transport_class" - ) as gtc: - transport = gtc.return_value = mock.MagicMock() + "google.cloud.gaming_v1.services.game_server_clusters_service.transports.GameServerClustersServiceGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None client = GameServerClustersServiceClient( client_options={"api_endpoint": "squid.clam.whelk"} ) - transport.assert_called_once_with(credentials=None, host="squid.clam.whelk") + grpc_transport.assert_called_once_with( + api_mtls_endpoint=None, + client_cert_source=None, + credentials=None, + host="squid.clam.whelk", + ) def test_list_game_server_clusters(transport: str = "grpc"): @@ -765,8 +860,87 @@ def test_game_server_clusters_service_host_with_port(): def test_game_server_clusters_service_grpc_transport_channel(): channel = grpc.insecure_channel("http://localhost/") - transport = transports.GameServerClustersServiceGrpcTransport(channel=channel) - assert transport.grpc_channel is channel + + # Check that if channel is provided, mtls endpoint and client_cert_source + # won't be used. + callback = mock.MagicMock() + transport = transports.GameServerClustersServiceGrpcTransport( + host="squid.clam.whelk", + channel=channel, + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=callback, + ) + assert transport.grpc_channel == channel + assert transport._host == "squid.clam.whelk:443" + assert not callback.called + + +@mock.patch("grpc.ssl_channel_credentials", autospec=True) +@mock.patch("google.api_core.grpc_helpers.create_channel", autospec=True) +def test_game_server_clusters_service_grpc_transport_channel_mtls_with_client_cert_source( + grpc_create_channel, grpc_ssl_channel_cred +): + # Check that if channel is None, but api_mtls_endpoint and client_cert_source + # are provided, then a mTLS channel will be created. + mock_cred = mock.Mock() + + mock_ssl_cred = mock.Mock() + grpc_ssl_channel_cred.return_value = mock_ssl_cred + + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + + transport = transports.GameServerClustersServiceGrpcTransport( + host="squid.clam.whelk", + credentials=mock_cred, + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=client_cert_source_callback, + ) + grpc_ssl_channel_cred.assert_called_once_with( + certificate_chain=b"cert bytes", private_key=b"key bytes" + ) + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=mock_cred, + ssl_credentials=mock_ssl_cred, + scopes=("https://www.googleapis.com/auth/cloud-platform",), + ) + assert transport.grpc_channel == mock_grpc_channel + + +@pytest.mark.parametrize( + "api_mtls_endpoint", ["mtls.squid.clam.whelk", "mtls.squid.clam.whelk:443"] +) +@mock.patch("google.api_core.grpc_helpers.create_channel", autospec=True) +def test_game_server_clusters_service_grpc_transport_channel_mtls_with_adc( + grpc_create_channel, api_mtls_endpoint +): + # Check that if channel and client_cert_source are None, but api_mtls_endpoint + # is provided, then a mTLS channel will be created with SSL ADC. + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + + # Mock google.auth.transport.grpc.SslCredentials class. + mock_ssl_cred = mock.Mock() + with mock.patch.multiple( + "google.auth.transport.grpc.SslCredentials", + __init__=mock.Mock(return_value=None), + ssl_credentials=mock.PropertyMock(return_value=mock_ssl_cred), + ): + mock_cred = mock.Mock() + transport = transports.GameServerClustersServiceGrpcTransport( + host="squid.clam.whelk", + credentials=mock_cred, + api_mtls_endpoint=api_mtls_endpoint, + client_cert_source=None, + ) + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=mock_cred, + ssl_credentials=mock_ssl_cred, + scopes=("https://www.googleapis.com/auth/cloud-platform",), + ) + assert transport.grpc_channel == mock_grpc_channel def test_game_server_clusters_service_grpc_lro_client(): @@ -795,3 +969,17 @@ def test_game_server_cluster_path(): project, location, realm, cluster ) assert expected == actual + + +def test_parse_game_server_cluster_path(): + expected = { + "project": "oyster", + "location": "nudibranch", + "realm": "cuttlefish", + "cluster": "mussel", + } + path = GameServerClustersServiceClient.game_server_cluster_path(**expected) + + # Check that the path construction is reversible. + actual = GameServerClustersServiceClient.parse_game_server_cluster_path(path) + assert expected == actual diff --git a/tests/unit/gaming_v1/test_game_server_configs_service.py b/tests/unit/gaming_v1/test_game_server_configs_service.py index 0f17d7ac..0561baf4 100644 --- a/tests/unit/gaming_v1/test_game_server_configs_service.py +++ b/tests/unit/gaming_v1/test_game_server_configs_service.py @@ -24,6 +24,7 @@ from google import auth from google.api_core import client_options from google.api_core import future +from google.api_core import grpc_helpers from google.api_core import operations_v1 from google.auth import credentials from google.cloud.gaming_v1.services.game_server_configs_service import ( @@ -39,6 +40,40 @@ from google.protobuf import timestamp_pb2 as timestamp # type: ignore +def client_cert_source_callback(): + return b"cert bytes", b"key bytes" + + +def test__get_default_mtls_endpoint(): + api_endpoint = "example.googleapis.com" + api_mtls_endpoint = "example.mtls.googleapis.com" + sandbox_endpoint = "example.sandbox.googleapis.com" + sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" + non_googleapi = "api.example.com" + + assert GameServerConfigsServiceClient._get_default_mtls_endpoint(None) is None + assert ( + GameServerConfigsServiceClient._get_default_mtls_endpoint(api_endpoint) + == api_mtls_endpoint + ) + assert ( + GameServerConfigsServiceClient._get_default_mtls_endpoint(api_mtls_endpoint) + == api_mtls_endpoint + ) + assert ( + GameServerConfigsServiceClient._get_default_mtls_endpoint(sandbox_endpoint) + == sandbox_mtls_endpoint + ) + assert ( + GameServerConfigsServiceClient._get_default_mtls_endpoint(sandbox_mtls_endpoint) + == sandbox_mtls_endpoint + ) + assert ( + GameServerConfigsServiceClient._get_default_mtls_endpoint(non_googleapi) + == non_googleapi + ) + + def test_game_server_configs_service_client_from_service_account_file(): creds = credentials.AnonymousCredentials() with mock.patch.object( @@ -59,31 +94,89 @@ def test_game_server_configs_service_client_from_service_account_file(): def test_game_server_configs_service_client_client_options(): - # Check the default options have their expected values. - assert ( - GameServerConfigsServiceClient.DEFAULT_OPTIONS.api_endpoint - == "gameservices.googleapis.com" - ) + # Check that if channel is provided we won't create a new one. + with mock.patch( + "google.cloud.gaming_v1.services.game_server_configs_service.GameServerConfigsServiceClient.get_transport_class" + ) as gtc: + transport = transports.GameServerConfigsServiceGrpcTransport( + credentials=credentials.AnonymousCredentials() + ) + client = GameServerConfigsServiceClient(transport=transport) + gtc.assert_not_called() - # Check that options can be customized. - options = client_options.ClientOptions(api_endpoint="squid.clam.whelk") + # Check mTLS is not triggered with empty client options. + options = client_options.ClientOptions() with mock.patch( "google.cloud.gaming_v1.services.game_server_configs_service.GameServerConfigsServiceClient.get_transport_class" ) as gtc: transport = gtc.return_value = mock.MagicMock() client = GameServerConfigsServiceClient(client_options=options) - transport.assert_called_once_with(credentials=None, host="squid.clam.whelk") + transport.assert_called_once_with( + credentials=None, host=client.DEFAULT_ENDPOINT + ) + + # Check mTLS is not triggered if api_endpoint is provided but + # client_cert_source is None. + options = client_options.ClientOptions(api_endpoint="squid.clam.whelk") + with mock.patch( + "google.cloud.gaming_v1.services.game_server_configs_service.transports.GameServerConfigsServiceGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None + client = GameServerConfigsServiceClient(client_options=options) + grpc_transport.assert_called_once_with( + api_mtls_endpoint=None, + client_cert_source=None, + credentials=None, + host="squid.clam.whelk", + ) + + # Check mTLS is triggered if client_cert_source is provided. + options = client_options.ClientOptions( + client_cert_source=client_cert_source_callback + ) + with mock.patch( + "google.cloud.gaming_v1.services.game_server_configs_service.transports.GameServerConfigsServiceGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None + client = GameServerConfigsServiceClient(client_options=options) + grpc_transport.assert_called_once_with( + api_mtls_endpoint=client.DEFAULT_MTLS_ENDPOINT, + client_cert_source=client_cert_source_callback, + credentials=None, + host=client.DEFAULT_ENDPOINT, + ) + + # Check mTLS is triggered if api_endpoint and client_cert_source are provided. + options = client_options.ClientOptions( + api_endpoint="squid.clam.whelk", client_cert_source=client_cert_source_callback + ) + with mock.patch( + "google.cloud.gaming_v1.services.game_server_configs_service.transports.GameServerConfigsServiceGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None + client = GameServerConfigsServiceClient(client_options=options) + grpc_transport.assert_called_once_with( + api_mtls_endpoint="squid.clam.whelk", + client_cert_source=client_cert_source_callback, + credentials=None, + host="squid.clam.whelk", + ) def test_game_server_configs_service_client_client_options_from_dict(): with mock.patch( - "google.cloud.gaming_v1.services.game_server_configs_service.GameServerConfigsServiceClient.get_transport_class" - ) as gtc: - transport = gtc.return_value = mock.MagicMock() + "google.cloud.gaming_v1.services.game_server_configs_service.transports.GameServerConfigsServiceGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None client = GameServerConfigsServiceClient( client_options={"api_endpoint": "squid.clam.whelk"} ) - transport.assert_called_once_with(credentials=None, host="squid.clam.whelk") + grpc_transport.assert_called_once_with( + api_mtls_endpoint=None, + client_cert_source=None, + credentials=None, + host="squid.clam.whelk", + ) def test_list_game_server_configs(transport: str = "grpc"): @@ -576,8 +669,87 @@ def test_game_server_configs_service_host_with_port(): def test_game_server_configs_service_grpc_transport_channel(): channel = grpc.insecure_channel("http://localhost/") - transport = transports.GameServerConfigsServiceGrpcTransport(channel=channel) - assert transport.grpc_channel is channel + + # Check that if channel is provided, mtls endpoint and client_cert_source + # won't be used. + callback = mock.MagicMock() + transport = transports.GameServerConfigsServiceGrpcTransport( + host="squid.clam.whelk", + channel=channel, + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=callback, + ) + assert transport.grpc_channel == channel + assert transport._host == "squid.clam.whelk:443" + assert not callback.called + + +@mock.patch("grpc.ssl_channel_credentials", autospec=True) +@mock.patch("google.api_core.grpc_helpers.create_channel", autospec=True) +def test_game_server_configs_service_grpc_transport_channel_mtls_with_client_cert_source( + grpc_create_channel, grpc_ssl_channel_cred +): + # Check that if channel is None, but api_mtls_endpoint and client_cert_source + # are provided, then a mTLS channel will be created. + mock_cred = mock.Mock() + + mock_ssl_cred = mock.Mock() + grpc_ssl_channel_cred.return_value = mock_ssl_cred + + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + + transport = transports.GameServerConfigsServiceGrpcTransport( + host="squid.clam.whelk", + credentials=mock_cred, + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=client_cert_source_callback, + ) + grpc_ssl_channel_cred.assert_called_once_with( + certificate_chain=b"cert bytes", private_key=b"key bytes" + ) + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=mock_cred, + ssl_credentials=mock_ssl_cred, + scopes=("https://www.googleapis.com/auth/cloud-platform",), + ) + assert transport.grpc_channel == mock_grpc_channel + + +@pytest.mark.parametrize( + "api_mtls_endpoint", ["mtls.squid.clam.whelk", "mtls.squid.clam.whelk:443"] +) +@mock.patch("google.api_core.grpc_helpers.create_channel", autospec=True) +def test_game_server_configs_service_grpc_transport_channel_mtls_with_adc( + grpc_create_channel, api_mtls_endpoint +): + # Check that if channel and client_cert_source are None, but api_mtls_endpoint + # is provided, then a mTLS channel will be created with SSL ADC. + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + + # Mock google.auth.transport.grpc.SslCredentials class. + mock_ssl_cred = mock.Mock() + with mock.patch.multiple( + "google.auth.transport.grpc.SslCredentials", + __init__=mock.Mock(return_value=None), + ssl_credentials=mock.PropertyMock(return_value=mock_ssl_cred), + ): + mock_cred = mock.Mock() + transport = transports.GameServerConfigsServiceGrpcTransport( + host="squid.clam.whelk", + credentials=mock_cred, + api_mtls_endpoint=api_mtls_endpoint, + client_cert_source=None, + ) + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=mock_cred, + ssl_credentials=mock_ssl_cred, + scopes=("https://www.googleapis.com/auth/cloud-platform",), + ) + assert transport.grpc_channel == mock_grpc_channel def test_game_server_configs_service_grpc_lro_client(): @@ -606,3 +778,17 @@ def test_game_server_config_path(): project, location, deployment, config ) assert expected == actual + + +def test_parse_game_server_config_path(): + expected = { + "project": "oyster", + "location": "nudibranch", + "deployment": "cuttlefish", + "config": "mussel", + } + path = GameServerConfigsServiceClient.game_server_config_path(**expected) + + # Check that the path construction is reversible. + actual = GameServerConfigsServiceClient.parse_game_server_config_path(path) + assert expected == actual diff --git a/tests/unit/gaming_v1/test_game_server_deployments_service.py b/tests/unit/gaming_v1/test_game_server_deployments_service.py index dd6cf4a1..847eb83f 100644 --- a/tests/unit/gaming_v1/test_game_server_deployments_service.py +++ b/tests/unit/gaming_v1/test_game_server_deployments_service.py @@ -24,6 +24,7 @@ from google import auth from google.api_core import client_options from google.api_core import future +from google.api_core import grpc_helpers from google.api_core import operations_v1 from google.auth import credentials from google.cloud.gaming_v1.services.game_server_deployments_service import ( @@ -39,6 +40,42 @@ from google.protobuf import timestamp_pb2 as timestamp # type: ignore +def client_cert_source_callback(): + return b"cert bytes", b"key bytes" + + +def test__get_default_mtls_endpoint(): + api_endpoint = "example.googleapis.com" + api_mtls_endpoint = "example.mtls.googleapis.com" + sandbox_endpoint = "example.sandbox.googleapis.com" + sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" + non_googleapi = "api.example.com" + + assert GameServerDeploymentsServiceClient._get_default_mtls_endpoint(None) is None + assert ( + GameServerDeploymentsServiceClient._get_default_mtls_endpoint(api_endpoint) + == api_mtls_endpoint + ) + assert ( + GameServerDeploymentsServiceClient._get_default_mtls_endpoint(api_mtls_endpoint) + == api_mtls_endpoint + ) + assert ( + GameServerDeploymentsServiceClient._get_default_mtls_endpoint(sandbox_endpoint) + == sandbox_mtls_endpoint + ) + assert ( + GameServerDeploymentsServiceClient._get_default_mtls_endpoint( + sandbox_mtls_endpoint + ) + == sandbox_mtls_endpoint + ) + assert ( + GameServerDeploymentsServiceClient._get_default_mtls_endpoint(non_googleapi) + == non_googleapi + ) + + def test_game_server_deployments_service_client_from_service_account_file(): creds = credentials.AnonymousCredentials() with mock.patch.object( @@ -59,31 +96,89 @@ def test_game_server_deployments_service_client_from_service_account_file(): def test_game_server_deployments_service_client_client_options(): - # Check the default options have their expected values. - assert ( - GameServerDeploymentsServiceClient.DEFAULT_OPTIONS.api_endpoint - == "gameservices.googleapis.com" - ) + # Check that if channel is provided we won't create a new one. + with mock.patch( + "google.cloud.gaming_v1.services.game_server_deployments_service.GameServerDeploymentsServiceClient.get_transport_class" + ) as gtc: + transport = transports.GameServerDeploymentsServiceGrpcTransport( + credentials=credentials.AnonymousCredentials() + ) + client = GameServerDeploymentsServiceClient(transport=transport) + gtc.assert_not_called() - # Check that options can be customized. - options = client_options.ClientOptions(api_endpoint="squid.clam.whelk") + # Check mTLS is not triggered with empty client options. + options = client_options.ClientOptions() with mock.patch( "google.cloud.gaming_v1.services.game_server_deployments_service.GameServerDeploymentsServiceClient.get_transport_class" ) as gtc: transport = gtc.return_value = mock.MagicMock() client = GameServerDeploymentsServiceClient(client_options=options) - transport.assert_called_once_with(credentials=None, host="squid.clam.whelk") + transport.assert_called_once_with( + credentials=None, host=client.DEFAULT_ENDPOINT + ) + + # Check mTLS is not triggered if api_endpoint is provided but + # client_cert_source is None. + options = client_options.ClientOptions(api_endpoint="squid.clam.whelk") + with mock.patch( + "google.cloud.gaming_v1.services.game_server_deployments_service.transports.GameServerDeploymentsServiceGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None + client = GameServerDeploymentsServiceClient(client_options=options) + grpc_transport.assert_called_once_with( + api_mtls_endpoint=None, + client_cert_source=None, + credentials=None, + host="squid.clam.whelk", + ) + + # Check mTLS is triggered if client_cert_source is provided. + options = client_options.ClientOptions( + client_cert_source=client_cert_source_callback + ) + with mock.patch( + "google.cloud.gaming_v1.services.game_server_deployments_service.transports.GameServerDeploymentsServiceGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None + client = GameServerDeploymentsServiceClient(client_options=options) + grpc_transport.assert_called_once_with( + api_mtls_endpoint=client.DEFAULT_MTLS_ENDPOINT, + client_cert_source=client_cert_source_callback, + credentials=None, + host=client.DEFAULT_ENDPOINT, + ) + + # Check mTLS is triggered if api_endpoint and client_cert_source are provided. + options = client_options.ClientOptions( + api_endpoint="squid.clam.whelk", client_cert_source=client_cert_source_callback + ) + with mock.patch( + "google.cloud.gaming_v1.services.game_server_deployments_service.transports.GameServerDeploymentsServiceGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None + client = GameServerDeploymentsServiceClient(client_options=options) + grpc_transport.assert_called_once_with( + api_mtls_endpoint="squid.clam.whelk", + client_cert_source=client_cert_source_callback, + credentials=None, + host="squid.clam.whelk", + ) def test_game_server_deployments_service_client_client_options_from_dict(): with mock.patch( - "google.cloud.gaming_v1.services.game_server_deployments_service.GameServerDeploymentsServiceClient.get_transport_class" - ) as gtc: - transport = gtc.return_value = mock.MagicMock() + "google.cloud.gaming_v1.services.game_server_deployments_service.transports.GameServerDeploymentsServiceGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None client = GameServerDeploymentsServiceClient( client_options={"api_endpoint": "squid.clam.whelk"} ) - transport.assert_called_once_with(credentials=None, host="squid.clam.whelk") + grpc_transport.assert_called_once_with( + api_mtls_endpoint=None, + client_cert_source=None, + credentials=None, + host="squid.clam.whelk", + ) def test_list_game_server_deployments(transport: str = "grpc"): @@ -917,8 +1012,87 @@ def test_game_server_deployments_service_host_with_port(): def test_game_server_deployments_service_grpc_transport_channel(): channel = grpc.insecure_channel("http://localhost/") - transport = transports.GameServerDeploymentsServiceGrpcTransport(channel=channel) - assert transport.grpc_channel is channel + + # Check that if channel is provided, mtls endpoint and client_cert_source + # won't be used. + callback = mock.MagicMock() + transport = transports.GameServerDeploymentsServiceGrpcTransport( + host="squid.clam.whelk", + channel=channel, + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=callback, + ) + assert transport.grpc_channel == channel + assert transport._host == "squid.clam.whelk:443" + assert not callback.called + + +@mock.patch("grpc.ssl_channel_credentials", autospec=True) +@mock.patch("google.api_core.grpc_helpers.create_channel", autospec=True) +def test_game_server_deployments_service_grpc_transport_channel_mtls_with_client_cert_source( + grpc_create_channel, grpc_ssl_channel_cred +): + # Check that if channel is None, but api_mtls_endpoint and client_cert_source + # are provided, then a mTLS channel will be created. + mock_cred = mock.Mock() + + mock_ssl_cred = mock.Mock() + grpc_ssl_channel_cred.return_value = mock_ssl_cred + + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + + transport = transports.GameServerDeploymentsServiceGrpcTransport( + host="squid.clam.whelk", + credentials=mock_cred, + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=client_cert_source_callback, + ) + grpc_ssl_channel_cred.assert_called_once_with( + certificate_chain=b"cert bytes", private_key=b"key bytes" + ) + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=mock_cred, + ssl_credentials=mock_ssl_cred, + scopes=("https://www.googleapis.com/auth/cloud-platform",), + ) + assert transport.grpc_channel == mock_grpc_channel + + +@pytest.mark.parametrize( + "api_mtls_endpoint", ["mtls.squid.clam.whelk", "mtls.squid.clam.whelk:443"] +) +@mock.patch("google.api_core.grpc_helpers.create_channel", autospec=True) +def test_game_server_deployments_service_grpc_transport_channel_mtls_with_adc( + grpc_create_channel, api_mtls_endpoint +): + # Check that if channel and client_cert_source are None, but api_mtls_endpoint + # is provided, then a mTLS channel will be created with SSL ADC. + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + + # Mock google.auth.transport.grpc.SslCredentials class. + mock_ssl_cred = mock.Mock() + with mock.patch.multiple( + "google.auth.transport.grpc.SslCredentials", + __init__=mock.Mock(return_value=None), + ssl_credentials=mock.PropertyMock(return_value=mock_ssl_cred), + ): + mock_cred = mock.Mock() + transport = transports.GameServerDeploymentsServiceGrpcTransport( + host="squid.clam.whelk", + credentials=mock_cred, + api_mtls_endpoint=api_mtls_endpoint, + client_cert_source=None, + ) + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=mock_cred, + ssl_credentials=mock_ssl_cred, + scopes=("https://www.googleapis.com/auth/cloud-platform",), + ) + assert transport.grpc_channel == mock_grpc_channel def test_game_server_deployments_service_grpc_lro_client(): @@ -934,29 +1108,51 @@ def test_game_server_deployments_service_grpc_lro_client(): assert transport.operations_client is transport.operations_client -def test_game_server_deployment_path(): +def test_game_server_deployment_rollout_path(): project = "squid" location = "clam" deployment = "whelk" - expected = "projects/{project}/locations/{location}/gameServerDeployments/{deployment}".format( + expected = "projects/{project}/locations/{location}/gameServerDeployments/{deployment}/rollout".format( project=project, location=location, deployment=deployment ) - actual = GameServerDeploymentsServiceClient.game_server_deployment_path( + actual = GameServerDeploymentsServiceClient.game_server_deployment_rollout_path( project, location, deployment ) assert expected == actual -def test_game_server_deployment_rollout_path(): +def test_parse_game_server_deployment_rollout_path(): + expected = {"project": "octopus", "location": "oyster", "deployment": "nudibranch"} + path = GameServerDeploymentsServiceClient.game_server_deployment_rollout_path( + **expected + ) + + # Check that the path construction is reversible. + actual = GameServerDeploymentsServiceClient.parse_game_server_deployment_rollout_path( + path + ) + assert expected == actual + + +def test_game_server_deployment_path(): project = "squid" location = "clam" deployment = "whelk" - expected = "projects/{project}/locations/{location}/gameServerDeployments/{deployment}/rollout".format( + expected = "projects/{project}/locations/{location}/gameServerDeployments/{deployment}".format( project=project, location=location, deployment=deployment ) - actual = GameServerDeploymentsServiceClient.game_server_deployment_rollout_path( + actual = GameServerDeploymentsServiceClient.game_server_deployment_path( project, location, deployment ) assert expected == actual + + +def test_parse_game_server_deployment_path(): + expected = {"project": "octopus", "location": "oyster", "deployment": "nudibranch"} + path = GameServerDeploymentsServiceClient.game_server_deployment_path(**expected) + + # Check that the path construction is reversible. + actual = GameServerDeploymentsServiceClient.parse_game_server_deployment_path(path) + assert expected == actual diff --git a/tests/unit/gaming_v1/test_realms_service.py b/tests/unit/gaming_v1/test_realms_service.py index 61596067..5223a020 100644 --- a/tests/unit/gaming_v1/test_realms_service.py +++ b/tests/unit/gaming_v1/test_realms_service.py @@ -24,6 +24,7 @@ from google import auth from google.api_core import client_options from google.api_core import future +from google.api_core import grpc_helpers from google.api_core import operations_v1 from google.auth import credentials from google.cloud.gaming_v1.services.realms_service import RealmsServiceClient @@ -37,6 +38,39 @@ from google.protobuf import timestamp_pb2 as timestamp # type: ignore +def client_cert_source_callback(): + return b"cert bytes", b"key bytes" + + +def test__get_default_mtls_endpoint(): + api_endpoint = "example.googleapis.com" + api_mtls_endpoint = "example.mtls.googleapis.com" + sandbox_endpoint = "example.sandbox.googleapis.com" + sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" + non_googleapi = "api.example.com" + + assert RealmsServiceClient._get_default_mtls_endpoint(None) is None + assert ( + RealmsServiceClient._get_default_mtls_endpoint(api_endpoint) + == api_mtls_endpoint + ) + assert ( + RealmsServiceClient._get_default_mtls_endpoint(api_mtls_endpoint) + == api_mtls_endpoint + ) + assert ( + RealmsServiceClient._get_default_mtls_endpoint(sandbox_endpoint) + == sandbox_mtls_endpoint + ) + assert ( + RealmsServiceClient._get_default_mtls_endpoint(sandbox_mtls_endpoint) + == sandbox_mtls_endpoint + ) + assert ( + RealmsServiceClient._get_default_mtls_endpoint(non_googleapi) == non_googleapi + ) + + def test_realms_service_client_from_service_account_file(): creds = credentials.AnonymousCredentials() with mock.patch.object( @@ -53,31 +87,89 @@ def test_realms_service_client_from_service_account_file(): def test_realms_service_client_client_options(): - # Check the default options have their expected values. - assert ( - RealmsServiceClient.DEFAULT_OPTIONS.api_endpoint - == "gameservices.googleapis.com" - ) + # Check that if channel is provided we won't create a new one. + with mock.patch( + "google.cloud.gaming_v1.services.realms_service.RealmsServiceClient.get_transport_class" + ) as gtc: + transport = transports.RealmsServiceGrpcTransport( + credentials=credentials.AnonymousCredentials() + ) + client = RealmsServiceClient(transport=transport) + gtc.assert_not_called() - # Check that options can be customized. - options = client_options.ClientOptions(api_endpoint="squid.clam.whelk") + # Check mTLS is not triggered with empty client options. + options = client_options.ClientOptions() with mock.patch( "google.cloud.gaming_v1.services.realms_service.RealmsServiceClient.get_transport_class" ) as gtc: transport = gtc.return_value = mock.MagicMock() client = RealmsServiceClient(client_options=options) - transport.assert_called_once_with(credentials=None, host="squid.clam.whelk") + transport.assert_called_once_with( + credentials=None, host=client.DEFAULT_ENDPOINT + ) + + # Check mTLS is not triggered if api_endpoint is provided but + # client_cert_source is None. + options = client_options.ClientOptions(api_endpoint="squid.clam.whelk") + with mock.patch( + "google.cloud.gaming_v1.services.realms_service.transports.RealmsServiceGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None + client = RealmsServiceClient(client_options=options) + grpc_transport.assert_called_once_with( + api_mtls_endpoint=None, + client_cert_source=None, + credentials=None, + host="squid.clam.whelk", + ) + + # Check mTLS is triggered if client_cert_source is provided. + options = client_options.ClientOptions( + client_cert_source=client_cert_source_callback + ) + with mock.patch( + "google.cloud.gaming_v1.services.realms_service.transports.RealmsServiceGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None + client = RealmsServiceClient(client_options=options) + grpc_transport.assert_called_once_with( + api_mtls_endpoint=client.DEFAULT_MTLS_ENDPOINT, + client_cert_source=client_cert_source_callback, + credentials=None, + host=client.DEFAULT_ENDPOINT, + ) + + # Check mTLS is triggered if api_endpoint and client_cert_source are provided. + options = client_options.ClientOptions( + api_endpoint="squid.clam.whelk", client_cert_source=client_cert_source_callback + ) + with mock.patch( + "google.cloud.gaming_v1.services.realms_service.transports.RealmsServiceGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None + client = RealmsServiceClient(client_options=options) + grpc_transport.assert_called_once_with( + api_mtls_endpoint="squid.clam.whelk", + client_cert_source=client_cert_source_callback, + credentials=None, + host="squid.clam.whelk", + ) def test_realms_service_client_client_options_from_dict(): with mock.patch( - "google.cloud.gaming_v1.services.realms_service.RealmsServiceClient.get_transport_class" - ) as gtc: - transport = gtc.return_value = mock.MagicMock() + "google.cloud.gaming_v1.services.realms_service.transports.RealmsServiceGrpcTransport.__init__" + ) as grpc_transport: + grpc_transport.return_value = None client = RealmsServiceClient( client_options={"api_endpoint": "squid.clam.whelk"} ) - transport.assert_called_once_with(credentials=None, host="squid.clam.whelk") + grpc_transport.assert_called_once_with( + api_mtls_endpoint=None, + client_cert_source=None, + credentials=None, + host="squid.clam.whelk", + ) def test_list_realms(transport: str = "grpc"): @@ -583,8 +675,87 @@ def test_realms_service_host_with_port(): def test_realms_service_grpc_transport_channel(): channel = grpc.insecure_channel("http://localhost/") - transport = transports.RealmsServiceGrpcTransport(channel=channel) - assert transport.grpc_channel is channel + + # Check that if channel is provided, mtls endpoint and client_cert_source + # won't be used. + callback = mock.MagicMock() + transport = transports.RealmsServiceGrpcTransport( + host="squid.clam.whelk", + channel=channel, + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=callback, + ) + assert transport.grpc_channel == channel + assert transport._host == "squid.clam.whelk:443" + assert not callback.called + + +@mock.patch("grpc.ssl_channel_credentials", autospec=True) +@mock.patch("google.api_core.grpc_helpers.create_channel", autospec=True) +def test_realms_service_grpc_transport_channel_mtls_with_client_cert_source( + grpc_create_channel, grpc_ssl_channel_cred +): + # Check that if channel is None, but api_mtls_endpoint and client_cert_source + # are provided, then a mTLS channel will be created. + mock_cred = mock.Mock() + + mock_ssl_cred = mock.Mock() + grpc_ssl_channel_cred.return_value = mock_ssl_cred + + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + + transport = transports.RealmsServiceGrpcTransport( + host="squid.clam.whelk", + credentials=mock_cred, + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=client_cert_source_callback, + ) + grpc_ssl_channel_cred.assert_called_once_with( + certificate_chain=b"cert bytes", private_key=b"key bytes" + ) + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=mock_cred, + ssl_credentials=mock_ssl_cred, + scopes=("https://www.googleapis.com/auth/cloud-platform",), + ) + assert transport.grpc_channel == mock_grpc_channel + + +@pytest.mark.parametrize( + "api_mtls_endpoint", ["mtls.squid.clam.whelk", "mtls.squid.clam.whelk:443"] +) +@mock.patch("google.api_core.grpc_helpers.create_channel", autospec=True) +def test_realms_service_grpc_transport_channel_mtls_with_adc( + grpc_create_channel, api_mtls_endpoint +): + # Check that if channel and client_cert_source are None, but api_mtls_endpoint + # is provided, then a mTLS channel will be created with SSL ADC. + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + + # Mock google.auth.transport.grpc.SslCredentials class. + mock_ssl_cred = mock.Mock() + with mock.patch.multiple( + "google.auth.transport.grpc.SslCredentials", + __init__=mock.Mock(return_value=None), + ssl_credentials=mock.PropertyMock(return_value=mock_ssl_cred), + ): + mock_cred = mock.Mock() + transport = transports.RealmsServiceGrpcTransport( + host="squid.clam.whelk", + credentials=mock_cred, + api_mtls_endpoint=api_mtls_endpoint, + client_cert_source=None, + ) + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=mock_cred, + ssl_credentials=mock_ssl_cred, + scopes=("https://www.googleapis.com/auth/cloud-platform",), + ) + assert transport.grpc_channel == mock_grpc_channel def test_realms_service_grpc_lro_client(): @@ -610,3 +781,12 @@ def test_realm_path(): ) actual = RealmsServiceClient.realm_path(project, location, realm) assert expected == actual + + +def test_parse_realm_path(): + expected = {"project": "octopus", "location": "oyster", "realm": "nudibranch"} + path = RealmsServiceClient.realm_path(**expected) + + # Check that the path construction is reversible. + actual = RealmsServiceClient.parse_realm_path(path) + assert expected == actual