Skip to content
This repository was archived by the owner on Mar 31, 2026. It is now read-only.

Commit d91dd6e

Browse files
authored
Merge branch 'main' into bug-1484
2 parents 80ffdae + 12773d7 commit d91dd6e

File tree

13 files changed

+197
-28
lines changed

13 files changed

+197
-28
lines changed

google/cloud/spanner_dbapi/connection.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,10 @@ def connect(
736736
route_to_leader_enabled=True,
737737
database_role=None,
738738
experimental_host=None,
739+
use_plain_text=False,
740+
ca_certificate=None,
741+
client_certificate=None,
742+
client_key=None,
739743
**kwargs,
740744
):
741745
"""Creates a connection to a Google Cloud Spanner database.
@@ -789,6 +793,28 @@ def connect(
789793
:rtype: :class:`google.cloud.spanner_dbapi.connection.Connection`
790794
:returns: Connection object associated with the given Google Cloud Spanner
791795
resource.
796+
797+
:type experimental_host: str
798+
:param experimental_host: (Optional) The endpoint for a spanner experimental host deployment.
799+
This is intended only for experimental host spanner endpoints.
800+
801+
:type use_plain_text: bool
802+
:param use_plain_text: (Optional) Whether to use plain text for the connection.
803+
This is intended only for experimental host spanner endpoints.
804+
If not set, the default behavior is to use TLS.
805+
806+
:type ca_certificate: str
807+
:param ca_certificate: (Optional) The path to the CA certificate file used for TLS connection.
808+
This is intended only for experimental host spanner endpoints.
809+
This is mandatory if the experimental_host requires a TLS connection.
810+
:type client_certificate: str
811+
:param client_certificate: (Optional) The path to the client certificate file used for mTLS connection.
812+
This is intended only for experimental host spanner endpoints.
813+
This is mandatory if the experimental_host requires an mTLS connection.
814+
:type client_key: str
815+
:param client_key: (Optional) The path to the client key file used for mTLS connection.
816+
This is intended only for experimental host spanner endpoints.
817+
This is mandatory if the experimental_host requires an mTLS connection.
792818
"""
793819
if client is None:
794820
client_info = ClientInfo(
@@ -817,6 +843,10 @@ def connect(
817843
client_info=client_info,
818844
route_to_leader_enabled=route_to_leader_enabled,
819845
client_options=client_options,
846+
use_plain_text=use_plain_text,
847+
ca_certificate=ca_certificate,
848+
client_certificate=client_certificate,
849+
client_key=client_key,
820850
)
821851
else:
822852
if project is not None and client.project != project:

google/cloud/spanner_v1/_helpers.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,3 +868,65 @@ def _merge_Transaction_Options(
868868

869869
# Convert protobuf object back into a TransactionOptions instance
870870
return TransactionOptions(merged_pb)
871+
872+
873+
def _create_experimental_host_transport(
874+
transport_factory,
875+
experimental_host,
876+
use_plain_text,
877+
ca_certificate,
878+
client_certificate,
879+
client_key,
880+
interceptors=None,
881+
):
882+
"""Creates an experimental host transport for Spanner.
883+
884+
Args:
885+
transport_factory (type): The transport class to instantiate (e.g.
886+
`SpannerGrpcTransport`).
887+
experimental_host (str): The endpoint for the experimental host.
888+
use_plain_text (bool): Whether to use a plain text (insecure) connection.
889+
ca_certificate (str): Path to the CA certificate file for TLS.
890+
client_certificate (str): Path to the client certificate file for mTLS.
891+
client_key (str): Path to the client key file for mTLS.
892+
interceptors (list): Optional list of interceptors to add to the channel.
893+
894+
Returns:
895+
object: An instance of the transport class created by `transport_factory`.
896+
897+
Raises:
898+
ValueError: If TLS/mTLS configuration is invalid.
899+
"""
900+
import grpc
901+
from google.auth.credentials import AnonymousCredentials
902+
903+
channel = None
904+
if use_plain_text:
905+
channel = grpc.insecure_channel(target=experimental_host)
906+
elif ca_certificate:
907+
with open(ca_certificate, "rb") as f:
908+
ca_cert = f.read()
909+
if client_certificate and client_key:
910+
with open(client_certificate, "rb") as f:
911+
client_cert = f.read()
912+
with open(client_key, "rb") as f:
913+
private_key = f.read()
914+
ssl_creds = grpc.ssl_channel_credentials(
915+
root_certificates=ca_cert,
916+
private_key=private_key,
917+
certificate_chain=client_cert,
918+
)
919+
elif client_certificate or client_key:
920+
raise ValueError(
921+
"Both client_certificate and client_key must be provided for mTLS connection"
922+
)
923+
else:
924+
ssl_creds = grpc.ssl_channel_credentials(root_certificates=ca_cert)
925+
channel = grpc.secure_channel(experimental_host, ssl_creds)
926+
else:
927+
raise ValueError(
928+
"TLS/mTLS connection requires ca_certificate to be set for experimental_host"
929+
)
930+
if interceptors is not None:
931+
channel = grpc.intercept_channel(channel, *interceptors)
932+
return transport_factory(channel=channel, credentials=AnonymousCredentials())

google/cloud/spanner_v1/client.py

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@
5050
from google.cloud.spanner_v1 import __version__
5151
from google.cloud.spanner_v1 import ExecuteSqlRequest
5252
from google.cloud.spanner_v1 import DefaultTransactionOptions
53-
from google.cloud.spanner_v1._helpers import _merge_query_options
53+
from google.cloud.spanner_v1._helpers import (
54+
_create_experimental_host_transport,
55+
_merge_query_options,
56+
)
5457
from google.cloud.spanner_v1._helpers import _metadata_with_prefix
5558
from google.cloud.spanner_v1.instance import Instance
5659
from google.cloud.spanner_v1.metrics.constants import (
@@ -227,6 +230,30 @@ class Client(ClientWithProject):
227230
228231
:raises: :class:`ValueError <exceptions.ValueError>` if both ``read_only``
229232
and ``admin`` are :data:`True`
233+
234+
:type use_plain_text: bool
235+
:param use_plain_text: (Optional) Whether to use plain text for the connection.
236+
This is intended only for experimental host spanner endpoints.
237+
If set, this will override the `api_endpoint` in `client_options`.
238+
If not set, the default behavior is to use TLS.
239+
240+
:type ca_certificate: str
241+
:param ca_certificate: (Optional) The path to the CA certificate file used for TLS connection.
242+
This is intended only for experimental host spanner endpoints.
243+
If set, this will override the `api_endpoint` in `client_options`.
244+
This is mandatory if the experimental_host requires a TLS connection.
245+
246+
:type client_certificate: str
247+
:param client_certificate: (Optional) The path to the client certificate file used for mTLS connection.
248+
This is intended only for experimental host spanner endpoints.
249+
If set, this will override the `api_endpoint` in `client_options`.
250+
This is mandatory if the experimental_host requires a mTLS connection.
251+
252+
:type client_key: str
253+
:param client_key: (Optional) The path to the client key file used for mTLS connection.
254+
This is intended only for experimental host spanner endpoints.
255+
If set, this will override the `api_endpoint` in `client_options`.
256+
This is mandatory if the experimental_host requires a mTLS connection.
230257
"""
231258

232259
_instance_admin_api = None
@@ -251,6 +278,10 @@ def __init__(
251278
default_transaction_options: Optional[DefaultTransactionOptions] = None,
252279
experimental_host=None,
253280
disable_builtin_metrics=False,
281+
use_plain_text=False,
282+
ca_certificate=None,
283+
client_certificate=None,
284+
client_key=None,
254285
):
255286
self._emulator_host = _get_spanner_emulator_host()
256287
self._experimental_host = experimental_host
@@ -265,6 +296,12 @@ def __init__(
265296
if self._emulator_host:
266297
credentials = AnonymousCredentials()
267298
elif self._experimental_host:
299+
# For all experimental host endpoints project is default
300+
project = "default"
301+
self._use_plain_text = use_plain_text
302+
self._ca_certificate = ca_certificate
303+
self._client_certificate = client_certificate
304+
self._client_key = client_key
268305
credentials = AnonymousCredentials()
269306
elif isinstance(credentials, AnonymousCredentials):
270307
self._emulator_host = self._client_options.api_endpoint
@@ -361,8 +398,13 @@ def instance_admin_api(self):
361398
transport=transport,
362399
)
363400
elif self._experimental_host:
364-
transport = InstanceAdminGrpcTransport(
365-
channel=grpc.insecure_channel(target=self._experimental_host)
401+
transport = _create_experimental_host_transport(
402+
InstanceAdminGrpcTransport,
403+
self._experimental_host,
404+
self._use_plain_text,
405+
self._ca_certificate,
406+
self._client_certificate,
407+
self._client_key,
366408
)
367409
self._instance_admin_api = InstanceAdminClient(
368410
client_info=self._client_info,
@@ -391,8 +433,13 @@ def database_admin_api(self):
391433
transport=transport,
392434
)
393435
elif self._experimental_host:
394-
transport = DatabaseAdminGrpcTransport(
395-
channel=grpc.insecure_channel(target=self._experimental_host)
436+
transport = _create_experimental_host_transport(
437+
DatabaseAdminGrpcTransport,
438+
self._experimental_host,
439+
self._use_plain_text,
440+
self._ca_certificate,
441+
self._client_certificate,
442+
self._client_key,
396443
)
397444
self._database_admin_api = DatabaseAdminClient(
398445
client_info=self._client_info,
@@ -539,7 +586,6 @@ def instance(
539586
self._emulator_host,
540587
labels,
541588
processing_units,
542-
self._experimental_host,
543589
)
544590

545591
def list_instances(self, filter_="", page_size=None):

google/cloud/spanner_v1/database.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
_metadata_with_request_id,
5757
_augment_errors_with_request_id,
5858
_metadata_with_request_id_and_req_id,
59+
_create_experimental_host_transport,
5960
)
6061
from google.cloud.spanner_v1.batch import Batch
6162
from google.cloud.spanner_v1.batch import MutationGroups
@@ -198,17 +199,15 @@ def __init__(
198199
)
199200
self._proto_descriptors = proto_descriptors
200201
self._channel_id = 0 # It'll be created when _spanner_api is created.
202+
self._experimental_host = self._instance._client._experimental_host
201203

202204
if pool is None:
203205
pool = BurstyPool(database_role=database_role)
204206

205207
self._pool = pool
206208
pool.bind(self)
207-
is_experimental_host = self._instance.experimental_host is not None
208209

209-
self._sessions_manager = DatabaseSessionsManager(
210-
self, pool, is_experimental_host
211-
)
210+
self._sessions_manager = DatabaseSessionsManager(self, pool)
212211

213212
@classmethod
214213
def from_pb(cls, database_pb, instance, pool=None):
@@ -453,9 +452,14 @@ def spanner_api(self):
453452
client_info=client_info, transport=transport
454453
)
455454
return self._spanner_api
456-
if self._instance.experimental_host is not None:
457-
transport = SpannerGrpcTransport(
458-
channel=grpc.insecure_channel(self._instance.experimental_host)
455+
if self._experimental_host is not None:
456+
transport = _create_experimental_host_transport(
457+
SpannerGrpcTransport,
458+
self._experimental_host,
459+
self._instance._client._use_plain_text,
460+
self._instance._client._ca_certificate,
461+
self._instance._client._client_certificate,
462+
self._instance._client._client_key,
459463
)
460464
self._spanner_api = SpannerClient(
461465
client_info=client_info,

google/cloud/spanner_v1/database_sessions_manager.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,9 @@ class DatabaseSessionsManager(object):
6262
_MAINTENANCE_THREAD_POLLING_INTERVAL = timedelta(minutes=10)
6363
_MAINTENANCE_THREAD_REFRESH_INTERVAL = timedelta(days=7)
6464

65-
def __init__(self, database, pool, is_experimental_host: bool = False):
65+
def __init__(self, database, pool):
6666
self._database = database
6767
self._pool = pool
68-
self._is_experimental_host = is_experimental_host
6968

7069
# Declare multiplexed session attributes. When a multiplexed session for the
7170
# database session manager is created, a maintenance thread is initialized to
@@ -89,7 +88,8 @@ def get_session(self, transaction_type: TransactionType) -> Session:
8988

9089
session = (
9190
self._get_multiplexed_session()
92-
if self._use_multiplexed(transaction_type) or self._is_experimental_host
91+
if self._use_multiplexed(transaction_type)
92+
or self._database._experimental_host is not None
9393
else self._pool.get()
9494
)
9595

google/cloud/spanner_v1/instance.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,6 @@ def __init__(
122122
emulator_host=None,
123123
labels=None,
124124
processing_units=None,
125-
experimental_host=None,
126125
):
127126
self.instance_id = instance_id
128127
self._client = client
@@ -143,7 +142,6 @@ def __init__(
143142
self._node_count = processing_units // PROCESSING_UNITS_PER_NODE
144143
self.display_name = display_name or instance_id
145144
self.emulator_host = emulator_host
146-
self.experimental_host = experimental_host
147145
if labels is None:
148146
labels = {}
149147
self.labels = labels

google/cloud/spanner_v1/testing/database_test.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import google.auth.credentials
1818
from google.cloud.spanner_admin_database_v1 import DatabaseDialect
1919
from google.cloud.spanner_v1 import SpannerClient
20+
from google.cloud.spanner_v1._helpers import _create_experimental_host_transport
2021
from google.cloud.spanner_v1.database import Database, SPANNER_DATA_SCOPE
2122
from google.cloud.spanner_v1.services.spanner.transports import (
2223
SpannerGrpcTransport,
@@ -86,12 +87,18 @@ def spanner_api(self):
8687
transport=transport,
8788
)
8889
return self._spanner_api
89-
if self._instance.experimental_host is not None:
90-
channel = grpc.insecure_channel(self._instance.experimental_host)
90+
if self._experimental_host is not None:
9191
self._x_goog_request_id_interceptor = XGoogRequestIDHeaderInterceptor()
9292
self._interceptors.append(self._x_goog_request_id_interceptor)
93-
channel = grpc.intercept_channel(channel, *self._interceptors)
94-
transport = SpannerGrpcTransport(channel=channel)
93+
transport = _create_experimental_host_transport(
94+
SpannerGrpcTransport,
95+
self._experimental_host,
96+
self._instance._client._use_plain_text,
97+
self._instance._client._ca_certificate,
98+
self._instance._client._client_certificate,
99+
self._instance._client._client_key,
100+
self._interceptors,
101+
)
95102
self._spanner_api = SpannerClient(
96103
client_info=client_info,
97104
transport=transport,

tests/system/_helpers.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,14 @@
6060
EXPERIMENTAL_HOST = os.getenv(USE_EXPERIMENTAL_HOST_ENVVAR)
6161
USE_EXPERIMENTAL_HOST = EXPERIMENTAL_HOST is not None
6262

63-
EXPERIMENTAL_HOST_PROJECT = "default"
63+
CA_CERTIFICATE_ENVVAR = "CA_CERTIFICATE"
64+
CA_CERTIFICATE = os.getenv(CA_CERTIFICATE_ENVVAR)
65+
CLIENT_CERTIFICATE_ENVVAR = "CLIENT_CERTIFICATE"
66+
CLIENT_CERTIFICATE = os.getenv(CLIENT_CERTIFICATE_ENVVAR)
67+
CLIENT_KEY_ENVVAR = "CLIENT_KEY"
68+
CLIENT_KEY = os.getenv(CLIENT_KEY_ENVVAR)
69+
USE_PLAIN_TEXT = CA_CERTIFICATE is None
70+
6471
EXPERIMENTAL_HOST_INSTANCE = "default"
6572

6673
DDL_STATEMENTS = (

tests/system/conftest.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,10 @@ def spanner_client():
115115

116116
credentials = AnonymousCredentials()
117117
return spanner_v1.Client(
118-
project=_helpers.EXPERIMENTAL_HOST_PROJECT,
118+
use_plain_text=_helpers.USE_PLAIN_TEXT,
119+
ca_certificate=_helpers.CA_CERTIFICATE,
120+
client_certificate=_helpers.CLIENT_CERTIFICATE,
121+
client_key=_helpers.CLIENT_KEY,
119122
credentials=credentials,
120123
experimental_host=_helpers.EXPERIMENTAL_HOST,
121124
)

tests/system/test_dbapi.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1442,6 +1442,10 @@ def test_user_agent(self, shared_instance, dbapi_database):
14421442
experimental_host=_helpers.EXPERIMENTAL_HOST
14431443
if _helpers.USE_EXPERIMENTAL_HOST
14441444
else None,
1445+
use_plain_text=_helpers.USE_PLAIN_TEXT,
1446+
ca_certificate=_helpers.CA_CERTIFICATE,
1447+
client_certificate=_helpers.CLIENT_CERTIFICATE,
1448+
client_key=_helpers.CLIENT_KEY,
14451449
)
14461450
assert (
14471451
conn.instance._client._client_info.user_agent

0 commit comments

Comments
 (0)