diff --git a/gapic/templates/%namespace/%name_%version/%sub/services/%service/_mixins.py.j2 b/gapic/templates/%namespace/%name_%version/%sub/services/%service/_mixins.py.j2 index 3417068a9f..ef8137dc3b 100644 --- a/gapic/templates/%namespace/%name_%version/%sub/services/%service/_mixins.py.j2 +++ b/gapic/templates/%namespace/%name_%version/%sub/services/%service/_mixins.py.j2 @@ -48,12 +48,16 @@ # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e {% endif %} @@ -105,12 +109,16 @@ # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e {% endif %} {% if "DeleteOperation" in api.mixin_api_methods %} @@ -278,12 +286,16 @@ # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e {% endif %} {% endif %} {# LRO #} @@ -405,12 +417,16 @@ # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e {% endif %} {% if "GetIamPolicy" in api.mixin_api_methods %} @@ -528,12 +544,16 @@ # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e {% endif %} {% if "TestIamPermissions" in api.mixin_api_methods %} @@ -589,12 +609,16 @@ # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e {% endif %} {% endif %} @@ -649,12 +673,16 @@ # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e {% endif %} {% if "ListLocations" in api.mixin_api_methods %} @@ -705,11 +733,15 @@ # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e {% endif %} {% endif %} diff --git a/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 b/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 index 6c45fdd728..0a97287a4e 100644 --- a/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 +++ b/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 @@ -8,6 +8,8 @@ from collections import OrderedDict {% if service.any_extended_operations_methods %} import functools {% endif %} +from http import HTTPStatus +import json import os import re from typing import Dict, Callable, Mapping, MutableMapping, MutableSequence, Optional, {% if service.any_server_streaming %}Iterable, {% endif %}{% if service.any_client_streaming %}Iterator, {% endif %}Sequence, Tuple, Type, Union, cast @@ -417,6 +419,26 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): {{ service.client_name }}._compare_universes(self.universe_domain, self.transport._credentials)) return self._is_universe_domain_valid + def _add_cred_info_for_auth_errors( + self, + error: core_exceptions.GoogleAPICallError + ) -> None: + """Adds credential info string to error details for 401/403/404 errors. + + Args: + error (google.api_core.exceptions.GoogleAPICallError): The error to add the cred info. + """ + if error.code not in [HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN, HTTPStatus.NOT_FOUND]: + return + + cred = self._transport._credentials + if not hasattr(cred, "get_cred_info"): + return + + cred_info = cred.get_cred_info() # type: ignore + if cred_info and hasattr(error._details, "append"): + error._details.append(json.dumps(cred_info)) + @property def api_endpoint(self): """Return the API endpoint used by the client instance. @@ -706,12 +728,16 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e def get_iam_policy( self, @@ -827,12 +853,16 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e def test_iam_permissions( self, @@ -886,12 +916,16 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e {% endif %} DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo(gapic_version=package_version.__version__) diff --git a/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index 82948346e6..5c5c2f1fb0 100644 --- a/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -20,8 +20,8 @@ from grpc.experimental import aio {% if "rest" in opts.transport %} from collections.abc import Iterable from google.protobuf import json_format -import json {% endif %} +import json import math import pytest from google.api_core import api_core_version @@ -89,6 +89,13 @@ from google.iam.v1 import policy_pb2 # type: ignore {% endfilter %} {{ shared_macros.add_google_api_core_version_header_import(service.version) }} +CRED_INFO_JSON = { + "credential_source": "/path/to/file", + "credential_type": "service account credentials", + "principal": "service-account@example.com", +} +CRED_INFO_STRING = json.dumps(CRED_INFO_JSON) + def client_cert_source_callback(): return b"cert bytes", b"key bytes" @@ -250,6 +257,44 @@ def test__get_universe_domain(): {{ service.client_name }}._get_universe_domain("", None) assert str(excinfo.value) == "Universe Domain cannot be an empty string." +@pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ + (401, CRED_INFO_JSON, True), + (403, CRED_INFO_JSON, True), + (404, CRED_INFO_JSON, True), + (500, CRED_INFO_JSON, False), + (401, None, False), + (403, None, False), + (404, None, False), + (500, None, False) +]) +def test__add_cred_info_for_auth_errors(error_code, cred_info_json, show_cred_info): + cred = mock.Mock(["get_cred_info"]) + cred.get_cred_info = mock.Mock(return_value=cred_info_json) + client = {{ service.client_name }}(credentials=cred) + client._transport._credentials = cred + + error = core_exceptions.GoogleAPICallError("message", details=[]) + error.code = error_code + + client._add_cred_info_for_auth_errors(error) + if show_cred_info: + assert error.details == [CRED_INFO_STRING] + else: + assert error.details == [] + +@pytest.mark.parametrize("error_code", [401,403,404,500]) +def test__add_cred_info_for_auth_errors_no_get_cred_info(error_code): + cred = mock.Mock([]) + assert not hasattr(cred, "get_cred_info") + client = {{ service.client_name }}(credentials=cred) + client._transport._credentials = cred + + error = core_exceptions.GoogleAPICallError("message", details=[]) + error.code = error_code + + client._add_cred_info_for_auth_errors(error) + assert error.details == [] + @pytest.mark.parametrize("client_class,transport_class,transport_name", [ {% if 'grpc' in opts.transport %} ({{ service.client_name }}, transports.{{ service.grpc_transport_name }}, "grpc"), diff --git a/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py b/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py index 54349918c7..95dd42b709 100755 --- a/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py +++ b/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/client.py @@ -14,6 +14,8 @@ # limitations under the License. # from collections import OrderedDict +from http import HTTPStatus +import json import os import re from typing import Dict, Callable, Mapping, MutableMapping, MutableSequence, Optional, Sequence, Tuple, Type, Union, cast @@ -500,6 +502,26 @@ def _validate_universe_domain(self): AssetServiceClient._compare_universes(self.universe_domain, self.transport._credentials)) return self._is_universe_domain_valid + def _add_cred_info_for_auth_errors( + self, + error: core_exceptions.GoogleAPICallError + ) -> None: + """Adds credential info string to error details for 401/403/404 errors. + + Args: + error (google.api_core.exceptions.GoogleAPICallError): The error to add the cred info. + """ + if error.code not in [HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN, HTTPStatus.NOT_FOUND]: + return + + cred = self._transport._credentials + if not hasattr(cred, "get_cred_info"): + return + + cred_info = cred.get_cred_info() # type: ignore + if cred_info and hasattr(error._details, "append"): + error._details.append(json.dumps(cred_info)) + @property def api_endpoint(self): """Return the API endpoint used by the client instance. @@ -3511,12 +3533,16 @@ def get_operation( # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e diff --git a/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py b/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py index 6e0e9a6381..183aa8b074 100755 --- a/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py +++ b/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py @@ -71,6 +71,13 @@ import google.auth +CRED_INFO_JSON = { + "credential_source": "/path/to/file", + "credential_type": "service account credentials", + "principal": "service-account@example.com", +} +CRED_INFO_STRING = json.dumps(CRED_INFO_JSON) + def client_cert_source_callback(): return b"cert bytes", b"key bytes" @@ -187,6 +194,44 @@ def test__get_universe_domain(): AssetServiceClient._get_universe_domain("", None) assert str(excinfo.value) == "Universe Domain cannot be an empty string." +@pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ + (401, CRED_INFO_JSON, True), + (403, CRED_INFO_JSON, True), + (404, CRED_INFO_JSON, True), + (500, CRED_INFO_JSON, False), + (401, None, False), + (403, None, False), + (404, None, False), + (500, None, False) +]) +def test__add_cred_info_for_auth_errors(error_code, cred_info_json, show_cred_info): + cred = mock.Mock(["get_cred_info"]) + cred.get_cred_info = mock.Mock(return_value=cred_info_json) + client = AssetServiceClient(credentials=cred) + client._transport._credentials = cred + + error = core_exceptions.GoogleAPICallError("message", details=[]) + error.code = error_code + + client._add_cred_info_for_auth_errors(error) + if show_cred_info: + assert error.details == [CRED_INFO_STRING] + else: + assert error.details == [] + +@pytest.mark.parametrize("error_code", [401,403,404,500]) +def test__add_cred_info_for_auth_errors_no_get_cred_info(error_code): + cred = mock.Mock([]) + assert not hasattr(cred, "get_cred_info") + client = AssetServiceClient(credentials=cred) + client._transport._credentials = cred + + error = core_exceptions.GoogleAPICallError("message", details=[]) + error.code = error_code + + client._add_cred_info_for_auth_errors(error) + assert error.details == [] + @pytest.mark.parametrize("client_class,transport_class,transport_name", [ (AssetServiceClient, transports.AssetServiceGrpcTransport, "grpc"), (AssetServiceClient, transports.AssetServiceRestTransport, "rest"), diff --git a/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py b/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py index b38845b359..3855cbc18d 100755 --- a/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py +++ b/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/client.py @@ -14,6 +14,8 @@ # limitations under the License. # from collections import OrderedDict +from http import HTTPStatus +import json import os import re from typing import Dict, Callable, Mapping, MutableMapping, MutableSequence, Optional, Sequence, Tuple, Type, Union, cast @@ -437,6 +439,26 @@ def _validate_universe_domain(self): IAMCredentialsClient._compare_universes(self.universe_domain, self.transport._credentials)) return self._is_universe_domain_valid + def _add_cred_info_for_auth_errors( + self, + error: core_exceptions.GoogleAPICallError + ) -> None: + """Adds credential info string to error details for 401/403/404 errors. + + Args: + error (google.api_core.exceptions.GoogleAPICallError): The error to add the cred info. + """ + if error.code not in [HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN, HTTPStatus.NOT_FOUND]: + return + + cred = self._transport._credentials + if not hasattr(cred, "get_cred_info"): + return + + cred_info = cred.get_cred_info() # type: ignore + if cred_info and hasattr(error._details, "append"): + error._details.append(json.dumps(cred_info)) + @property def api_endpoint(self): """Return the API endpoint used by the client instance. diff --git a/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py b/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py index 2f646744fd..ba379ed9ae 100755 --- a/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py +++ b/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py @@ -61,6 +61,13 @@ import google.auth +CRED_INFO_JSON = { + "credential_source": "/path/to/file", + "credential_type": "service account credentials", + "principal": "service-account@example.com", +} +CRED_INFO_STRING = json.dumps(CRED_INFO_JSON) + def client_cert_source_callback(): return b"cert bytes", b"key bytes" @@ -177,6 +184,44 @@ def test__get_universe_domain(): IAMCredentialsClient._get_universe_domain("", None) assert str(excinfo.value) == "Universe Domain cannot be an empty string." +@pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ + (401, CRED_INFO_JSON, True), + (403, CRED_INFO_JSON, True), + (404, CRED_INFO_JSON, True), + (500, CRED_INFO_JSON, False), + (401, None, False), + (403, None, False), + (404, None, False), + (500, None, False) +]) +def test__add_cred_info_for_auth_errors(error_code, cred_info_json, show_cred_info): + cred = mock.Mock(["get_cred_info"]) + cred.get_cred_info = mock.Mock(return_value=cred_info_json) + client = IAMCredentialsClient(credentials=cred) + client._transport._credentials = cred + + error = core_exceptions.GoogleAPICallError("message", details=[]) + error.code = error_code + + client._add_cred_info_for_auth_errors(error) + if show_cred_info: + assert error.details == [CRED_INFO_STRING] + else: + assert error.details == [] + +@pytest.mark.parametrize("error_code", [401,403,404,500]) +def test__add_cred_info_for_auth_errors_no_get_cred_info(error_code): + cred = mock.Mock([]) + assert not hasattr(cred, "get_cred_info") + client = IAMCredentialsClient(credentials=cred) + client._transport._credentials = cred + + error = core_exceptions.GoogleAPICallError("message", details=[]) + error.code = error_code + + client._add_cred_info_for_auth_errors(error) + assert error.details == [] + @pytest.mark.parametrize("client_class,transport_class,transport_name", [ (IAMCredentialsClient, transports.IAMCredentialsGrpcTransport, "grpc"), (IAMCredentialsClient, transports.IAMCredentialsRestTransport, "rest"), diff --git a/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py b/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py index ad080aa3ba..aa4e14e52e 100755 --- a/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py +++ b/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/client.py @@ -14,6 +14,8 @@ # limitations under the License. # from collections import OrderedDict +from http import HTTPStatus +import json import os import re from typing import Dict, Callable, Mapping, MutableMapping, MutableSequence, Optional, Sequence, Tuple, Type, Union, cast @@ -545,6 +547,26 @@ def _validate_universe_domain(self): EventarcClient._compare_universes(self.universe_domain, self.transport._credentials)) return self._is_universe_domain_valid + def _add_cred_info_for_auth_errors( + self, + error: core_exceptions.GoogleAPICallError + ) -> None: + """Adds credential info string to error details for 401/403/404 errors. + + Args: + error (google.api_core.exceptions.GoogleAPICallError): The error to add the cred info. + """ + if error.code not in [HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN, HTTPStatus.NOT_FOUND]: + return + + cred = self._transport._credentials + if not hasattr(cred, "get_cred_info"): + return + + cred_info = cred.get_cred_info() # type: ignore + if cred_info and hasattr(error._details, "append"): + error._details.append(json.dumps(cred_info)) + @property def api_endpoint(self): """Return the API endpoint used by the client instance. @@ -2938,12 +2960,16 @@ def list_operations( # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e def get_operation( self, @@ -2992,12 +3018,16 @@ def get_operation( # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e def delete_operation( self, @@ -3219,12 +3249,16 @@ def set_iam_policy( # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e def get_iam_policy( self, @@ -3340,12 +3374,16 @@ def get_iam_policy( # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e def test_iam_permissions( self, @@ -3399,12 +3437,16 @@ def test_iam_permissions( # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e def get_location( self, @@ -3453,12 +3495,16 @@ def get_location( # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e def list_locations( self, @@ -3507,12 +3553,16 @@ def list_locations( # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo(gapic_version=package_version.__version__) diff --git a/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py b/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py index 23ef030671..666a4d908f 100755 --- a/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py +++ b/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py @@ -81,6 +81,13 @@ import google.auth +CRED_INFO_JSON = { + "credential_source": "/path/to/file", + "credential_type": "service account credentials", + "principal": "service-account@example.com", +} +CRED_INFO_STRING = json.dumps(CRED_INFO_JSON) + def client_cert_source_callback(): return b"cert bytes", b"key bytes" @@ -197,6 +204,44 @@ def test__get_universe_domain(): EventarcClient._get_universe_domain("", None) assert str(excinfo.value) == "Universe Domain cannot be an empty string." +@pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ + (401, CRED_INFO_JSON, True), + (403, CRED_INFO_JSON, True), + (404, CRED_INFO_JSON, True), + (500, CRED_INFO_JSON, False), + (401, None, False), + (403, None, False), + (404, None, False), + (500, None, False) +]) +def test__add_cred_info_for_auth_errors(error_code, cred_info_json, show_cred_info): + cred = mock.Mock(["get_cred_info"]) + cred.get_cred_info = mock.Mock(return_value=cred_info_json) + client = EventarcClient(credentials=cred) + client._transport._credentials = cred + + error = core_exceptions.GoogleAPICallError("message", details=[]) + error.code = error_code + + client._add_cred_info_for_auth_errors(error) + if show_cred_info: + assert error.details == [CRED_INFO_STRING] + else: + assert error.details == [] + +@pytest.mark.parametrize("error_code", [401,403,404,500]) +def test__add_cred_info_for_auth_errors_no_get_cred_info(error_code): + cred = mock.Mock([]) + assert not hasattr(cred, "get_cred_info") + client = EventarcClient(credentials=cred) + client._transport._credentials = cred + + error = core_exceptions.GoogleAPICallError("message", details=[]) + error.code = error_code + + client._add_cred_info_for_auth_errors(error) + assert error.details == [] + @pytest.mark.parametrize("client_class,transport_class,transport_name", [ (EventarcClient, transports.EventarcGrpcTransport, "grpc"), (EventarcClient, transports.EventarcRestTransport, "rest"), diff --git a/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py b/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py index 92a2df84cf..aea2902983 100755 --- a/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py +++ b/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/client.py @@ -14,6 +14,8 @@ # limitations under the License. # from collections import OrderedDict +from http import HTTPStatus +import json import os import re from typing import Dict, Callable, Mapping, MutableMapping, MutableSequence, Optional, Sequence, Tuple, Type, Union, cast @@ -496,6 +498,26 @@ def _validate_universe_domain(self): ConfigServiceV2Client._compare_universes(self.universe_domain, self.transport._credentials)) return self._is_universe_domain_valid + def _add_cred_info_for_auth_errors( + self, + error: core_exceptions.GoogleAPICallError + ) -> None: + """Adds credential info string to error details for 401/403/404 errors. + + Args: + error (google.api_core.exceptions.GoogleAPICallError): The error to add the cred info. + """ + if error.code not in [HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN, HTTPStatus.NOT_FOUND]: + return + + cred = self._transport._credentials + if not hasattr(cred, "get_cred_info"): + return + + cred_info = cred.get_cred_info() # type: ignore + if cred_info and hasattr(error._details, "append"): + error._details.append(json.dumps(cred_info)) + @property def api_endpoint(self): """Return the API endpoint used by the client instance. @@ -4229,12 +4251,16 @@ def list_operations( # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e def get_operation( self, @@ -4283,12 +4309,16 @@ def get_operation( # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e def cancel_operation( self, diff --git a/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py b/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py index 9d085d369e..db77c53b9b 100755 --- a/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py +++ b/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/client.py @@ -14,6 +14,8 @@ # limitations under the License. # from collections import OrderedDict +from http import HTTPStatus +import json import os import re from typing import Dict, Callable, Mapping, MutableMapping, MutableSequence, Optional, Iterable, Iterator, Sequence, Tuple, Type, Union, cast @@ -427,6 +429,26 @@ def _validate_universe_domain(self): LoggingServiceV2Client._compare_universes(self.universe_domain, self.transport._credentials)) return self._is_universe_domain_valid + def _add_cred_info_for_auth_errors( + self, + error: core_exceptions.GoogleAPICallError + ) -> None: + """Adds credential info string to error details for 401/403/404 errors. + + Args: + error (google.api_core.exceptions.GoogleAPICallError): The error to add the cred info. + """ + if error.code not in [HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN, HTTPStatus.NOT_FOUND]: + return + + cred = self._transport._credentials + if not hasattr(cred, "get_cred_info"): + return + + cred_info = cred.get_cred_info() # type: ignore + if cred_info and hasattr(error._details, "append"): + error._details.append(json.dumps(cred_info)) + @property def api_endpoint(self): """Return the API endpoint used by the client instance. @@ -1369,12 +1391,16 @@ def list_operations( # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e def get_operation( self, @@ -1423,12 +1449,16 @@ def get_operation( # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e def cancel_operation( self, diff --git a/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py b/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py index 1fff39e085..c2aa3c37dd 100755 --- a/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py +++ b/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/client.py @@ -14,6 +14,8 @@ # limitations under the License. # from collections import OrderedDict +from http import HTTPStatus +import json import os import re from typing import Dict, Callable, Mapping, MutableMapping, MutableSequence, Optional, Sequence, Tuple, Type, Union, cast @@ -428,6 +430,26 @@ def _validate_universe_domain(self): MetricsServiceV2Client._compare_universes(self.universe_domain, self.transport._credentials)) return self._is_universe_domain_valid + def _add_cred_info_for_auth_errors( + self, + error: core_exceptions.GoogleAPICallError + ) -> None: + """Adds credential info string to error details for 401/403/404 errors. + + Args: + error (google.api_core.exceptions.GoogleAPICallError): The error to add the cred info. + """ + if error.code not in [HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN, HTTPStatus.NOT_FOUND]: + return + + cred = self._transport._credentials + if not hasattr(cred, "get_cred_info"): + return + + cred_info = cred.get_cred_info() # type: ignore + if cred_info and hasattr(error._details, "append"): + error._details.append(json.dumps(cred_info)) + @property def api_endpoint(self): """Return the API endpoint used by the client instance. @@ -1221,12 +1243,16 @@ def list_operations( # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e def get_operation( self, @@ -1275,12 +1301,16 @@ def get_operation( # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e def cancel_operation( self, diff --git a/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py b/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py index 62c50348d5..7c08b2c0f4 100755 --- a/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -23,6 +23,7 @@ import grpc from grpc.experimental import aio +import json import math import pytest from google.api_core import api_core_version @@ -61,6 +62,13 @@ import google.auth +CRED_INFO_JSON = { + "credential_source": "/path/to/file", + "credential_type": "service account credentials", + "principal": "service-account@example.com", +} +CRED_INFO_STRING = json.dumps(CRED_INFO_JSON) + def client_cert_source_callback(): return b"cert bytes", b"key bytes" @@ -177,6 +185,44 @@ def test__get_universe_domain(): ConfigServiceV2Client._get_universe_domain("", None) assert str(excinfo.value) == "Universe Domain cannot be an empty string." +@pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ + (401, CRED_INFO_JSON, True), + (403, CRED_INFO_JSON, True), + (404, CRED_INFO_JSON, True), + (500, CRED_INFO_JSON, False), + (401, None, False), + (403, None, False), + (404, None, False), + (500, None, False) +]) +def test__add_cred_info_for_auth_errors(error_code, cred_info_json, show_cred_info): + cred = mock.Mock(["get_cred_info"]) + cred.get_cred_info = mock.Mock(return_value=cred_info_json) + client = ConfigServiceV2Client(credentials=cred) + client._transport._credentials = cred + + error = core_exceptions.GoogleAPICallError("message", details=[]) + error.code = error_code + + client._add_cred_info_for_auth_errors(error) + if show_cred_info: + assert error.details == [CRED_INFO_STRING] + else: + assert error.details == [] + +@pytest.mark.parametrize("error_code", [401,403,404,500]) +def test__add_cred_info_for_auth_errors_no_get_cred_info(error_code): + cred = mock.Mock([]) + assert not hasattr(cred, "get_cred_info") + client = ConfigServiceV2Client(credentials=cred) + client._transport._credentials = cred + + error = core_exceptions.GoogleAPICallError("message", details=[]) + error.code = error_code + + client._add_cred_info_for_auth_errors(error) + assert error.details == [] + @pytest.mark.parametrize("client_class,transport_class,transport_name", [ (ConfigServiceV2Client, transports.ConfigServiceV2GrpcTransport, "grpc"), ]) diff --git a/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py index b0fd3c2d5f..c518e0b28d 100755 --- a/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -23,6 +23,7 @@ import grpc from grpc.experimental import aio +import json import math import pytest from google.api_core import api_core_version @@ -62,6 +63,13 @@ import google.auth +CRED_INFO_JSON = { + "credential_source": "/path/to/file", + "credential_type": "service account credentials", + "principal": "service-account@example.com", +} +CRED_INFO_STRING = json.dumps(CRED_INFO_JSON) + def client_cert_source_callback(): return b"cert bytes", b"key bytes" @@ -178,6 +186,44 @@ def test__get_universe_domain(): LoggingServiceV2Client._get_universe_domain("", None) assert str(excinfo.value) == "Universe Domain cannot be an empty string." +@pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ + (401, CRED_INFO_JSON, True), + (403, CRED_INFO_JSON, True), + (404, CRED_INFO_JSON, True), + (500, CRED_INFO_JSON, False), + (401, None, False), + (403, None, False), + (404, None, False), + (500, None, False) +]) +def test__add_cred_info_for_auth_errors(error_code, cred_info_json, show_cred_info): + cred = mock.Mock(["get_cred_info"]) + cred.get_cred_info = mock.Mock(return_value=cred_info_json) + client = LoggingServiceV2Client(credentials=cred) + client._transport._credentials = cred + + error = core_exceptions.GoogleAPICallError("message", details=[]) + error.code = error_code + + client._add_cred_info_for_auth_errors(error) + if show_cred_info: + assert error.details == [CRED_INFO_STRING] + else: + assert error.details == [] + +@pytest.mark.parametrize("error_code", [401,403,404,500]) +def test__add_cred_info_for_auth_errors_no_get_cred_info(error_code): + cred = mock.Mock([]) + assert not hasattr(cred, "get_cred_info") + client = LoggingServiceV2Client(credentials=cred) + client._transport._credentials = cred + + error = core_exceptions.GoogleAPICallError("message", details=[]) + error.code = error_code + + client._add_cred_info_for_auth_errors(error) + assert error.details == [] + @pytest.mark.parametrize("client_class,transport_class,transport_name", [ (LoggingServiceV2Client, transports.LoggingServiceV2GrpcTransport, "grpc"), ]) diff --git a/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index 939eb6911f..123710f74f 100755 --- a/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -23,6 +23,7 @@ import grpc from grpc.experimental import aio +import json import math import pytest from google.api_core import api_core_version @@ -60,6 +61,13 @@ import google.auth +CRED_INFO_JSON = { + "credential_source": "/path/to/file", + "credential_type": "service account credentials", + "principal": "service-account@example.com", +} +CRED_INFO_STRING = json.dumps(CRED_INFO_JSON) + def client_cert_source_callback(): return b"cert bytes", b"key bytes" @@ -176,6 +184,44 @@ def test__get_universe_domain(): MetricsServiceV2Client._get_universe_domain("", None) assert str(excinfo.value) == "Universe Domain cannot be an empty string." +@pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ + (401, CRED_INFO_JSON, True), + (403, CRED_INFO_JSON, True), + (404, CRED_INFO_JSON, True), + (500, CRED_INFO_JSON, False), + (401, None, False), + (403, None, False), + (404, None, False), + (500, None, False) +]) +def test__add_cred_info_for_auth_errors(error_code, cred_info_json, show_cred_info): + cred = mock.Mock(["get_cred_info"]) + cred.get_cred_info = mock.Mock(return_value=cred_info_json) + client = MetricsServiceV2Client(credentials=cred) + client._transport._credentials = cred + + error = core_exceptions.GoogleAPICallError("message", details=[]) + error.code = error_code + + client._add_cred_info_for_auth_errors(error) + if show_cred_info: + assert error.details == [CRED_INFO_STRING] + else: + assert error.details == [] + +@pytest.mark.parametrize("error_code", [401,403,404,500]) +def test__add_cred_info_for_auth_errors_no_get_cred_info(error_code): + cred = mock.Mock([]) + assert not hasattr(cred, "get_cred_info") + client = MetricsServiceV2Client(credentials=cred) + client._transport._credentials = cred + + error = core_exceptions.GoogleAPICallError("message", details=[]) + error.code = error_code + + client._add_cred_info_for_auth_errors(error) + assert error.details == [] + @pytest.mark.parametrize("client_class,transport_class,transport_name", [ (MetricsServiceV2Client, transports.MetricsServiceV2GrpcTransport, "grpc"), ]) diff --git a/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py b/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py index 925ff8da9f..5706dafa1c 100755 --- a/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py +++ b/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py @@ -14,6 +14,8 @@ # limitations under the License. # from collections import OrderedDict +from http import HTTPStatus +import json import os import re from typing import Dict, Callable, Mapping, MutableMapping, MutableSequence, Optional, Sequence, Tuple, Type, Union, cast @@ -454,6 +456,26 @@ def _validate_universe_domain(self): CloudRedisClient._compare_universes(self.universe_domain, self.transport._credentials)) return self._is_universe_domain_valid + def _add_cred_info_for_auth_errors( + self, + error: core_exceptions.GoogleAPICallError + ) -> None: + """Adds credential info string to error details for 401/403/404 errors. + + Args: + error (google.api_core.exceptions.GoogleAPICallError): The error to add the cred info. + """ + if error.code not in [HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN, HTTPStatus.NOT_FOUND]: + return + + cred = self._transport._credentials + if not hasattr(cred, "get_cred_info"): + return + + cred_info = cred.get_cred_info() # type: ignore + if cred_info and hasattr(error._details, "append"): + error._details.append(json.dumps(cred_info)) + @property def api_endpoint(self): """Return the API endpoint used by the client instance. @@ -2106,12 +2128,16 @@ def list_operations( # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e def get_operation( self, @@ -2160,12 +2186,16 @@ def get_operation( # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e def delete_operation( self, @@ -2321,12 +2351,16 @@ def get_location( # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e def list_locations( self, @@ -2375,12 +2409,16 @@ def list_locations( # Validate the universe domain. self._validate_universe_domain() - # Send the request. - response = rpc( - request, retry=retry, timeout=timeout, metadata=metadata,) + try: + # Send the request. + response = rpc( + request, retry=retry, timeout=timeout, metadata=metadata,) - # Done; return the response. - return response + # Done; return the response. + return response + except core_exceptions.GoogleAPICallError as e: + self._add_cred_info_for_auth_errors(e) + raise e DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo(gapic_version=package_version.__version__) diff --git a/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py b/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py index b06145517d..578663ebe1 100755 --- a/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -72,6 +72,13 @@ import google.auth +CRED_INFO_JSON = { + "credential_source": "/path/to/file", + "credential_type": "service account credentials", + "principal": "service-account@example.com", +} +CRED_INFO_STRING = json.dumps(CRED_INFO_JSON) + def client_cert_source_callback(): return b"cert bytes", b"key bytes" @@ -188,6 +195,44 @@ def test__get_universe_domain(): CloudRedisClient._get_universe_domain("", None) assert str(excinfo.value) == "Universe Domain cannot be an empty string." +@pytest.mark.parametrize("error_code,cred_info_json,show_cred_info", [ + (401, CRED_INFO_JSON, True), + (403, CRED_INFO_JSON, True), + (404, CRED_INFO_JSON, True), + (500, CRED_INFO_JSON, False), + (401, None, False), + (403, None, False), + (404, None, False), + (500, None, False) +]) +def test__add_cred_info_for_auth_errors(error_code, cred_info_json, show_cred_info): + cred = mock.Mock(["get_cred_info"]) + cred.get_cred_info = mock.Mock(return_value=cred_info_json) + client = CloudRedisClient(credentials=cred) + client._transport._credentials = cred + + error = core_exceptions.GoogleAPICallError("message", details=[]) + error.code = error_code + + client._add_cred_info_for_auth_errors(error) + if show_cred_info: + assert error.details == [CRED_INFO_STRING] + else: + assert error.details == [] + +@pytest.mark.parametrize("error_code", [401,403,404,500]) +def test__add_cred_info_for_auth_errors_no_get_cred_info(error_code): + cred = mock.Mock([]) + assert not hasattr(cred, "get_cred_info") + client = CloudRedisClient(credentials=cred) + client._transport._credentials = cred + + error = core_exceptions.GoogleAPICallError("message", details=[]) + error.code = error_code + + client._add_cred_info_for_auth_errors(error) + assert error.details == [] + @pytest.mark.parametrize("client_class,transport_class,transport_name", [ (CloudRedisClient, transports.CloudRedisGrpcTransport, "grpc"), (CloudRedisClient, transports.CloudRedisRestTransport, "rest"),