Skip to content

Commit c238fe3

Browse files
feat: add support for PSC connections (#766)
1 parent a1ed78a commit c238fe3

File tree

7 files changed

+64
-6
lines changed

7 files changed

+64
-6
lines changed

README.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -252,9 +252,17 @@ with Connector() as connector:
252252
print(row)
253253
```
254254

255-
### Specifying Public or Private IP
255+
### Specifying IP Address Type
256+
257+
The Cloud SQL Python Connector can be used to connect to Cloud SQL instances
258+
using both public and private IP addresses, as well as
259+
[Private Service Connect][psc] (PSC). To specify which IP address type to connect
260+
with, set the `ip_type` keyword argument when initializing a `Connector()` or when
261+
calling `connector.connect()`.
262+
263+
Possible values for `ip_type` are `IPTypes.PUBLIC` (default value),
264+
`IPTypes.PRIVATE`, and `IPTypes.PSC`.
256265

257-
The Cloud SQL Connector for Python can be used to connect to Cloud SQL instances using both public and private IP addresses. To specify which IP address to use to connect, set the `ip_type` keyword argument Possible values are `IPTypes.PUBLIC` and `IPTypes.PRIVATE`.
258266
Example:
259267

260268
```python
@@ -268,9 +276,14 @@ conn = connector.connect(
268276
)
269277
```
270278

271-
Note: If specifying Private IP, your application must already be in the same VPC network as your Cloud SQL Instance.
279+
Note: If specifying Private IP or Private Service Connect, your application must be
280+
attached to the proper VPC network to connect to your Cloud SQL instance. For most
281+
applications this will require the use of a [VPC Connector][vpc-connector].
282+
283+
[psc]: https://cloud.google.com/vpc/docs/private-service-connect
284+
[vpc-connector]: https://cloud.google.com/vpc/docs/configure-serverless-vpc-access#create-connector
272285

273-
### IAM Authentication
286+
### Automatic IAM Database Authentication
274287

275288
Connections using [Automatic IAM database authentication](https://cloud.google.com/sql/docs/postgres/authentication#automatic) are supported when using Postgres or MySQL drivers.
276289
First, make sure to [configure your Cloud SQL Instance to allow IAM authentication](https://cloud.google.com/sql/docs/postgres/create-edit-iam-instances#configure-iam-db-instance)

google/cloud/sql/connector/connector.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@
1818
import asyncio
1919
from functools import partial
2020
import logging
21+
import socket
2122
from threading import Thread
2223
from types import TracebackType
2324
from typing import Any, Dict, Optional, Type, TYPE_CHECKING
2425

2526
import google.cloud.sql.connector.asyncpg as asyncpg
26-
from google.cloud.sql.connector.exceptions import ConnectorLoopError
27+
from google.cloud.sql.connector.exceptions import (
28+
ConnectorLoopError,
29+
DnsNameResolutionError,
30+
)
2731
from google.cloud.sql.connector.instance import (
2832
Instance,
2933
IPTypes,
@@ -238,6 +242,21 @@ async def connect_async(
238242
# attempt to make connection to Cloud SQL instance
239243
try:
240244
instance_data, ip_address = await instance.connect_info(ip_type)
245+
# resolve DNS name into IP address for PSC
246+
if ip_type.value == "PSC":
247+
addr_info = await self._loop.getaddrinfo(
248+
ip_address, None, family=socket.AF_INET, type=socket.SOCK_STREAM
249+
)
250+
# getaddrinfo returns a list of 5-tuples that contain socket
251+
# connection info in the form
252+
# (family, type, proto, canonname, sockaddr), where sockaddr is a
253+
# 2-tuple in the form (ip_address, port)
254+
try:
255+
ip_address = addr_info[0][4][0]
256+
except IndexError as e:
257+
raise DnsNameResolutionError(
258+
f"['{instance_connection_string}']: DNS name could not be resolved into IP address"
259+
) from e
241260

242261
# format `user` param for automatic IAM database authn
243262
if enable_iam_auth:

google/cloud/sql/connector/exceptions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,10 @@ class AutoIAMAuthNotSupported(Exception):
6363
"""
6464

6565
pass
66+
67+
68+
class DnsNameResolutionError(Exception):
69+
"""
70+
Exception to be raised when the DnsName of a PSC connection to a
71+
Cloud SQL instance can not be resolved to a proper IP address.
72+
"""

google/cloud/sql/connector/instance.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
class IPTypes(Enum):
6161
PUBLIC: str = "PRIMARY"
6262
PRIVATE: str = "PRIVATE"
63+
PSC: str = "PSC"
6364

6465

6566
class InstanceMetadata:

google/cloud/sql/connector/refresh_utils.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,16 @@ async def _get_metadata(
108108
f'[{project}:{region}:{instance}]: Provided region was mismatched - got region {region}, expected {ret_dict["region"]}.'
109109
)
110110

111+
ip_addresses = (
112+
{ip["type"]: ip["ipAddress"] for ip in ret_dict["ipAddresses"]}
113+
if "ipAddresses" in ret_dict
114+
else {}
115+
)
116+
if "dnsName" in ret_dict:
117+
ip_addresses["PSC"] = ret_dict["dnsName"]
118+
111119
metadata = {
112-
"ip_addresses": {ip["type"]: ip["ipAddress"] for ip in ret_dict["ipAddresses"]},
120+
"ip_addresses": ip_addresses,
113121
"server_ca_cert": ret_dict["serverCaCert"]["cert"],
114122
"database_version": ret_dict["databaseVersion"],
115123
}

tests/unit/mocks.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ def connect_settings(self, ip_addrs: Optional[Dict] = None) -> str:
219219
datetime.datetime.utcnow() + datetime.timedelta(minutes=10)
220220
),
221221
},
222+
"dnsName": "abcde.12345.us-central1.sql.goog",
222223
"ipAddresses": ip_addresses,
223224
"region": self.region,
224225
"databaseVersion": self.db_version,

tests/unit/test_instance.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,11 @@ async def test_get_preferred_ip(instance: Instance) -> None:
301301
# verify private ip address is preferred
302302
assert ip_addr == "1.1.1.1"
303303

304+
# test PSC as preferred IP type for connection
305+
ip_addr = instance_metadata.get_preferred_ip(IPTypes.PSC)
306+
# verify PSC ip address is preferred
307+
assert ip_addr == "abcde.12345.us-central1.sql.goog"
308+
304309

305310
@pytest.mark.asyncio
306311
async def test_get_preferred_ip_CloudSQLIPTypeError(instance: Instance) -> None:
@@ -319,6 +324,10 @@ async def test_get_preferred_ip_CloudSQLIPTypeError(instance: Instance) -> None:
319324
with pytest.raises(CloudSQLIPTypeError):
320325
instance_metadata.get_preferred_ip(IPTypes.PRIVATE)
321326

327+
# test error when PSC is missing
328+
with pytest.raises(CloudSQLIPTypeError):
329+
instance_metadata.get_preferred_ip(IPTypes.PSC)
330+
322331

323332
@pytest.mark.asyncio
324333
async def test_ClientResponseError(

0 commit comments

Comments
 (0)