cloud-sql-python-connector 1.14.0__tar.gz → 1.16.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/PKG-INFO +65 -1
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/README.md +63 -0
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/cloud_sql_python_connector.egg-info/PKG-INFO +65 -1
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/cloud_sql_python_connector.egg-info/SOURCES.txt +1 -0
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/cloud_sql_python_connector.egg-info/requires.txt +1 -0
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/google/cloud/sql/connector/__init__.py +4 -0
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/google/cloud/sql/connector/client.py +36 -16
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/google/cloud/sql/connector/connection_info.py +2 -0
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/google/cloud/sql/connector/connection_name.py +11 -1
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/google/cloud/sql/connector/connector.py +26 -16
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/google/cloud/sql/connector/enums.py +6 -5
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/google/cloud/sql/connector/exceptions.py +7 -0
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/google/cloud/sql/connector/instance.py +7 -27
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/google/cloud/sql/connector/lazy.py +5 -15
- cloud_sql_python_connector-1.16.0/google/cloud/sql/connector/resolver.py +70 -0
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/google/cloud/sql/connector/version.py +1 -1
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/setup.py +1 -0
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/LICENSE +0 -0
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/cloud_sql_python_connector.egg-info/dependency_links.txt +0 -0
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/cloud_sql_python_connector.egg-info/not-zip-safe +0 -0
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/cloud_sql_python_connector.egg-info/top_level.txt +0 -0
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/google/cloud/sql/connector/asyncpg.py +0 -0
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/google/cloud/sql/connector/pg8000.py +0 -0
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/google/cloud/sql/connector/py.typed +0 -0
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/google/cloud/sql/connector/pymysql.py +0 -0
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/google/cloud/sql/connector/pytds.py +0 -0
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/google/cloud/sql/connector/rate_limiter.py +0 -0
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/google/cloud/sql/connector/refresh_utils.py +0 -0
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/google/cloud/sql/connector/utils.py +0 -0
- {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.16.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cloud-sql-python-connector
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.16.0
|
|
4
4
|
Summary: The Cloud SQL Python Connector is a library that can be used alongside a database driver to allow users with sufficient permissions to connect to a Cloud SQL database without having to manually allowlist IPs or manage SSL certificates.
|
|
5
5
|
Home-page: https://github.com/GoogleCloudPlatform/cloud-sql-python-connector
|
|
6
6
|
Author: Google LLC
|
|
@@ -22,6 +22,7 @@ License-File: LICENSE
|
|
|
22
22
|
Requires-Dist: aiofiles
|
|
23
23
|
Requires-Dist: aiohttp
|
|
24
24
|
Requires-Dist: cryptography>=42.0.0
|
|
25
|
+
Requires-Dist: dnspython>=2.0.0
|
|
25
26
|
Requires-Dist: Requests
|
|
26
27
|
Requires-Dist: google-auth>=2.28.0
|
|
27
28
|
Provides-Extra: pymysql
|
|
@@ -400,6 +401,69 @@ conn = connector.connect(
|
|
|
400
401
|
)
|
|
401
402
|
```
|
|
402
403
|
|
|
404
|
+
### Using DNS domain names to identify instances
|
|
405
|
+
|
|
406
|
+
The connector can be configured to use DNS to look up an instance. This would
|
|
407
|
+
allow you to configure your application to connect to a database instance, and
|
|
408
|
+
centrally configure which instance in your DNS zone.
|
|
409
|
+
|
|
410
|
+
#### Configure your DNS Records
|
|
411
|
+
|
|
412
|
+
Add a DNS TXT record for the Cloud SQL instance to a **private** DNS server
|
|
413
|
+
or a private Google Cloud DNS Zone used by your application.
|
|
414
|
+
|
|
415
|
+
> [!NOTE]
|
|
416
|
+
>
|
|
417
|
+
> You are strongly discouraged from adding DNS records for your
|
|
418
|
+
> Cloud SQL instances to a public DNS server. This would allow anyone on the
|
|
419
|
+
> internet to discover the Cloud SQL instance name.
|
|
420
|
+
|
|
421
|
+
For example: suppose you wanted to use the domain name
|
|
422
|
+
`prod-db.mycompany.example.com` to connect to your database instance
|
|
423
|
+
`my-project:region:my-instance`. You would create the following DNS record:
|
|
424
|
+
|
|
425
|
+
* Record type: `TXT`
|
|
426
|
+
* Name: `prod-db.mycompany.example.com` – This is the domain name used by the application
|
|
427
|
+
* Value: `my-project:my-region:my-instance` – This is the Cloud SQL instance connection name
|
|
428
|
+
|
|
429
|
+
#### Configure the connector
|
|
430
|
+
|
|
431
|
+
Configure the connector to resolve DNS names by initializing it with
|
|
432
|
+
`resolver=DnsResolver` and replacing the instance connection name with the DNS
|
|
433
|
+
name in `connector.connect`:
|
|
434
|
+
|
|
435
|
+
```python
|
|
436
|
+
from google.cloud.sql.connector import Connector, DnsResolver
|
|
437
|
+
import pymysql
|
|
438
|
+
import sqlalchemy
|
|
439
|
+
|
|
440
|
+
# helper function to return SQLAlchemy connection pool
|
|
441
|
+
def init_connection_pool(connector: Connector) -> sqlalchemy.engine.Engine:
|
|
442
|
+
# function used to generate database connection
|
|
443
|
+
def getconn() -> pymysql.connections.Connection:
|
|
444
|
+
conn = connector.connect(
|
|
445
|
+
"prod-db.mycompany.example.com", # using DNS name
|
|
446
|
+
"pymysql",
|
|
447
|
+
user="my-user",
|
|
448
|
+
password="my-password",
|
|
449
|
+
db="my-db-name"
|
|
450
|
+
)
|
|
451
|
+
return conn
|
|
452
|
+
|
|
453
|
+
# create connection pool
|
|
454
|
+
pool = sqlalchemy.create_engine(
|
|
455
|
+
"mysql+pymysql://",
|
|
456
|
+
creator=getconn,
|
|
457
|
+
)
|
|
458
|
+
return pool
|
|
459
|
+
|
|
460
|
+
# initialize Cloud SQL Python Connector with `resolver=DnsResolver`
|
|
461
|
+
with Connector(resolver=DnsResolver) as connector:
|
|
462
|
+
# initialize connection pool
|
|
463
|
+
pool = init_connection_pool(connector)
|
|
464
|
+
# ... use SQLAlchemy engine normally
|
|
465
|
+
```
|
|
466
|
+
|
|
403
467
|
### Using the Python Connector with Python Web Frameworks
|
|
404
468
|
|
|
405
469
|
The Python Connector can be used alongside popular Python web frameworks such
|
|
@@ -365,6 +365,69 @@ conn = connector.connect(
|
|
|
365
365
|
)
|
|
366
366
|
```
|
|
367
367
|
|
|
368
|
+
### Using DNS domain names to identify instances
|
|
369
|
+
|
|
370
|
+
The connector can be configured to use DNS to look up an instance. This would
|
|
371
|
+
allow you to configure your application to connect to a database instance, and
|
|
372
|
+
centrally configure which instance in your DNS zone.
|
|
373
|
+
|
|
374
|
+
#### Configure your DNS Records
|
|
375
|
+
|
|
376
|
+
Add a DNS TXT record for the Cloud SQL instance to a **private** DNS server
|
|
377
|
+
or a private Google Cloud DNS Zone used by your application.
|
|
378
|
+
|
|
379
|
+
> [!NOTE]
|
|
380
|
+
>
|
|
381
|
+
> You are strongly discouraged from adding DNS records for your
|
|
382
|
+
> Cloud SQL instances to a public DNS server. This would allow anyone on the
|
|
383
|
+
> internet to discover the Cloud SQL instance name.
|
|
384
|
+
|
|
385
|
+
For example: suppose you wanted to use the domain name
|
|
386
|
+
`prod-db.mycompany.example.com` to connect to your database instance
|
|
387
|
+
`my-project:region:my-instance`. You would create the following DNS record:
|
|
388
|
+
|
|
389
|
+
* Record type: `TXT`
|
|
390
|
+
* Name: `prod-db.mycompany.example.com` – This is the domain name used by the application
|
|
391
|
+
* Value: `my-project:my-region:my-instance` – This is the Cloud SQL instance connection name
|
|
392
|
+
|
|
393
|
+
#### Configure the connector
|
|
394
|
+
|
|
395
|
+
Configure the connector to resolve DNS names by initializing it with
|
|
396
|
+
`resolver=DnsResolver` and replacing the instance connection name with the DNS
|
|
397
|
+
name in `connector.connect`:
|
|
398
|
+
|
|
399
|
+
```python
|
|
400
|
+
from google.cloud.sql.connector import Connector, DnsResolver
|
|
401
|
+
import pymysql
|
|
402
|
+
import sqlalchemy
|
|
403
|
+
|
|
404
|
+
# helper function to return SQLAlchemy connection pool
|
|
405
|
+
def init_connection_pool(connector: Connector) -> sqlalchemy.engine.Engine:
|
|
406
|
+
# function used to generate database connection
|
|
407
|
+
def getconn() -> pymysql.connections.Connection:
|
|
408
|
+
conn = connector.connect(
|
|
409
|
+
"prod-db.mycompany.example.com", # using DNS name
|
|
410
|
+
"pymysql",
|
|
411
|
+
user="my-user",
|
|
412
|
+
password="my-password",
|
|
413
|
+
db="my-db-name"
|
|
414
|
+
)
|
|
415
|
+
return conn
|
|
416
|
+
|
|
417
|
+
# create connection pool
|
|
418
|
+
pool = sqlalchemy.create_engine(
|
|
419
|
+
"mysql+pymysql://",
|
|
420
|
+
creator=getconn,
|
|
421
|
+
)
|
|
422
|
+
return pool
|
|
423
|
+
|
|
424
|
+
# initialize Cloud SQL Python Connector with `resolver=DnsResolver`
|
|
425
|
+
with Connector(resolver=DnsResolver) as connector:
|
|
426
|
+
# initialize connection pool
|
|
427
|
+
pool = init_connection_pool(connector)
|
|
428
|
+
# ... use SQLAlchemy engine normally
|
|
429
|
+
```
|
|
430
|
+
|
|
368
431
|
### Using the Python Connector with Python Web Frameworks
|
|
369
432
|
|
|
370
433
|
The Python Connector can be used alongside popular Python web frameworks such
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cloud-sql-python-connector
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.16.0
|
|
4
4
|
Summary: The Cloud SQL Python Connector is a library that can be used alongside a database driver to allow users with sufficient permissions to connect to a Cloud SQL database without having to manually allowlist IPs or manage SSL certificates.
|
|
5
5
|
Home-page: https://github.com/GoogleCloudPlatform/cloud-sql-python-connector
|
|
6
6
|
Author: Google LLC
|
|
@@ -22,6 +22,7 @@ License-File: LICENSE
|
|
|
22
22
|
Requires-Dist: aiofiles
|
|
23
23
|
Requires-Dist: aiohttp
|
|
24
24
|
Requires-Dist: cryptography>=42.0.0
|
|
25
|
+
Requires-Dist: dnspython>=2.0.0
|
|
25
26
|
Requires-Dist: Requests
|
|
26
27
|
Requires-Dist: google-auth>=2.28.0
|
|
27
28
|
Provides-Extra: pymysql
|
|
@@ -400,6 +401,69 @@ conn = connector.connect(
|
|
|
400
401
|
)
|
|
401
402
|
```
|
|
402
403
|
|
|
404
|
+
### Using DNS domain names to identify instances
|
|
405
|
+
|
|
406
|
+
The connector can be configured to use DNS to look up an instance. This would
|
|
407
|
+
allow you to configure your application to connect to a database instance, and
|
|
408
|
+
centrally configure which instance in your DNS zone.
|
|
409
|
+
|
|
410
|
+
#### Configure your DNS Records
|
|
411
|
+
|
|
412
|
+
Add a DNS TXT record for the Cloud SQL instance to a **private** DNS server
|
|
413
|
+
or a private Google Cloud DNS Zone used by your application.
|
|
414
|
+
|
|
415
|
+
> [!NOTE]
|
|
416
|
+
>
|
|
417
|
+
> You are strongly discouraged from adding DNS records for your
|
|
418
|
+
> Cloud SQL instances to a public DNS server. This would allow anyone on the
|
|
419
|
+
> internet to discover the Cloud SQL instance name.
|
|
420
|
+
|
|
421
|
+
For example: suppose you wanted to use the domain name
|
|
422
|
+
`prod-db.mycompany.example.com` to connect to your database instance
|
|
423
|
+
`my-project:region:my-instance`. You would create the following DNS record:
|
|
424
|
+
|
|
425
|
+
* Record type: `TXT`
|
|
426
|
+
* Name: `prod-db.mycompany.example.com` – This is the domain name used by the application
|
|
427
|
+
* Value: `my-project:my-region:my-instance` – This is the Cloud SQL instance connection name
|
|
428
|
+
|
|
429
|
+
#### Configure the connector
|
|
430
|
+
|
|
431
|
+
Configure the connector to resolve DNS names by initializing it with
|
|
432
|
+
`resolver=DnsResolver` and replacing the instance connection name with the DNS
|
|
433
|
+
name in `connector.connect`:
|
|
434
|
+
|
|
435
|
+
```python
|
|
436
|
+
from google.cloud.sql.connector import Connector, DnsResolver
|
|
437
|
+
import pymysql
|
|
438
|
+
import sqlalchemy
|
|
439
|
+
|
|
440
|
+
# helper function to return SQLAlchemy connection pool
|
|
441
|
+
def init_connection_pool(connector: Connector) -> sqlalchemy.engine.Engine:
|
|
442
|
+
# function used to generate database connection
|
|
443
|
+
def getconn() -> pymysql.connections.Connection:
|
|
444
|
+
conn = connector.connect(
|
|
445
|
+
"prod-db.mycompany.example.com", # using DNS name
|
|
446
|
+
"pymysql",
|
|
447
|
+
user="my-user",
|
|
448
|
+
password="my-password",
|
|
449
|
+
db="my-db-name"
|
|
450
|
+
)
|
|
451
|
+
return conn
|
|
452
|
+
|
|
453
|
+
# create connection pool
|
|
454
|
+
pool = sqlalchemy.create_engine(
|
|
455
|
+
"mysql+pymysql://",
|
|
456
|
+
creator=getconn,
|
|
457
|
+
)
|
|
458
|
+
return pool
|
|
459
|
+
|
|
460
|
+
# initialize Cloud SQL Python Connector with `resolver=DnsResolver`
|
|
461
|
+
with Connector(resolver=DnsResolver) as connector:
|
|
462
|
+
# initialize connection pool
|
|
463
|
+
pool = init_connection_pool(connector)
|
|
464
|
+
# ... use SQLAlchemy engine normally
|
|
465
|
+
```
|
|
466
|
+
|
|
403
467
|
### Using the Python Connector with Python Web Frameworks
|
|
404
468
|
|
|
405
469
|
The Python Connector can be used alongside popular Python web frameworks such
|
|
@@ -24,5 +24,6 @@ google/cloud/sql/connector/pymysql.py
|
|
|
24
24
|
google/cloud/sql/connector/pytds.py
|
|
25
25
|
google/cloud/sql/connector/rate_limiter.py
|
|
26
26
|
google/cloud/sql/connector/refresh_utils.py
|
|
27
|
+
google/cloud/sql/connector/resolver.py
|
|
27
28
|
google/cloud/sql/connector/utils.py
|
|
28
29
|
google/cloud/sql/connector/version.py
|
|
@@ -18,12 +18,16 @@ from google.cloud.sql.connector.connector import Connector
|
|
|
18
18
|
from google.cloud.sql.connector.connector import create_async_connector
|
|
19
19
|
from google.cloud.sql.connector.enums import IPTypes
|
|
20
20
|
from google.cloud.sql.connector.enums import RefreshStrategy
|
|
21
|
+
from google.cloud.sql.connector.resolver import DefaultResolver
|
|
22
|
+
from google.cloud.sql.connector.resolver import DnsResolver
|
|
21
23
|
from google.cloud.sql.connector.version import __version__
|
|
22
24
|
|
|
23
25
|
__all__ = [
|
|
24
26
|
"__version__",
|
|
25
27
|
"create_async_connector",
|
|
26
28
|
"Connector",
|
|
29
|
+
"DefaultResolver",
|
|
30
|
+
"DnsResolver",
|
|
27
31
|
"IPTypes",
|
|
28
32
|
"RefreshStrategy",
|
|
29
33
|
]
|
|
@@ -26,6 +26,7 @@ from google.auth.credentials import TokenState
|
|
|
26
26
|
from google.auth.transport import requests
|
|
27
27
|
|
|
28
28
|
from google.cloud.sql.connector.connection_info import ConnectionInfo
|
|
29
|
+
from google.cloud.sql.connector.connection_name import ConnectionName
|
|
29
30
|
from google.cloud.sql.connector.exceptions import AutoIAMAuthNotSupported
|
|
30
31
|
from google.cloud.sql.connector.refresh_utils import _downscope_credentials
|
|
31
32
|
from google.cloud.sql.connector.refresh_utils import retry_50x
|
|
@@ -128,8 +129,19 @@ class CloudSQLClient:
|
|
|
128
129
|
resp = await self._client.get(url, headers=headers)
|
|
129
130
|
if resp.status >= 500:
|
|
130
131
|
resp = await retry_50x(self._client.get, url, headers=headers)
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
# try to get response json for better error message
|
|
133
|
+
try:
|
|
134
|
+
ret_dict = await resp.json()
|
|
135
|
+
if resp.status >= 400:
|
|
136
|
+
# if detailed error message is in json response, use as error message
|
|
137
|
+
message = ret_dict.get("error", {}).get("message")
|
|
138
|
+
if message:
|
|
139
|
+
resp.reason = message
|
|
140
|
+
# skip, raise_for_status will catch all errors in finally block
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
finally:
|
|
144
|
+
resp.raise_for_status()
|
|
133
145
|
|
|
134
146
|
if ret_dict["region"] != region:
|
|
135
147
|
raise ValueError(
|
|
@@ -198,8 +210,19 @@ class CloudSQLClient:
|
|
|
198
210
|
resp = await self._client.post(url, headers=headers, json=data)
|
|
199
211
|
if resp.status >= 500:
|
|
200
212
|
resp = await retry_50x(self._client.post, url, headers=headers, json=data)
|
|
201
|
-
|
|
202
|
-
|
|
213
|
+
# try to get response json for better error message
|
|
214
|
+
try:
|
|
215
|
+
ret_dict = await resp.json()
|
|
216
|
+
if resp.status >= 400:
|
|
217
|
+
# if detailed error message is in json response, use as error message
|
|
218
|
+
message = ret_dict.get("error", {}).get("message")
|
|
219
|
+
if message:
|
|
220
|
+
resp.reason = message
|
|
221
|
+
# skip, raise_for_status will catch all errors in finally block
|
|
222
|
+
except Exception:
|
|
223
|
+
pass
|
|
224
|
+
finally:
|
|
225
|
+
resp.raise_for_status()
|
|
203
226
|
|
|
204
227
|
ephemeral_cert: str = ret_dict["ephemeralCert"]["cert"]
|
|
205
228
|
|
|
@@ -223,9 +246,7 @@ class CloudSQLClient:
|
|
|
223
246
|
|
|
224
247
|
async def get_connection_info(
|
|
225
248
|
self,
|
|
226
|
-
|
|
227
|
-
region: str,
|
|
228
|
-
instance: str,
|
|
249
|
+
conn_name: ConnectionName,
|
|
229
250
|
keys: asyncio.Future,
|
|
230
251
|
enable_iam_auth: bool,
|
|
231
252
|
) -> ConnectionInfo:
|
|
@@ -233,10 +254,8 @@ class CloudSQLClient:
|
|
|
233
254
|
Admin API.
|
|
234
255
|
|
|
235
256
|
Args:
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
region (str): The region the Cloud SQL instance is located in.
|
|
239
|
-
instance (str): Name of the Cloud SQL instance.
|
|
257
|
+
conn_name (ConnectionName): The Cloud SQL instance's
|
|
258
|
+
connection name.
|
|
240
259
|
keys (asyncio.Future): A future to the client's public-private key
|
|
241
260
|
pair.
|
|
242
261
|
enable_iam_auth (bool): Whether an automatic IAM database
|
|
@@ -256,16 +275,16 @@ class CloudSQLClient:
|
|
|
256
275
|
|
|
257
276
|
metadata_task = asyncio.create_task(
|
|
258
277
|
self._get_metadata(
|
|
259
|
-
project,
|
|
260
|
-
region,
|
|
261
|
-
|
|
278
|
+
conn_name.project,
|
|
279
|
+
conn_name.region,
|
|
280
|
+
conn_name.instance_name,
|
|
262
281
|
)
|
|
263
282
|
)
|
|
264
283
|
|
|
265
284
|
ephemeral_task = asyncio.create_task(
|
|
266
285
|
self._get_ephemeral(
|
|
267
|
-
project,
|
|
268
|
-
|
|
286
|
+
conn_name.project,
|
|
287
|
+
conn_name.instance_name,
|
|
269
288
|
pub_key,
|
|
270
289
|
enable_iam_auth,
|
|
271
290
|
)
|
|
@@ -289,6 +308,7 @@ class CloudSQLClient:
|
|
|
289
308
|
ephemeral_cert, expiration = await ephemeral_task
|
|
290
309
|
|
|
291
310
|
return ConnectionInfo(
|
|
311
|
+
conn_name,
|
|
292
312
|
ephemeral_cert,
|
|
293
313
|
metadata["server_ca_cert"],
|
|
294
314
|
priv_key,
|
|
@@ -21,6 +21,7 @@ from typing import Any, Optional, TYPE_CHECKING
|
|
|
21
21
|
|
|
22
22
|
from aiofiles.tempfile import TemporaryDirectory
|
|
23
23
|
|
|
24
|
+
from google.cloud.sql.connector.connection_name import ConnectionName
|
|
24
25
|
from google.cloud.sql.connector.exceptions import CloudSQLIPTypeError
|
|
25
26
|
from google.cloud.sql.connector.exceptions import TLSVersionError
|
|
26
27
|
from google.cloud.sql.connector.utils import write_to_file
|
|
@@ -38,6 +39,7 @@ class ConnectionInfo:
|
|
|
38
39
|
"""Contains all necessary information to connect securely to the
|
|
39
40
|
server-side Proxy running on a Cloud SQL instance."""
|
|
40
41
|
|
|
42
|
+
conn_name: ConnectionName
|
|
41
43
|
client_cert: str
|
|
42
44
|
server_ca_cert: str
|
|
43
45
|
private_key: bytes
|
|
@@ -31,12 +31,21 @@ class ConnectionName:
|
|
|
31
31
|
project: str
|
|
32
32
|
region: str
|
|
33
33
|
instance_name: str
|
|
34
|
+
domain_name: str = ""
|
|
34
35
|
|
|
35
36
|
def __str__(self) -> str:
|
|
37
|
+
if self.domain_name:
|
|
38
|
+
return f"{self.domain_name} -> {self.project}:{self.region}:{self.instance_name}"
|
|
36
39
|
return f"{self.project}:{self.region}:{self.instance_name}"
|
|
37
40
|
|
|
38
41
|
|
|
39
|
-
def
|
|
42
|
+
def _parse_connection_name(connection_name: str) -> ConnectionName:
|
|
43
|
+
return _parse_connection_name_with_domain_name(connection_name, "")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _parse_connection_name_with_domain_name(
|
|
47
|
+
connection_name: str, domain_name: str
|
|
48
|
+
) -> ConnectionName:
|
|
40
49
|
if CONN_NAME_REGEX.fullmatch(connection_name) is None:
|
|
41
50
|
raise ValueError(
|
|
42
51
|
"Arg `instance_connection_string` must have "
|
|
@@ -48,4 +57,5 @@ def _parse_instance_connection_name(connection_name: str) -> ConnectionName:
|
|
|
48
57
|
connection_name_split[1],
|
|
49
58
|
connection_name_split[3],
|
|
50
59
|
connection_name_split[4],
|
|
60
|
+
domain_name,
|
|
51
61
|
)
|
|
@@ -19,9 +19,10 @@ from __future__ import annotations
|
|
|
19
19
|
import asyncio
|
|
20
20
|
from functools import partial
|
|
21
21
|
import logging
|
|
22
|
+
import os
|
|
22
23
|
from threading import Thread
|
|
23
24
|
from types import TracebackType
|
|
24
|
-
from typing import Any, Optional,
|
|
25
|
+
from typing import Any, Optional, Union
|
|
25
26
|
|
|
26
27
|
import google.auth
|
|
27
28
|
from google.auth.credentials import Credentials
|
|
@@ -37,6 +38,8 @@ from google.cloud.sql.connector.lazy import LazyRefreshCache
|
|
|
37
38
|
import google.cloud.sql.connector.pg8000 as pg8000
|
|
38
39
|
import google.cloud.sql.connector.pymysql as pymysql
|
|
39
40
|
import google.cloud.sql.connector.pytds as pytds
|
|
41
|
+
from google.cloud.sql.connector.resolver import DefaultResolver
|
|
42
|
+
from google.cloud.sql.connector.resolver import DnsResolver
|
|
40
43
|
from google.cloud.sql.connector.utils import format_database_user
|
|
41
44
|
from google.cloud.sql.connector.utils import generate_keys
|
|
42
45
|
|
|
@@ -63,6 +66,7 @@ class Connector:
|
|
|
63
66
|
user_agent: Optional[str] = None,
|
|
64
67
|
universe_domain: Optional[str] = None,
|
|
65
68
|
refresh_strategy: str | RefreshStrategy = RefreshStrategy.BACKGROUND,
|
|
69
|
+
resolver: type[DefaultResolver] | type[DnsResolver] = DefaultResolver,
|
|
66
70
|
) -> None:
|
|
67
71
|
"""Initializes a Connector instance.
|
|
68
72
|
|
|
@@ -104,6 +108,12 @@ class Connector:
|
|
|
104
108
|
of the following: RefreshStrategy.LAZY ("LAZY") or
|
|
105
109
|
RefreshStrategy.BACKGROUND ("BACKGROUND").
|
|
106
110
|
Default: RefreshStrategy.BACKGROUND
|
|
111
|
+
|
|
112
|
+
resolver (DefaultResolver | DnsResolver): The class name of the
|
|
113
|
+
resolver to use for resolving the Cloud SQL instance connection
|
|
114
|
+
name. To resolve a DNS record to an instance connection name, use
|
|
115
|
+
DnsResolver.
|
|
116
|
+
Default: DefaultResolver
|
|
107
117
|
"""
|
|
108
118
|
# if refresh_strategy is str, convert to RefreshStrategy enum
|
|
109
119
|
if isinstance(refresh_strategy, str):
|
|
@@ -157,11 +167,16 @@ class Connector:
|
|
|
157
167
|
self._enable_iam_auth = enable_iam_auth
|
|
158
168
|
self._quota_project = quota_project
|
|
159
169
|
self._user_agent = user_agent
|
|
170
|
+
self._resolver = resolver()
|
|
160
171
|
# if ip_type is str, convert to IPTypes enum
|
|
161
172
|
if isinstance(ip_type, str):
|
|
162
173
|
ip_type = IPTypes._from_str(ip_type)
|
|
163
174
|
self._ip_type = ip_type
|
|
164
|
-
|
|
175
|
+
# check for universe domain arg and then env var
|
|
176
|
+
if universe_domain:
|
|
177
|
+
self._universe_domain = universe_domain
|
|
178
|
+
else:
|
|
179
|
+
self._universe_domain = os.environ.get("GOOGLE_CLOUD_UNIVERSE_DOMAIN") # type: ignore
|
|
165
180
|
# construct service endpoint for Cloud SQL Admin API calls
|
|
166
181
|
if not sqladmin_api_endpoint:
|
|
167
182
|
self._sqladmin_api_endpoint = (
|
|
@@ -269,31 +284,28 @@ class Connector:
|
|
|
269
284
|
if (instance_connection_string, enable_iam_auth) in self._cache:
|
|
270
285
|
cache = self._cache[(instance_connection_string, enable_iam_auth)]
|
|
271
286
|
else:
|
|
287
|
+
conn_name = await self._resolver.resolve(instance_connection_string)
|
|
272
288
|
if self._refresh_strategy == RefreshStrategy.LAZY:
|
|
273
289
|
logger.debug(
|
|
274
|
-
f"['{
|
|
275
|
-
" to lazy refresh"
|
|
290
|
+
f"['{conn_name}']: Refresh strategy is set to lazy refresh"
|
|
276
291
|
)
|
|
277
292
|
cache = LazyRefreshCache(
|
|
278
|
-
|
|
293
|
+
conn_name,
|
|
279
294
|
self._client,
|
|
280
295
|
self._keys,
|
|
281
296
|
enable_iam_auth,
|
|
282
297
|
)
|
|
283
298
|
else:
|
|
284
299
|
logger.debug(
|
|
285
|
-
f"['{
|
|
286
|
-
" to backgound refresh"
|
|
300
|
+
f"['{conn_name}']: Refresh strategy is set to backgound refresh"
|
|
287
301
|
)
|
|
288
302
|
cache = RefreshAheadCache(
|
|
289
|
-
|
|
303
|
+
conn_name,
|
|
290
304
|
self._client,
|
|
291
305
|
self._keys,
|
|
292
306
|
enable_iam_auth,
|
|
293
307
|
)
|
|
294
|
-
logger.debug(
|
|
295
|
-
f"['{instance_connection_string}']: Connection info added to cache"
|
|
296
|
-
)
|
|
308
|
+
logger.debug(f"['{conn_name}']: Connection info added to cache")
|
|
297
309
|
self._cache[(instance_connection_string, enable_iam_auth)] = cache
|
|
298
310
|
|
|
299
311
|
connect_func = {
|
|
@@ -332,9 +344,7 @@ class Connector:
|
|
|
332
344
|
# the cache and re-raise the error
|
|
333
345
|
await self._remove_cached(instance_connection_string, enable_iam_auth)
|
|
334
346
|
raise
|
|
335
|
-
logger.debug(
|
|
336
|
-
f"['{instance_connection_string}']: Connecting to {ip_address}:3307"
|
|
337
|
-
)
|
|
347
|
+
logger.debug(f"['{conn_info.conn_name}']: Connecting to {ip_address}:3307")
|
|
338
348
|
# format `user` param for automatic IAM database authn
|
|
339
349
|
if enable_iam_auth:
|
|
340
350
|
formatted_user = format_database_user(
|
|
@@ -386,7 +396,7 @@ class Connector:
|
|
|
386
396
|
|
|
387
397
|
def __exit__(
|
|
388
398
|
self,
|
|
389
|
-
exc_type: Optional[
|
|
399
|
+
exc_type: Optional[type[BaseException]],
|
|
390
400
|
exc_val: Optional[BaseException],
|
|
391
401
|
exc_tb: Optional[TracebackType],
|
|
392
402
|
) -> None:
|
|
@@ -399,7 +409,7 @@ class Connector:
|
|
|
399
409
|
|
|
400
410
|
async def __aexit__(
|
|
401
411
|
self,
|
|
402
|
-
exc_type: Optional[
|
|
412
|
+
exc_type: Optional[type[BaseException]],
|
|
403
413
|
exc_val: Optional[BaseException],
|
|
404
414
|
exc_tb: Optional[TracebackType],
|
|
405
415
|
) -> None:
|
|
@@ -19,9 +19,10 @@ from enum import Enum
|
|
|
19
19
|
from google.cloud.sql.connector.exceptions import IncompatibleDriverError
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
# TODO: Replace Enum with StrEnum when Python 3.11 is minimum supported version
|
|
22
23
|
class RefreshStrategy(Enum):
|
|
23
|
-
LAZY
|
|
24
|
-
BACKGROUND
|
|
24
|
+
LAZY = "LAZY"
|
|
25
|
+
BACKGROUND = "BACKGROUND"
|
|
25
26
|
|
|
26
27
|
@classmethod
|
|
27
28
|
def _missing_(cls, value: object) -> None:
|
|
@@ -37,9 +38,9 @@ class RefreshStrategy(Enum):
|
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
class IPTypes(Enum):
|
|
40
|
-
PUBLIC
|
|
41
|
-
PRIVATE
|
|
42
|
-
PSC
|
|
41
|
+
PUBLIC = "PRIMARY"
|
|
42
|
+
PRIVATE = "PRIVATE"
|
|
43
|
+
PSC = "PSC"
|
|
43
44
|
|
|
44
45
|
@classmethod
|
|
45
46
|
def _missing_(cls, value: object) -> None:
|
|
@@ -70,3 +70,10 @@ class IncompatibleDriverError(Exception):
|
|
|
70
70
|
Exception to be raised when the database driver given is for the wrong
|
|
71
71
|
database engine. (i.e. asyncpg for a MySQL database)
|
|
72
72
|
"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class DnsResolutionError(Exception):
|
|
76
|
+
"""
|
|
77
|
+
Exception to be raised when an instance connection name can not be resolved
|
|
78
|
+
from a DNS record.
|
|
79
|
+
"""
|
|
@@ -22,11 +22,9 @@ from datetime import timedelta
|
|
|
22
22
|
from datetime import timezone
|
|
23
23
|
import logging
|
|
24
24
|
|
|
25
|
-
import aiohttp
|
|
26
|
-
|
|
27
25
|
from google.cloud.sql.connector.client import CloudSQLClient
|
|
28
26
|
from google.cloud.sql.connector.connection_info import ConnectionInfo
|
|
29
|
-
from google.cloud.sql.connector.connection_name import
|
|
27
|
+
from google.cloud.sql.connector.connection_name import ConnectionName
|
|
30
28
|
from google.cloud.sql.connector.exceptions import RefreshNotValidError
|
|
31
29
|
from google.cloud.sql.connector.rate_limiter import AsyncRateLimiter
|
|
32
30
|
from google.cloud.sql.connector.refresh_utils import _is_valid
|
|
@@ -47,7 +45,7 @@ class RefreshAheadCache:
|
|
|
47
45
|
|
|
48
46
|
def __init__(
|
|
49
47
|
self,
|
|
50
|
-
|
|
48
|
+
conn_name: ConnectionName,
|
|
51
49
|
client: CloudSQLClient,
|
|
52
50
|
keys: asyncio.Future,
|
|
53
51
|
enable_iam_auth: bool = False,
|
|
@@ -55,8 +53,8 @@ class RefreshAheadCache:
|
|
|
55
53
|
"""Initializes a RefreshAheadCache instance.
|
|
56
54
|
|
|
57
55
|
Args:
|
|
58
|
-
|
|
59
|
-
connection
|
|
56
|
+
conn_name (ConnectionName): The Cloud SQL instance's
|
|
57
|
+
connection name.
|
|
60
58
|
client (CloudSQLClient): The Cloud SQL Client instance.
|
|
61
59
|
keys (asyncio.Future): A future to the client's public-private key
|
|
62
60
|
pair.
|
|
@@ -64,13 +62,6 @@ class RefreshAheadCache:
|
|
|
64
62
|
(Postgres and MySQL) as the default authentication method for all
|
|
65
63
|
connections.
|
|
66
64
|
"""
|
|
67
|
-
# validate and parse instance connection name
|
|
68
|
-
conn_name = _parse_instance_connection_name(instance_connection_string)
|
|
69
|
-
self._project, self._region, self._instance = (
|
|
70
|
-
conn_name.project,
|
|
71
|
-
conn_name.region,
|
|
72
|
-
conn_name.instance_name,
|
|
73
|
-
)
|
|
74
65
|
self._conn_name = conn_name
|
|
75
66
|
|
|
76
67
|
self._enable_iam_auth = enable_iam_auth
|
|
@@ -108,35 +99,24 @@ class RefreshAheadCache:
|
|
|
108
99
|
"""
|
|
109
100
|
self._refresh_in_progress.set()
|
|
110
101
|
logger.debug(
|
|
111
|
-
f"['{self._conn_name}']: Connection info refresh
|
|
102
|
+
f"['{self._conn_name}']: Connection info refresh operation started"
|
|
112
103
|
)
|
|
113
104
|
|
|
114
105
|
try:
|
|
115
106
|
await self._refresh_rate_limiter.acquire()
|
|
116
107
|
connection_info = await self._client.get_connection_info(
|
|
117
|
-
self.
|
|
118
|
-
self._region,
|
|
119
|
-
self._instance,
|
|
108
|
+
self._conn_name,
|
|
120
109
|
self._keys,
|
|
121
110
|
self._enable_iam_auth,
|
|
122
111
|
)
|
|
123
112
|
logger.debug(
|
|
124
|
-
f"['{self._conn_name}']: Connection info
|
|
113
|
+
f"['{self._conn_name}']: Connection info refresh operation complete"
|
|
125
114
|
)
|
|
126
115
|
logger.debug(
|
|
127
116
|
f"['{self._conn_name}']: Current certificate "
|
|
128
117
|
f"expiration = {connection_info.expiration.isoformat()}"
|
|
129
118
|
)
|
|
130
119
|
|
|
131
|
-
except aiohttp.ClientResponseError as e:
|
|
132
|
-
logger.debug(
|
|
133
|
-
f"['{self._conn_name}']: Connection info "
|
|
134
|
-
f"refresh operation failed: {str(e)}"
|
|
135
|
-
)
|
|
136
|
-
if e.status == 403:
|
|
137
|
-
e.message = "Forbidden: Authenticated IAM principal does not seem authorized to make API request. Verify 'Cloud SQL Admin API' is enabled within your GCP project and 'Cloud SQL Client' role has been granted to IAM principal."
|
|
138
|
-
raise
|
|
139
|
-
|
|
140
120
|
except Exception as e:
|
|
141
121
|
logger.debug(
|
|
142
122
|
f"['{self._conn_name}']: Connection info "
|
|
@@ -21,7 +21,7 @@ from typing import Optional
|
|
|
21
21
|
|
|
22
22
|
from google.cloud.sql.connector.client import CloudSQLClient
|
|
23
23
|
from google.cloud.sql.connector.connection_info import ConnectionInfo
|
|
24
|
-
from google.cloud.sql.connector.connection_name import
|
|
24
|
+
from google.cloud.sql.connector.connection_name import ConnectionName
|
|
25
25
|
from google.cloud.sql.connector.refresh_utils import _refresh_buffer
|
|
26
26
|
|
|
27
27
|
logger = logging.getLogger(name=__name__)
|
|
@@ -38,7 +38,7 @@ class LazyRefreshCache:
|
|
|
38
38
|
|
|
39
39
|
def __init__(
|
|
40
40
|
self,
|
|
41
|
-
|
|
41
|
+
conn_name: ConnectionName,
|
|
42
42
|
client: CloudSQLClient,
|
|
43
43
|
keys: asyncio.Future,
|
|
44
44
|
enable_iam_auth: bool = False,
|
|
@@ -46,8 +46,8 @@ class LazyRefreshCache:
|
|
|
46
46
|
"""Initializes a LazyRefreshCache instance.
|
|
47
47
|
|
|
48
48
|
Args:
|
|
49
|
-
|
|
50
|
-
connection
|
|
49
|
+
conn_name (ConnectionName): The Cloud SQL instance's
|
|
50
|
+
connection name.
|
|
51
51
|
client (CloudSQLClient): The Cloud SQL Client instance.
|
|
52
52
|
keys (asyncio.Future): A future to the client's public-private key
|
|
53
53
|
pair.
|
|
@@ -55,15 +55,7 @@ class LazyRefreshCache:
|
|
|
55
55
|
(Postgres and MySQL) as the default authentication method for all
|
|
56
56
|
connections.
|
|
57
57
|
"""
|
|
58
|
-
# validate and parse instance connection name
|
|
59
|
-
conn_name = _parse_instance_connection_name(instance_connection_string)
|
|
60
|
-
self._project, self._region, self._instance = (
|
|
61
|
-
conn_name.project,
|
|
62
|
-
conn_name.region,
|
|
63
|
-
conn_name.instance_name,
|
|
64
|
-
)
|
|
65
58
|
self._conn_name = conn_name
|
|
66
|
-
|
|
67
59
|
self._enable_iam_auth = enable_iam_auth
|
|
68
60
|
self._keys = keys
|
|
69
61
|
self._client = client
|
|
@@ -103,9 +95,7 @@ class LazyRefreshCache:
|
|
|
103
95
|
)
|
|
104
96
|
try:
|
|
105
97
|
conn_info = await self._client.get_connection_info(
|
|
106
|
-
self.
|
|
107
|
-
self._region,
|
|
108
|
-
self._instance,
|
|
98
|
+
self._conn_name,
|
|
109
99
|
self._keys,
|
|
110
100
|
self._enable_iam_auth,
|
|
111
101
|
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Copyright 2024 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import dns.asyncresolver
|
|
16
|
+
|
|
17
|
+
from google.cloud.sql.connector.connection_name import (
|
|
18
|
+
_parse_connection_name_with_domain_name,
|
|
19
|
+
)
|
|
20
|
+
from google.cloud.sql.connector.connection_name import _parse_connection_name
|
|
21
|
+
from google.cloud.sql.connector.connection_name import ConnectionName
|
|
22
|
+
from google.cloud.sql.connector.exceptions import DnsResolutionError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DefaultResolver:
|
|
26
|
+
"""DefaultResolver simply validates and parses instance connection name."""
|
|
27
|
+
|
|
28
|
+
async def resolve(self, connection_name: str) -> ConnectionName:
|
|
29
|
+
return _parse_connection_name(connection_name)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DnsResolver(dns.asyncresolver.Resolver):
|
|
33
|
+
"""
|
|
34
|
+
DnsResolver resolves domain names into instance connection names using
|
|
35
|
+
TXT records in DNS.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
async def resolve(self, dns: str) -> ConnectionName: # type: ignore
|
|
39
|
+
try:
|
|
40
|
+
conn_name = _parse_connection_name(dns)
|
|
41
|
+
except ValueError:
|
|
42
|
+
# The connection name was not project:region:instance format.
|
|
43
|
+
# Attempt to query a TXT record to get connection name.
|
|
44
|
+
conn_name = await self.query_dns(dns)
|
|
45
|
+
return conn_name
|
|
46
|
+
|
|
47
|
+
async def query_dns(self, dns: str) -> ConnectionName:
|
|
48
|
+
try:
|
|
49
|
+
# Attempt to query the TXT records.
|
|
50
|
+
records = await super().resolve(dns, "TXT", raise_on_no_answer=True)
|
|
51
|
+
# Sort the TXT record values alphabetically, strip quotes as record
|
|
52
|
+
# values can be returned as raw strings
|
|
53
|
+
rdata = [record.to_text().strip('"') for record in records]
|
|
54
|
+
rdata.sort()
|
|
55
|
+
# Attempt to parse records, returning the first valid record.
|
|
56
|
+
for record in rdata:
|
|
57
|
+
try:
|
|
58
|
+
conn_name = _parse_connection_name_with_domain_name(record, dns)
|
|
59
|
+
return conn_name
|
|
60
|
+
except Exception:
|
|
61
|
+
continue
|
|
62
|
+
# If all records failed to parse, throw error
|
|
63
|
+
raise DnsResolutionError(
|
|
64
|
+
f"Unable to parse TXT record for `{dns}` -> `{rdata[0]}`"
|
|
65
|
+
)
|
|
66
|
+
# Don't override above DnsResolutionError
|
|
67
|
+
except DnsResolutionError:
|
|
68
|
+
raise
|
|
69
|
+
except Exception as e:
|
|
70
|
+
raise DnsResolutionError(f"Unable to resolve TXT record for `{dns}`") from e
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|