Skip to content

Commit 9c5d624

Browse files
authored
fix: address issue in establishing an emulator connection (#246)
Adjusts emulation code to use a newer method of creating a gRPC channel adds a test scenario to validate emulation.
1 parent d3c0aec commit 9c5d624

File tree

4 files changed

+129
-54
lines changed

4 files changed

+129
-54
lines changed

packages/google-cloud-bigtable/google/cloud/bigtable/client.py

Lines changed: 71 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import grpc
3333

3434
from google.api_core.gapic_v1 import client_info
35+
import google.auth
3536

3637
from google.cloud import bigtable_v2
3738
from google.cloud import bigtable_admin_v2
@@ -69,17 +70,12 @@
6970

7071
def _create_gapic_client(client_class, client_options=None, transport=None):
7172
def inner(self):
72-
if self._emulator_host is None:
73-
return client_class(
74-
credentials=None,
75-
client_info=self._client_info,
76-
client_options=client_options,
77-
transport=transport,
78-
)
79-
else:
80-
return client_class(
81-
channel=self._emulator_channel, client_info=self._client_info
82-
)
73+
return client_class(
74+
credentials=None,
75+
client_info=self._client_info,
76+
client_options=client_options,
77+
transport=transport,
78+
)
8379

8480
return inner
8581

@@ -166,16 +162,6 @@ def __init__(
166162
self._admin = bool(admin)
167163
self._client_info = client_info
168164
self._emulator_host = os.getenv(BIGTABLE_EMULATOR)
169-
self._emulator_channel = None
170-
171-
if self._emulator_host is not None:
172-
self._emulator_channel = grpc.insecure_channel(
173-
target=self._emulator_host,
174-
options={
175-
"grpc.keepalive_time_ms": 30000,
176-
"grpc.keepalive_timeout_ms": 10000,
177-
}.items(),
178-
)
179165

180166
if channel is not None:
181167
warnings.warn(
@@ -208,22 +194,76 @@ def _get_scopes(self):
208194

209195
return scopes
210196

197+
def _emulator_channel(self, transport, options):
198+
"""
199+
Creates a channel using self._credentials in a similar way to grpc.secure_channel but
200+
using grpc.local_channel_credentials() rather than grpc.ssh_channel_credentials()
201+
to allow easy connection to a local emulator.
202+
:return: grpc.Channel or grpc.aio.Channel
203+
"""
204+
# TODO: Implement a special credentials type for emulator and use
205+
# "transport.create_channel" to create gRPC channels once google-auth
206+
# extends it's allowed credentials types.
207+
# Note: this code also exists in the firestore client.
208+
if "GrpcAsyncIOTransport" in str(transport.__name__):
209+
return grpc.aio.secure_channel(
210+
self._emulator_host,
211+
self._local_composite_credentials(),
212+
options=options,
213+
)
214+
else:
215+
return grpc.secure_channel(
216+
self._emulator_host,
217+
self._local_composite_credentials(),
218+
options=options,
219+
)
220+
221+
def _local_composite_credentials(self):
222+
"""
223+
Creates the credentials for the local emulator channel
224+
:return: grpc.ChannelCredentials
225+
"""
226+
credentials = google.auth.credentials.with_scopes_if_required(
227+
self._credentials, None
228+
)
229+
request = google.auth.transport.requests.Request()
230+
231+
# Create the metadata plugin for inserting the authorization header.
232+
metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin(
233+
credentials, request
234+
)
235+
236+
# Create a set of grpc.CallCredentials using the metadata plugin.
237+
google_auth_credentials = grpc.metadata_call_credentials(metadata_plugin)
238+
239+
# Using the local_credentials to allow connection to emulator
240+
local_credentials = grpc.local_channel_credentials()
241+
242+
# Combine the local credentials and the authorization credentials.
243+
return grpc.composite_channel_credentials(
244+
local_credentials, google_auth_credentials
245+
)
246+
211247
def _create_gapic_client_channel(self, client_class, grpc_transport):
248+
options = {
249+
"grpc.max_send_message_length": -1,
250+
"grpc.max_receive_message_length": -1,
251+
"grpc.keepalive_time_ms": 30000,
252+
"grpc.keepalive_timeout_ms": 10000,
253+
}.items()
212254
if self._client_options and self._client_options.api_endpoint:
213255
api_endpoint = self._client_options.api_endpoint
214256
else:
215257
api_endpoint = client_class.DEFAULT_ENDPOINT
216258

217-
channel = grpc_transport.create_channel(
218-
host=api_endpoint,
219-
credentials=self._credentials,
220-
options={
221-
"grpc.max_send_message_length": -1,
222-
"grpc.max_receive_message_length": -1,
223-
"grpc.keepalive_time_ms": 30000,
224-
"grpc.keepalive_timeout_ms": 10000,
225-
}.items(),
226-
)
259+
channel = None
260+
if self._emulator_host is not None:
261+
api_endpoint = self._emulator_host
262+
channel = self._emulator_channel(grpc_transport, options)
263+
else:
264+
channel = grpc_transport.create_channel(
265+
host=api_endpoint, credentials=self._credentials, options=options,
266+
)
227267
transport = grpc_transport(channel=channel, host=api_endpoint)
228268
return transport
229269

packages/google-cloud-bigtable/noxfile.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
# 'docfx' is excluded since it only needs to run in 'docs-presubmit'
3434
nox.options.sessions = [
3535
"unit",
36+
"system_emulated",
3637
"system",
3738
"cover",
3839
"lint",
@@ -111,6 +112,31 @@ def unit(session):
111112
default(session)
112113

113114

115+
@nox.session(python="3.8")
116+
def system_emulated(session):
117+
import subprocess
118+
import signal
119+
120+
try:
121+
subprocess.call(["gcloud", "--version"])
122+
except OSError:
123+
session.skip("gcloud not found but required for emulator support")
124+
125+
# Currently, CI/CD doesn't have beta component of gcloud.
126+
subprocess.call(["gcloud", "components", "install", "beta", "bigtable"])
127+
128+
hostport = "localhost:8789"
129+
p = subprocess.Popen(
130+
["gcloud", "beta", "emulators", "bigtable", "start", "--host-port", hostport]
131+
)
132+
133+
session.env["BIGTABLE_EMULATOR_HOST"] = hostport
134+
system(session)
135+
136+
# Stop Emulator
137+
os.killpg(os.getpgid(p.pid), signal.SIGTERM)
138+
139+
114140
@nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS)
115141
def system(session):
116142
"""Run the system test suite."""

packages/google-cloud-bigtable/tests/system.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
from google.cloud.environment_vars import BIGTABLE_EMULATOR
2525
from test_utils.retry import RetryErrors
2626
from test_utils.retry import RetryResult
27-
from test_utils.system import EmulatorCreds
27+
28+
# from test_utils.system import EmulatorCreds
2829
from test_utils.system import unique_resource_id
2930

3031
from google.cloud._helpers import _datetime_from_microseconds
@@ -114,11 +115,9 @@ def setUpModule():
114115

115116
Config.IN_EMULATOR = os.getenv(BIGTABLE_EMULATOR) is not None
116117

117-
if Config.IN_EMULATOR:
118-
credentials = EmulatorCreds()
119-
Config.CLIENT = Client(admin=True, credentials=credentials)
120-
else:
121-
Config.CLIENT = Client(admin=True)
118+
# Previously we created clients using a mock EmulatorCreds when targeting
119+
# an emulator.
120+
Config.CLIENT = Client(admin=True)
122121

123122
Config.INSTANCE = Config.CLIENT.instance(INSTANCE_ID, labels=LABELS)
124123
Config.CLUSTER = Config.INSTANCE.cluster(
@@ -840,6 +839,9 @@ def test_delete_column_family(self):
840839
self.assertEqual(temp_table.list_column_families(), {})
841840

842841
def test_backup(self):
842+
if Config.IN_EMULATOR:
843+
self.skipTest("backups are not supported in the emulator")
844+
843845
from google.cloud._helpers import _datetime_to_pb_timestamp
844846

845847
temp_table_id = "test-backup-table"

packages/google-cloud-bigtable/tests/unit/test_client.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,23 @@ def test_w_emulator(self):
6767
client_class = mock.Mock()
6868
emulator_host = emulator_channel = object()
6969
credentials = _make_credentials()
70+
client_options = mock.Mock()
71+
transport = mock.Mock()
72+
7073
client = _Client(
7174
credentials, emulator_host=emulator_host, emulator_channel=emulator_channel
7275
)
7376
client_info = client._client_info = mock.Mock()
74-
75-
result = self._invoke_client_factory(client_class)(client)
77+
result = self._invoke_client_factory(
78+
client_class, client_options=client_options, transport=transport
79+
)(client)
7680

7781
self.assertIs(result, client_class.return_value)
7882
client_class.assert_called_once_with(
79-
channel=client._emulator_channel, client_info=client_info
83+
credentials=None,
84+
client_info=client_info,
85+
client_options=client_options,
86+
transport=transport,
8087
)
8188

8289

@@ -121,7 +128,6 @@ def test_constructor_defaults(self):
121128
self.assertIs(client._client_info, _CLIENT_INFO)
122129
self.assertIsNone(client._channel)
123130
self.assertIsNone(client._emulator_host)
124-
self.assertIsNone(client._emulator_channel)
125131
self.assertEqual(client.SCOPE, (DATA_SCOPE,))
126132

127133
def test_constructor_explicit(self):
@@ -167,22 +173,23 @@ def test_constructor_with_emulator_host(self):
167173

168174
credentials = _make_credentials()
169175
emulator_host = "localhost:8081"
170-
with mock.patch("os.getenv") as getenv:
171-
getenv.return_value = emulator_host
172-
with mock.patch("grpc.insecure_channel") as factory:
173-
getenv.return_value = emulator_host
176+
with mock.patch("os.environ", {BIGTABLE_EMULATOR: emulator_host}):
177+
with mock.patch("grpc.secure_channel") as factory:
174178
client = self._make_one(project=self.PROJECT, credentials=credentials)
179+
# don't test local_composite_credentials
180+
client._local_composite_credentials = lambda: credentials
181+
# channels are formed when needed, so access a client
182+
# create a gapic channel
183+
client.table_data_client
175184

176185
self.assertEqual(client._emulator_host, emulator_host)
177-
self.assertIs(client._emulator_channel, factory.return_value)
178-
factory.assert_called_once_with(
179-
target=emulator_host,
180-
options={
181-
"grpc.keepalive_time_ms": 30000,
182-
"grpc.keepalive_timeout_ms": 10000,
183-
}.items(),
184-
)
185-
getenv.assert_called_once_with(BIGTABLE_EMULATOR)
186+
options = {
187+
"grpc.max_send_message_length": -1,
188+
"grpc.max_receive_message_length": -1,
189+
"grpc.keepalive_time_ms": 30000,
190+
"grpc.keepalive_timeout_ms": 10000,
191+
}.items()
192+
factory.assert_called_once_with(emulator_host, credentials, options=options)
186193

187194
def test__get_scopes_default(self):
188195
from google.cloud.bigtable.client import DATA_SCOPE

0 commit comments

Comments
 (0)