Skip to content
This repository was archived by the owner on Sep 5, 2023. It is now read-only.

Commit 669d2a9

Browse files
feat: add mtls support (#7)
1 parent 247ad66 commit 669d2a9

File tree

7 files changed

+338
-36
lines changed

7 files changed

+338
-36
lines changed

docs/conf.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"sphinx.ext.napoleon",
3939
"sphinx.ext.todo",
4040
"sphinx.ext.viewcode",
41+
"recommonmark",
4142
]
4243

4344
# autodoc/autosummary flags
@@ -49,10 +50,6 @@
4950
# Add any paths that contain templates here, relative to this directory.
5051
templates_path = ["_templates"]
5152

52-
# Allow markdown includes (so releases.md can include CHANGLEOG.md)
53-
# http://www.sphinx-doc.org/en/master/markdown.html
54-
source_parsers = {".md": "recommonmark.parser.CommonMarkParser"}
55-
5653
# The suffix(es) of source filenames.
5754
# You can specify multiple suffix as a list of string:
5855
# source_suffix = ['.rst', '.md']

google/cloud/memcache_v1beta2/services/cloud_memcache/client.py

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
#
1717

1818
from collections import OrderedDict
19-
from typing import Dict, Sequence, Tuple, Type, Union
19+
import re
20+
from typing import Callable, Dict, Sequence, Tuple, Type, Union
2021
import pkg_resources
2122

2223
import google.api_core.client_options as ClientOptions # type: ignore
@@ -90,8 +91,38 @@ class CloudMemcacheClient(metaclass=CloudMemcacheClientMeta):
9091
- ``projects/my-memcached-project/locations/us-central1/instances/my-memcached``
9192
"""
9293

93-
DEFAULT_OPTIONS = ClientOptions.ClientOptions(
94-
api_endpoint="memcache.googleapis.com"
94+
@staticmethod
95+
def _get_default_mtls_endpoint(api_endpoint):
96+
"""Convert api endpoint to mTLS endpoint.
97+
Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to
98+
"*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively.
99+
Args:
100+
api_endpoint (Optional[str]): the api endpoint to convert.
101+
Returns:
102+
str: converted mTLS api endpoint.
103+
"""
104+
if not api_endpoint:
105+
return api_endpoint
106+
107+
mtls_endpoint_re = re.compile(
108+
r"(?P<name>[^.]+)(?P<mtls>\.mtls)?(?P<sandbox>\.sandbox)?(?P<googledomain>\.googleapis\.com)?"
109+
)
110+
111+
m = mtls_endpoint_re.match(api_endpoint)
112+
name, mtls, sandbox, googledomain = m.groups()
113+
if mtls or not googledomain:
114+
return api_endpoint
115+
116+
if sandbox:
117+
return api_endpoint.replace(
118+
"sandbox.googleapis.com", "mtls.sandbox.googleapis.com"
119+
)
120+
121+
return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com")
122+
123+
DEFAULT_ENDPOINT = "memcache.googleapis.com"
124+
DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore
125+
DEFAULT_ENDPOINT
95126
)
96127

97128
@classmethod
@@ -121,12 +152,21 @@ def instance_path(project: str, location: str, instance: str) -> str:
121152
project=project, location=location, instance=instance
122153
)
123154

155+
@staticmethod
156+
def parse_instance_path(path: str) -> Dict[str, str]:
157+
"""Parse a instance path into its component segments."""
158+
m = re.match(
159+
r"^projects/(?P<project>.+?)/locations/(?P<location>.+?)/instances/(?P<instance>.+?)$",
160+
path,
161+
)
162+
return m.groupdict() if m else {}
163+
124164
def __init__(
125165
self,
126166
*,
127167
credentials: credentials.Credentials = None,
128168
transport: Union[str, CloudMemcacheTransport] = None,
129-
client_options: ClientOptions = DEFAULT_OPTIONS,
169+
client_options: ClientOptions = None,
130170
) -> None:
131171
"""Instantiate the cloud memcache client.
132172
@@ -140,6 +180,17 @@ def __init__(
140180
transport to use. If set to None, a transport is chosen
141181
automatically.
142182
client_options (ClientOptions): Custom options for the client.
183+
(1) The ``api_endpoint`` property can be used to override the
184+
default endpoint provided by the client.
185+
(2) If ``transport`` argument is None, ``client_options`` can be
186+
used to create a mutual TLS transport. If ``client_cert_source``
187+
is provided, mutual TLS transport will be created with the given
188+
``api_endpoint`` or the default mTLS endpoint, and the client
189+
SSL credentials obtained from ``client_cert_source``.
190+
191+
Raises:
192+
google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport
193+
creation failed for any reason.
143194
"""
144195
if isinstance(client_options, dict):
145196
client_options = ClientOptions.from_dict(client_options)
@@ -148,17 +199,46 @@ def __init__(
148199
# Ordinarily, we provide the transport, but allowing a custom transport
149200
# instance provides an extensibility point for unusual situations.
150201
if isinstance(transport, CloudMemcacheTransport):
202+
# transport is a CloudMemcacheTransport instance.
151203
if credentials:
152204
raise ValueError(
153205
"When providing a transport instance, "
154206
"provide its credentials directly."
155207
)
156208
self._transport = transport
157-
else:
209+
elif client_options is None or (
210+
client_options.api_endpoint is None
211+
and client_options.client_cert_source is None
212+
):
213+
# Don't trigger mTLS if we get an empty ClientOptions.
158214
Transport = type(self).get_transport_class(transport)
159215
self._transport = Transport(
216+
credentials=credentials, host=self.DEFAULT_ENDPOINT
217+
)
218+
else:
219+
# We have a non-empty ClientOptions. If client_cert_source is
220+
# provided, trigger mTLS with user provided endpoint or the default
221+
# mTLS endpoint.
222+
if client_options.client_cert_source:
223+
api_mtls_endpoint = (
224+
client_options.api_endpoint
225+
if client_options.api_endpoint
226+
else self.DEFAULT_MTLS_ENDPOINT
227+
)
228+
else:
229+
api_mtls_endpoint = None
230+
231+
api_endpoint = (
232+
client_options.api_endpoint
233+
if client_options.api_endpoint
234+
else self.DEFAULT_ENDPOINT
235+
)
236+
237+
self._transport = CloudMemcacheGrpcTransport(
160238
credentials=credentials,
161-
host=client_options.api_endpoint or "memcache.googleapis.com",
239+
host=api_endpoint,
240+
api_mtls_endpoint=api_mtls_endpoint,
241+
client_cert_source=client_options.client_cert_source,
162242
)
163243

164244
def list_instances(

google/cloud/memcache_v1beta2/services/cloud_memcache/transports/grpc.py

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515
# limitations under the License.
1616
#
1717

18-
from typing import Callable, Dict
18+
from typing import Callable, Dict, Tuple
1919

2020
from google.api_core import grpc_helpers # type: ignore
2121
from google.api_core import operations_v1 # type: ignore
2222
from google.auth import credentials # type: ignore
23+
from google.auth.transport.grpc import SslCredentials # type: ignore
24+
2325

2426
import grpc # type: ignore
2527

@@ -66,7 +68,9 @@ def __init__(
6668
*,
6769
host: str = "memcache.googleapis.com",
6870
credentials: credentials.Credentials = None,
69-
channel: grpc.Channel = None
71+
channel: grpc.Channel = None,
72+
api_mtls_endpoint: str = None,
73+
client_cert_source: Callable[[], Tuple[bytes, bytes]] = None
7074
) -> None:
7175
"""Instantiate the transport.
7276
@@ -80,20 +84,55 @@ def __init__(
8084
This argument is ignored if ``channel`` is provided.
8185
channel (Optional[grpc.Channel]): A ``Channel`` instance through
8286
which to make calls.
87+
api_mtls_endpoint (Optional[str]): The mutual TLS endpoint. If
88+
provided, it overrides the ``host`` argument and tries to create
89+
a mutual TLS channel with client SSL credentials from
90+
``client_cert_source`` or applicatin default SSL credentials.
91+
client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): A
92+
callback to provide client SSL certificate bytes and private key
93+
bytes, both in PEM format. It is ignored if ``api_mtls_endpoint``
94+
is None.
95+
96+
Raises:
97+
google.auth.exceptions.MutualTlsChannelError: If mutual TLS transport
98+
creation failed for any reason.
8399
"""
84-
# Sanity check: Ensure that channel and credentials are not both
85-
# provided.
86100
if channel:
101+
# Sanity check: Ensure that channel and credentials are not both
102+
# provided.
87103
credentials = False
88104

105+
# If a channel was explicitly provided, set it.
106+
self._grpc_channel = channel
107+
elif api_mtls_endpoint:
108+
host = (
109+
api_mtls_endpoint
110+
if ":" in api_mtls_endpoint
111+
else api_mtls_endpoint + ":443"
112+
)
113+
114+
# Create SSL credentials with client_cert_source or application
115+
# default SSL credentials.
116+
if client_cert_source:
117+
cert, key = client_cert_source()
118+
ssl_credentials = grpc.ssl_channel_credentials(
119+
certificate_chain=cert, private_key=key
120+
)
121+
else:
122+
ssl_credentials = SslCredentials().ssl_credentials
123+
124+
# create a new channel. The provided one is ignored.
125+
self._grpc_channel = grpc_helpers.create_channel(
126+
host,
127+
credentials=credentials,
128+
ssl_credentials=ssl_credentials,
129+
scopes=self.AUTH_SCOPES,
130+
)
131+
89132
# Run the base constructor.
90133
super().__init__(host=host, credentials=credentials)
91134
self._stubs = {} # type: Dict[str, Callable]
92135

93-
# If a channel was explicitly provided, set it.
94-
if channel:
95-
self._grpc_channel = channel
96-
97136
@classmethod
98137
def create_channel(
99138
cls,

mypy.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[mypy]
2-
python_version = 3.5
2+
python_version = 3.6
33
namespace_packages = True

setup.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,7 @@
4040
platforms="Posix; MacOS X; Windows",
4141
include_package_data=True,
4242
install_requires=(
43-
"google-api-core >= 1.8.0, < 2.0.0dev",
44-
"googleapis-common-protos >= 1.5.8",
45-
"grpcio >= 1.10.0",
43+
"google-api-core[grpc] >= 1.17.0, < 2.0.0dev",
4644
"proto-plus >= 0.4.0",
4745
),
4846
python_requires=">=3.6",

0 commit comments

Comments
 (0)