Skip to content

Commit 0a5c85c

Browse files
WeiranFangtheacodes
authored andcommitted
Add support for gRPC connection management (available when using optional grpc_gcp dependency) (#5553)
1 parent f1f5f78 commit 0a5c85c

File tree

3 files changed

+165
-22
lines changed

3 files changed

+165
-22
lines changed

google/api_core/grpc_helpers.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
import google.auth.transport.grpc
2727
import google.auth.transport.requests
2828

29+
try:
30+
import grpc_gcp
31+
HAS_GRPC_GCP = True
32+
except ImportError:
33+
HAS_GRPC_GCP = False
2934

3035
# The list of gRPC Callable interfaces that return iterators.
3136
_STREAM_WRAP_CLASSES = (
@@ -149,7 +154,11 @@ def wrap_errors(callable_):
149154
return _wrap_unary_errors(callable_)
150155

151156

152-
def create_channel(target, credentials=None, scopes=None, **kwargs):
157+
def create_channel(target,
158+
credentials=None,
159+
scopes=None,
160+
ssl_credentials=None,
161+
**kwargs):
153162
"""Create a secure channel with credentials.
154163
155164
Args:
@@ -160,8 +169,10 @@ def create_channel(target, credentials=None, scopes=None, **kwargs):
160169
scopes (Sequence[str]): A optional list of scopes needed for this
161170
service. These are only used when credentials are not specified and
162171
are passed to :func:`google.auth.default`.
172+
ssl_credentials (grpc.ChannelCredentials): Optional SSL channel
173+
credentials. This can be used to specify different certificates.
163174
kwargs: Additional key-word args passed to
164-
:func:`google.auth.transport.grpc.secure_authorized_channel`.
175+
:func:`grpc_gcp.secure_channel` or :func:`grpc.secure_channel`.
165176
166177
Returns:
167178
grpc.Channel: The created channel.
@@ -174,8 +185,26 @@ def create_channel(target, credentials=None, scopes=None, **kwargs):
174185

175186
request = google.auth.transport.requests.Request()
176187

177-
return google.auth.transport.grpc.secure_authorized_channel(
178-
credentials, request, target, **kwargs)
188+
# Create the metadata plugin for inserting the authorization header.
189+
metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin(
190+
credentials, request)
191+
192+
# Create a set of grpc.CallCredentials using the metadata plugin.
193+
google_auth_credentials = grpc.metadata_call_credentials(metadata_plugin)
194+
195+
if ssl_credentials is None:
196+
ssl_credentials = grpc.ssl_channel_credentials()
197+
198+
# Combine the ssl credentials and the authorization credentials.
199+
composite_credentials = grpc.composite_channel_credentials(
200+
ssl_credentials, google_auth_credentials)
201+
202+
if HAS_GRPC_GCP:
203+
# If grpc_gcp module is available use grpc_gcp.secure_channel,
204+
# otherwise, use grpc.secure_channel to create grpc channel.
205+
return grpc_gcp.secure_channel(target, composite_credentials, **kwargs)
206+
else:
207+
return grpc.secure_channel(target, composite_credentials, **kwargs)
179208

180209

181210
_MethodCall = collections.namedtuple(

nox.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,23 @@ def unit(session, py):
6565
default(session)
6666

6767

68+
@nox.session
69+
@nox.parametrize('py', ['2.7', '3.5', '3.6', '3.7'])
70+
def unit_grpc_gcp(session, py):
71+
"""Run the unit test suite with grpcio-gcp installed."""
72+
73+
# Run unit tests against all supported versions of Python.
74+
session.interpreter = 'python{}'.format(py)
75+
76+
# Set the virtualenv dirname.
77+
session.virtualenv_dirname = 'unit-grpc-gcp-' + py
78+
79+
# Install grpcio-gcp
80+
session.install('grpcio-gcp')
81+
82+
default(session)
83+
84+
6885
@nox.session
6986
def lint(session):
7087
"""Run linters.

tests/unit/test_grpc_helpers.py

Lines changed: 115 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -176,60 +176,157 @@ def test_wrap_errors_streaming(wrap_stream_errors):
176176
wrap_stream_errors.assert_called_once_with(callable_)
177177

178178

179+
@mock.patch('grpc.composite_channel_credentials')
179180
@mock.patch(
180181
'google.auth.default',
181182
return_value=(mock.sentinel.credentials, mock.sentinel.projet))
182-
@mock.patch('google.auth.transport.grpc.secure_authorized_channel')
183-
def test_create_channel_implicit(secure_authorized_channel, default):
183+
@mock.patch('grpc.secure_channel')
184+
def test_create_channel_implicit(
185+
grpc_secure_channel, default, composite_creds_call):
184186
target = 'example.com:443'
187+
composite_creds = composite_creds_call.return_value
185188

186189
channel = grpc_helpers.create_channel(target)
187190

188-
assert channel is secure_authorized_channel.return_value
191+
assert channel is grpc_secure_channel.return_value
189192
default.assert_called_once_with(scopes=None)
190-
secure_authorized_channel.assert_called_once_with(
191-
mock.sentinel.credentials, mock.ANY, target)
193+
if (grpc_helpers.HAS_GRPC_GCP):
194+
grpc_secure_channel.assert_called_once_with(
195+
target, composite_creds, None)
196+
else:
197+
grpc_secure_channel.assert_called_once_with(
198+
target, composite_creds)
192199

193200

201+
@mock.patch('grpc.composite_channel_credentials')
194202
@mock.patch(
195203
'google.auth.default',
196204
return_value=(mock.sentinel.credentials, mock.sentinel.projet))
197-
@mock.patch('google.auth.transport.grpc.secure_authorized_channel')
205+
@mock.patch('grpc.secure_channel')
206+
def test_create_channel_implicit_with_ssl_creds(
207+
grpc_secure_channel, default, composite_creds_call):
208+
target = 'example.com:443'
209+
210+
ssl_creds = grpc.ssl_channel_credentials()
211+
212+
grpc_helpers.create_channel(target, ssl_credentials=ssl_creds)
213+
214+
default.assert_called_once_with(scopes=None)
215+
composite_creds_call.assert_called_once_with(ssl_creds, mock.ANY)
216+
composite_creds = composite_creds_call.return_value
217+
if (grpc_helpers.HAS_GRPC_GCP):
218+
grpc_secure_channel.assert_called_once_with(
219+
target, composite_creds, None)
220+
else:
221+
grpc_secure_channel.assert_called_once_with(
222+
target, composite_creds)
223+
224+
225+
@mock.patch('grpc.composite_channel_credentials')
226+
@mock.patch(
227+
'google.auth.default',
228+
return_value=(mock.sentinel.credentials, mock.sentinel.projet))
229+
@mock.patch('grpc.secure_channel')
198230
def test_create_channel_implicit_with_scopes(
199-
secure_authorized_channel, default):
231+
grpc_secure_channel, default, composite_creds_call):
200232
target = 'example.com:443'
233+
composite_creds = composite_creds_call.return_value
201234

202235
channel = grpc_helpers.create_channel(target, scopes=['one', 'two'])
203236

204-
assert channel is secure_authorized_channel.return_value
237+
assert channel is grpc_secure_channel.return_value
205238
default.assert_called_once_with(scopes=['one', 'two'])
206-
207-
208-
@mock.patch('google.auth.transport.grpc.secure_authorized_channel')
209-
def test_create_channel_explicit(secure_authorized_channel):
239+
if (grpc_helpers.HAS_GRPC_GCP):
240+
grpc_secure_channel.assert_called_once_with(
241+
target, composite_creds, None)
242+
else:
243+
grpc_secure_channel.assert_called_once_with(
244+
target, composite_creds)
245+
246+
247+
@mock.patch('grpc.composite_channel_credentials')
248+
@mock.patch('google.auth.credentials.with_scopes_if_required')
249+
@mock.patch('grpc.secure_channel')
250+
def test_create_channel_explicit(
251+
grpc_secure_channel, auth_creds, composite_creds_call):
210252
target = 'example.com:443'
253+
composite_creds = composite_creds_call.return_value
211254

212255
channel = grpc_helpers.create_channel(
213256
target, credentials=mock.sentinel.credentials)
214257

215-
assert channel is secure_authorized_channel.return_value
216-
secure_authorized_channel.assert_called_once_with(
217-
mock.sentinel.credentials, mock.ANY, target)
258+
auth_creds.assert_called_once_with(mock.sentinel.credentials, None)
259+
assert channel is grpc_secure_channel.return_value
260+
if (grpc_helpers.HAS_GRPC_GCP):
261+
grpc_secure_channel.assert_called_once_with(
262+
target, composite_creds, None)
263+
else:
264+
grpc_secure_channel.assert_called_once_with(
265+
target, composite_creds)
218266

219267

220-
@mock.patch('google.auth.transport.grpc.secure_authorized_channel')
221-
def test_create_channel_explicit_scoped(unused_secure_authorized_channel):
268+
@mock.patch('grpc.composite_channel_credentials')
269+
@mock.patch('grpc.secure_channel')
270+
def test_create_channel_explicit_scoped(
271+
grpc_secure_channel, composite_creds_call):
272+
target = 'example.com:443'
222273
scopes = ['1', '2']
274+
composite_creds = composite_creds_call.return_value
275+
276+
credentials = mock.create_autospec(
277+
google.auth.credentials.Scoped, instance=True)
278+
credentials.requires_scopes = True
279+
280+
channel = grpc_helpers.create_channel(
281+
target,
282+
credentials=credentials,
283+
scopes=scopes)
284+
285+
credentials.with_scopes.assert_called_once_with(scopes)
286+
assert channel is grpc_secure_channel.return_value
287+
if (grpc_helpers.HAS_GRPC_GCP):
288+
grpc_secure_channel.assert_called_once_with(
289+
target, composite_creds, None)
290+
else:
291+
grpc_secure_channel.assert_called_once_with(
292+
target, composite_creds)
293+
294+
295+
@pytest.mark.skipif(not grpc_helpers.HAS_GRPC_GCP,
296+
reason='grpc_gcp module not available')
297+
@mock.patch('grpc_gcp.secure_channel')
298+
def test_create_channel_with_grpc_gcp(grpc_gcp_secure_channel):
299+
target = 'example.com:443'
300+
scopes = ['test_scope']
223301

224302
credentials = mock.create_autospec(
225303
google.auth.credentials.Scoped, instance=True)
226304
credentials.requires_scopes = True
227305

228306
grpc_helpers.create_channel(
229-
mock.sentinel.target,
307+
target,
230308
credentials=credentials,
231309
scopes=scopes)
310+
grpc_gcp_secure_channel.assert_called()
311+
credentials.with_scopes.assert_called_once_with(scopes)
232312

313+
314+
@pytest.mark.skipif(grpc_helpers.HAS_GRPC_GCP,
315+
reason='grpc_gcp module not available')
316+
@mock.patch('grpc.secure_channel')
317+
def test_create_channel_without_grpc_gcp(grpc_secure_channel):
318+
target = 'example.com:443'
319+
scopes = ['test_scope']
320+
321+
credentials = mock.create_autospec(
322+
google.auth.credentials.Scoped, instance=True)
323+
credentials.requires_scopes = True
324+
325+
grpc_helpers.create_channel(
326+
target,
327+
credentials=credentials,
328+
scopes=scopes)
329+
grpc_secure_channel.assert_called()
233330
credentials.with_scopes.assert_called_once_with(scopes)
234331

235332

0 commit comments

Comments
 (0)