cloud-sql-python-connector 1.13.0__py2.py3-none-any.whl → 1.15.0__py2.py3-none-any.whl
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.13.0.dist-info → cloud_sql_python_connector-1.15.0.dist-info}/METADATA +152 -18
- cloud_sql_python_connector-1.15.0.dist-info/RECORD +24 -0
- {cloud_sql_python_connector-1.13.0.dist-info → cloud_sql_python_connector-1.15.0.dist-info}/WHEEL +1 -1
- google/cloud/sql/connector/__init__.py +4 -0
- google/cloud/sql/connector/asyncpg.py +1 -0
- google/cloud/sql/connector/client.py +40 -20
- google/cloud/sql/connector/connection_info.py +4 -2
- google/cloud/sql/connector/connection_name.py +61 -0
- google/cloud/sql/connector/connector.py +20 -15
- google/cloud/sql/connector/exceptions.py +7 -0
- google/cloud/sql/connector/instance.py +15 -52
- google/cloud/sql/connector/lazy.py +11 -19
- google/cloud/sql/connector/pg8000.py +1 -0
- google/cloud/sql/connector/pymysql.py +1 -0
- google/cloud/sql/connector/pytds.py +1 -0
- google/cloud/sql/connector/refresh_utils.py +3 -3
- google/cloud/sql/connector/resolver.py +70 -0
- google/cloud/sql/connector/utils.py +2 -4
- google/cloud/sql/connector/version.py +1 -1
- cloud_sql_python_connector-1.13.0.dist-info/RECORD +0 -22
- {cloud_sql_python_connector-1.13.0.dist-info → cloud_sql_python_connector-1.15.0.dist-info}/LICENSE +0 -0
- {cloud_sql_python_connector-1.13.0.dist-info → cloud_sql_python_connector-1.15.0.dist-info}/top_level.txt +0 -0
{cloud_sql_python_connector-1.13.0.dist-info → cloud_sql_python_connector-1.15.0.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cloud-sql-python-connector
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.15.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
|
|
@@ -21,17 +21,18 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: aiofiles
|
|
23
23
|
Requires-Dist: aiohttp
|
|
24
|
-
Requires-Dist: cryptography
|
|
24
|
+
Requires-Dist: cryptography>=42.0.0
|
|
25
|
+
Requires-Dist: dnspython>=2.0.0
|
|
25
26
|
Requires-Dist: Requests
|
|
26
|
-
Requires-Dist: google-auth
|
|
27
|
+
Requires-Dist: google-auth>=2.28.0
|
|
27
28
|
Provides-Extra: asyncpg
|
|
28
|
-
Requires-Dist: asyncpg
|
|
29
|
+
Requires-Dist: asyncpg>=0.30.0; extra == "asyncpg"
|
|
29
30
|
Provides-Extra: pg8000
|
|
30
|
-
Requires-Dist: pg8000
|
|
31
|
+
Requires-Dist: pg8000>=1.31.1; extra == "pg8000"
|
|
31
32
|
Provides-Extra: pymysql
|
|
32
|
-
Requires-Dist: PyMySQL
|
|
33
|
+
Requires-Dist: PyMySQL>=1.1.0; extra == "pymysql"
|
|
33
34
|
Provides-Extra: pytds
|
|
34
|
-
Requires-Dist: python-tds
|
|
35
|
+
Requires-Dist: python-tds>=1.15.0; extra == "pytds"
|
|
35
36
|
|
|
36
37
|
<p align="center">
|
|
37
38
|
<a href="https://cloud.google.com/blog/topics/developers-practitioners/how-connect-cloud-sql-using-python-easy-way">
|
|
@@ -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
|
|
@@ -537,6 +601,42 @@ The `create_async_connector` allows all the same input arguments as the
|
|
|
537
601
|
Once a `Connector` object is returned by `create_async_connector` you can call
|
|
538
602
|
its `connect_async` method, just as you would the `connect` method:
|
|
539
603
|
|
|
604
|
+
#### Asyncpg Connection Pool
|
|
605
|
+
|
|
606
|
+
```python
|
|
607
|
+
import asyncpg
|
|
608
|
+
from google.cloud.sql.connector import Connector, create_async_connector
|
|
609
|
+
|
|
610
|
+
async def main():
|
|
611
|
+
# initialize Connector object for connections to Cloud SQL
|
|
612
|
+
connector = create_async_connector()
|
|
613
|
+
|
|
614
|
+
# creation function to generate asyncpg connections as the 'connect' arg
|
|
615
|
+
async def getconn(instance_connection_name, **kwargs) -> asyncpg.Connection:
|
|
616
|
+
return await connector.connect_async(
|
|
617
|
+
instance_connection_name,
|
|
618
|
+
"asyncpg",
|
|
619
|
+
user="my-user",
|
|
620
|
+
password="my-password",
|
|
621
|
+
db="my-db",
|
|
622
|
+
**kwargs, # ... additional asyncpg args
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# initialize connection pool
|
|
626
|
+
pool = await asyncpg.create_pool(
|
|
627
|
+
"my-project:my-region:my-instance", connect=getconn
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
# acquire connection and query Cloud SQL database
|
|
631
|
+
async with pool.acquire() as conn:
|
|
632
|
+
res = await conn.fetch("SELECT NOW()")
|
|
633
|
+
|
|
634
|
+
# close Connector
|
|
635
|
+
await connector.close_async()
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
#### SQLAlchemy Async Engine
|
|
639
|
+
|
|
540
640
|
```python
|
|
541
641
|
import asyncpg
|
|
542
642
|
|
|
@@ -546,7 +646,7 @@ from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
|
|
|
546
646
|
from google.cloud.sql.connector import Connector, create_async_connector
|
|
547
647
|
|
|
548
648
|
async def init_connection_pool(connector: Connector) -> AsyncEngine:
|
|
549
|
-
#
|
|
649
|
+
# creation function to generate asyncpg connections as 'async_creator' arg
|
|
550
650
|
async def getconn() -> asyncpg.Connection:
|
|
551
651
|
conn: asyncpg.Connection = await connector.connect_async(
|
|
552
652
|
"project:region:instance", # Cloud SQL instance connection name
|
|
@@ -599,6 +699,40 @@ calls to `connector.close_async()` to cleanup resources.
|
|
|
599
699
|
> This alternative requires that the running event loop be
|
|
600
700
|
> passed in as the `loop` argument to `Connector()`.
|
|
601
701
|
|
|
702
|
+
#### Asyncpg Connection Pool
|
|
703
|
+
|
|
704
|
+
```python
|
|
705
|
+
import asyncpg
|
|
706
|
+
from google.cloud.sql.connector import Connector, create_async_connector
|
|
707
|
+
|
|
708
|
+
async def main():
|
|
709
|
+
# initialize Connector object for connections to Cloud SQL
|
|
710
|
+
loop = asyncio.get_running_loop()
|
|
711
|
+
async with Connector(loop=loop) as connector:
|
|
712
|
+
|
|
713
|
+
# creation function to generate asyncpg connections as the 'connect' arg
|
|
714
|
+
async def getconn(instance_connection_name, **kwargs) -> asyncpg.Connection:
|
|
715
|
+
return await connector.connect_async(
|
|
716
|
+
instance_connection_name,
|
|
717
|
+
"asyncpg",
|
|
718
|
+
user="my-user",
|
|
719
|
+
password="my-password",
|
|
720
|
+
db="my-db",
|
|
721
|
+
**kwargs, # ... additional asyncpg args
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
# create connection pool
|
|
725
|
+
pool = await asyncpg.create_pool(
|
|
726
|
+
"my-project:my-region:my-instance", connect=getconn
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
# acquire connection and query Cloud SQL database
|
|
730
|
+
async with pool.acquire() as conn:
|
|
731
|
+
res = await conn.fetch("SELECT NOW()")
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
#### SQLAlchemy Async Engine
|
|
735
|
+
|
|
602
736
|
```python
|
|
603
737
|
import asyncio
|
|
604
738
|
import asyncpg
|
|
@@ -609,17 +743,17 @@ from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
|
|
|
609
743
|
from google.cloud.sql.connector import Connector
|
|
610
744
|
|
|
611
745
|
async def init_connection_pool(connector: Connector) -> AsyncEngine:
|
|
612
|
-
#
|
|
746
|
+
# creation function to generate asyncpg connections as 'async_creator' arg
|
|
613
747
|
async def getconn() -> asyncpg.Connection:
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
748
|
+
conn: asyncpg.Connection = await connector.connect_async(
|
|
749
|
+
"project:region:instance", # Cloud SQL instance connection name
|
|
750
|
+
"asyncpg",
|
|
751
|
+
user="my-user",
|
|
752
|
+
password="my-password",
|
|
753
|
+
db="my-db-name"
|
|
754
|
+
# ... additional database driver args
|
|
755
|
+
)
|
|
756
|
+
return conn
|
|
623
757
|
|
|
624
758
|
# The Cloud SQL Python Connector can be used along with SQLAlchemy using the
|
|
625
759
|
# 'async_creator' argument to 'create_async_engine'
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
google/cloud/sql/connector/__init__.py,sha256=kSGB2q6hQwkFLD1eI7RjY4BvPLvg2FD3wfPBmbnsjDE,1149
|
|
2
|
+
google/cloud/sql/connector/asyncpg.py,sha256=HUOB0rRhr2-zMbygDtdWTDNlYqEPkN0ADqCIpM3waDc,1839
|
|
3
|
+
google/cloud/sql/connector/client.py,sha256=ocUN1nrgZyJRnxgh6Ym3w0c_qKb8hqKCmR1OiJ2KUpM,12622
|
|
4
|
+
google/cloud/sql/connector/connection_info.py,sha256=cFNOeHJ0lQODf7q5Hc25LXsNWzWgzYcFMrBuNdZ1y5k,4430
|
|
5
|
+
google/cloud/sql/connector/connection_name.py,sha256=4F5XhgmSWGEeF-3TJk2E61YPbxMAecPGw56nPTZmQ2o,2067
|
|
6
|
+
google/cloud/sql/connector/connector.py,sha256=9t2iz_Tx1NluGgaM8JlZUfb7HWHgsWjmLnAL8WvKp4E,21157
|
|
7
|
+
google/cloud/sql/connector/enums.py,sha256=44tfMm8D_t0ls6GfKwiQ6YkslWM21PZR40RhJPWntkc,2882
|
|
8
|
+
google/cloud/sql/connector/exceptions.py,sha256=eyflYggkKZx3t944FTPqQrX-R75lZgtpOwC4uVLPhG0,1823
|
|
9
|
+
google/cloud/sql/connector/instance.py,sha256=nRzhE7t_M6AhjiYvEZHOjkBHx3d9wGRCC6RekgDApFI,8379
|
|
10
|
+
google/cloud/sql/connector/lazy.py,sha256=p1wLte49xE_aHiY5shjMbxwytgW-ycS0Hd67C34gaDY,4631
|
|
11
|
+
google/cloud/sql/connector/pg8000.py,sha256=gZisjfXP4rNIjyxOQS75fvr0Xe6eLrFo4mIhVjoMLTw,1815
|
|
12
|
+
google/cloud/sql/connector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
google/cloud/sql/connector/pymysql.py,sha256=82DwSJUhZHH2AW-vO7qhkljyShwj6Kmd17WCbOGZH74,2106
|
|
14
|
+
google/cloud/sql/connector/pytds.py,sha256=h-1pDhqLDkhviKBBOJ29gnIeYHKWw7Dtj91VLu1iNUI,2453
|
|
15
|
+
google/cloud/sql/connector/rate_limiter.py,sha256=bPTupfM4f5QRSCmCAWMqjQylEB7bUM-50plVBU3W2CA,2993
|
|
16
|
+
google/cloud/sql/connector/refresh_utils.py,sha256=HfuuLdlqg2IxusB4T3BMj-BJlGsXtHsrCX_Hp-eJ_j0,5063
|
|
17
|
+
google/cloud/sql/connector/resolver.py,sha256=cNDJMhmS_0CIAmz8kCABKHJNLNm1wiwIVoPEN-gWzzY,2850
|
|
18
|
+
google/cloud/sql/connector/utils.py,sha256=7g1fX-JsZc7xOgTuxrAiroFHQIQk_BEJZNYaKOinwQY,3508
|
|
19
|
+
google/cloud/sql/connector/version.py,sha256=l3R3usCjpr4Wwp3w2gpWFcrGmsLi8Y1gAAvZC15RLv0,598
|
|
20
|
+
cloud_sql_python_connector-1.15.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
21
|
+
cloud_sql_python_connector-1.15.0.dist-info/METADATA,sha256=Qszn2k_QqcVlX92c_1A-7MurYUoojTIMbx5M13aPZnI,30314
|
|
22
|
+
cloud_sql_python_connector-1.15.0.dist-info/WHEEL,sha256=OpXWERl2xLPRHTvd2ZXo_iluPEQd8uSbYkJ53NAER_Y,109
|
|
23
|
+
cloud_sql_python_connector-1.15.0.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
|
|
24
|
+
cloud_sql_python_connector-1.15.0.dist-info/RECORD,,
|
|
@@ -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
|
]
|
|
@@ -17,7 +17,7 @@ from __future__ import annotations
|
|
|
17
17
|
import asyncio
|
|
18
18
|
import datetime
|
|
19
19
|
import logging
|
|
20
|
-
from typing import Any,
|
|
20
|
+
from typing import Any, Optional, TYPE_CHECKING
|
|
21
21
|
|
|
22
22
|
import aiohttp
|
|
23
23
|
from cryptography.hazmat.backends import default_backend
|
|
@@ -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
|
|
@@ -98,7 +99,7 @@ class CloudSQLClient:
|
|
|
98
99
|
project: str,
|
|
99
100
|
region: str,
|
|
100
101
|
instance: str,
|
|
101
|
-
) ->
|
|
102
|
+
) -> dict[str, Any]:
|
|
102
103
|
"""Requests metadata from the Cloud SQL Instance
|
|
103
104
|
and returns a dictionary containing the IP addresses and certificate
|
|
104
105
|
authority of the Cloud SQL Instance.
|
|
@@ -113,7 +114,7 @@ class CloudSQLClient:
|
|
|
113
114
|
:type instance: str
|
|
114
115
|
:param instance: A string representing the name of the instance.
|
|
115
116
|
|
|
116
|
-
:rtype:
|
|
117
|
+
:rtype: dict[str: Union[dict, str]]
|
|
117
118
|
:returns: Returns a dictionary containing a dictionary of all IP
|
|
118
119
|
addresses and their type and a string representing the
|
|
119
120
|
certificate authority.
|
|
@@ -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(
|
|
@@ -161,7 +173,7 @@ class CloudSQLClient:
|
|
|
161
173
|
instance: str,
|
|
162
174
|
pub_key: str,
|
|
163
175
|
enable_iam_auth: bool = False,
|
|
164
|
-
) ->
|
|
176
|
+
) -> tuple[str, datetime.datetime]:
|
|
165
177
|
"""Asynchronously requests an ephemeral certificate from the Cloud SQL Instance.
|
|
166
178
|
|
|
167
179
|
:type project: str
|
|
@@ -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,
|
|
@@ -17,10 +17,11 @@ from __future__ import annotations
|
|
|
17
17
|
from dataclasses import dataclass
|
|
18
18
|
import logging
|
|
19
19
|
import ssl
|
|
20
|
-
from typing import Any,
|
|
20
|
+
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,10 +39,11 @@ 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
|
|
44
|
-
ip_addrs:
|
|
46
|
+
ip_addrs: dict[str, Any]
|
|
45
47
|
database_version: str
|
|
46
48
|
expiration: datetime.datetime
|
|
47
49
|
context: Optional[ssl.SSLContext] = None
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
from dataclasses import dataclass
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
# Instance connection name is the format <PROJECT>:<REGION>:<INSTANCE_NAME>
|
|
19
|
+
# Additionally, we have to support legacy "domain-scoped" projects
|
|
20
|
+
# (e.g. "google.com:PROJECT")
|
|
21
|
+
CONN_NAME_REGEX = re.compile(("([^:]+(:[^:]+)?):([^:]+):([^:]+)"))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ConnectionName:
|
|
26
|
+
"""ConnectionName represents a Cloud SQL instance's "instance connection name".
|
|
27
|
+
|
|
28
|
+
Takes the format "<PROJECT>:<REGION>:<INSTANCE_NAME>".
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
project: str
|
|
32
|
+
region: str
|
|
33
|
+
instance_name: str
|
|
34
|
+
domain_name: str = ""
|
|
35
|
+
|
|
36
|
+
def __str__(self) -> str:
|
|
37
|
+
if self.domain_name:
|
|
38
|
+
return f"{self.domain_name} -> {self.project}:{self.region}:{self.instance_name}"
|
|
39
|
+
return f"{self.project}:{self.region}:{self.instance_name}"
|
|
40
|
+
|
|
41
|
+
|
|
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:
|
|
49
|
+
if CONN_NAME_REGEX.fullmatch(connection_name) is None:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
"Arg `instance_connection_string` must have "
|
|
52
|
+
"format: PROJECT:REGION:INSTANCE, "
|
|
53
|
+
f"got {connection_name}."
|
|
54
|
+
)
|
|
55
|
+
connection_name_split = CONN_NAME_REGEX.split(connection_name)
|
|
56
|
+
return ConnectionName(
|
|
57
|
+
connection_name_split[1],
|
|
58
|
+
connection_name_split[3],
|
|
59
|
+
connection_name_split[4],
|
|
60
|
+
domain_name,
|
|
61
|
+
)
|
|
@@ -21,7 +21,7 @@ from functools import partial
|
|
|
21
21
|
import logging
|
|
22
22
|
from threading import Thread
|
|
23
23
|
from types import TracebackType
|
|
24
|
-
from typing import Any,
|
|
24
|
+
from typing import Any, Optional, Type, Union
|
|
25
25
|
|
|
26
26
|
import google.auth
|
|
27
27
|
from google.auth.credentials import Credentials
|
|
@@ -37,6 +37,8 @@ from google.cloud.sql.connector.lazy import LazyRefreshCache
|
|
|
37
37
|
import google.cloud.sql.connector.pg8000 as pg8000
|
|
38
38
|
import google.cloud.sql.connector.pymysql as pymysql
|
|
39
39
|
import google.cloud.sql.connector.pytds as pytds
|
|
40
|
+
from google.cloud.sql.connector.resolver import DefaultResolver
|
|
41
|
+
from google.cloud.sql.connector.resolver import DnsResolver
|
|
40
42
|
from google.cloud.sql.connector.utils import format_database_user
|
|
41
43
|
from google.cloud.sql.connector.utils import generate_keys
|
|
42
44
|
|
|
@@ -63,6 +65,7 @@ class Connector:
|
|
|
63
65
|
user_agent: Optional[str] = None,
|
|
64
66
|
universe_domain: Optional[str] = None,
|
|
65
67
|
refresh_strategy: str | RefreshStrategy = RefreshStrategy.BACKGROUND,
|
|
68
|
+
resolver: Type[DefaultResolver] | Type[DnsResolver] = DefaultResolver,
|
|
66
69
|
) -> None:
|
|
67
70
|
"""Initializes a Connector instance.
|
|
68
71
|
|
|
@@ -104,6 +107,12 @@ class Connector:
|
|
|
104
107
|
of the following: RefreshStrategy.LAZY ("LAZY") or
|
|
105
108
|
RefreshStrategy.BACKGROUND ("BACKGROUND").
|
|
106
109
|
Default: RefreshStrategy.BACKGROUND
|
|
110
|
+
|
|
111
|
+
resolver (DefaultResolver | DnsResolver): The class name of the
|
|
112
|
+
resolver to use for resolving the Cloud SQL instance connection
|
|
113
|
+
name. To resolve a DNS record to an instance connection name, use
|
|
114
|
+
DnsResolver.
|
|
115
|
+
Default: DefaultResolver
|
|
107
116
|
"""
|
|
108
117
|
# if refresh_strategy is str, convert to RefreshStrategy enum
|
|
109
118
|
if isinstance(refresh_strategy, str):
|
|
@@ -133,8 +142,8 @@ class Connector:
|
|
|
133
142
|
)
|
|
134
143
|
# initialize dict to store caches, key is a tuple consisting of instance
|
|
135
144
|
# connection name string and enable_iam_auth boolean flag
|
|
136
|
-
self._cache:
|
|
137
|
-
|
|
145
|
+
self._cache: dict[
|
|
146
|
+
tuple[str, bool], Union[RefreshAheadCache, LazyRefreshCache]
|
|
138
147
|
] = {}
|
|
139
148
|
self._client: Optional[CloudSQLClient] = None
|
|
140
149
|
|
|
@@ -157,6 +166,7 @@ class Connector:
|
|
|
157
166
|
self._enable_iam_auth = enable_iam_auth
|
|
158
167
|
self._quota_project = quota_project
|
|
159
168
|
self._user_agent = user_agent
|
|
169
|
+
self._resolver = resolver()
|
|
160
170
|
# if ip_type is str, convert to IPTypes enum
|
|
161
171
|
if isinstance(ip_type, str):
|
|
162
172
|
ip_type = IPTypes._from_str(ip_type)
|
|
@@ -269,31 +279,28 @@ class Connector:
|
|
|
269
279
|
if (instance_connection_string, enable_iam_auth) in self._cache:
|
|
270
280
|
cache = self._cache[(instance_connection_string, enable_iam_auth)]
|
|
271
281
|
else:
|
|
282
|
+
conn_name = await self._resolver.resolve(instance_connection_string)
|
|
272
283
|
if self._refresh_strategy == RefreshStrategy.LAZY:
|
|
273
284
|
logger.debug(
|
|
274
|
-
f"['{
|
|
275
|
-
" to lazy refresh"
|
|
285
|
+
f"['{conn_name}']: Refresh strategy is set to lazy refresh"
|
|
276
286
|
)
|
|
277
287
|
cache = LazyRefreshCache(
|
|
278
|
-
|
|
288
|
+
conn_name,
|
|
279
289
|
self._client,
|
|
280
290
|
self._keys,
|
|
281
291
|
enable_iam_auth,
|
|
282
292
|
)
|
|
283
293
|
else:
|
|
284
294
|
logger.debug(
|
|
285
|
-
f"['{
|
|
286
|
-
" to backgound refresh"
|
|
295
|
+
f"['{conn_name}']: Refresh strategy is set to backgound refresh"
|
|
287
296
|
)
|
|
288
297
|
cache = RefreshAheadCache(
|
|
289
|
-
|
|
298
|
+
conn_name,
|
|
290
299
|
self._client,
|
|
291
300
|
self._keys,
|
|
292
301
|
enable_iam_auth,
|
|
293
302
|
)
|
|
294
|
-
logger.debug(
|
|
295
|
-
f"['{instance_connection_string}']: Connection info added to cache"
|
|
296
|
-
)
|
|
303
|
+
logger.debug(f"['{conn_name}']: Connection info added to cache")
|
|
297
304
|
self._cache[(instance_connection_string, enable_iam_auth)] = cache
|
|
298
305
|
|
|
299
306
|
connect_func = {
|
|
@@ -332,9 +339,7 @@ class Connector:
|
|
|
332
339
|
# the cache and re-raise the error
|
|
333
340
|
await self._remove_cached(instance_connection_string, enable_iam_auth)
|
|
334
341
|
raise
|
|
335
|
-
logger.debug(
|
|
336
|
-
f"['{instance_connection_string}']: Connecting to {ip_address}:3307"
|
|
337
|
-
)
|
|
342
|
+
logger.debug(f"['{conn_info.conn_name}']: Connecting to {ip_address}:3307")
|
|
338
343
|
# format `user` param for automatic IAM database authn
|
|
339
344
|
if enable_iam_auth:
|
|
340
345
|
formatted_user = format_database_user(
|
|
@@ -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
|
+
"""
|
|
@@ -21,13 +21,10 @@ from datetime import datetime
|
|
|
21
21
|
from datetime import timedelta
|
|
22
22
|
from datetime import timezone
|
|
23
23
|
import logging
|
|
24
|
-
import re
|
|
25
|
-
from typing import Tuple
|
|
26
|
-
|
|
27
|
-
import aiohttp
|
|
28
24
|
|
|
29
25
|
from google.cloud.sql.connector.client import CloudSQLClient
|
|
30
26
|
from google.cloud.sql.connector.connection_info import ConnectionInfo
|
|
27
|
+
from google.cloud.sql.connector.connection_name import ConnectionName
|
|
31
28
|
from google.cloud.sql.connector.exceptions import RefreshNotValidError
|
|
32
29
|
from google.cloud.sql.connector.rate_limiter import AsyncRateLimiter
|
|
33
30
|
from google.cloud.sql.connector.refresh_utils import _is_valid
|
|
@@ -37,22 +34,6 @@ logger = logging.getLogger(name=__name__)
|
|
|
37
34
|
|
|
38
35
|
APPLICATION_NAME = "cloud-sql-python-connector"
|
|
39
36
|
|
|
40
|
-
# Instance connection name is the format <PROJECT>:<REGION>:<INSTANCE>
|
|
41
|
-
# Additionally, we have to support legacy "domain-scoped" projects
|
|
42
|
-
# (e.g. "google.com:PROJECT")
|
|
43
|
-
CONN_NAME_REGEX = re.compile(("([^:]+(:[^:]+)?):([^:]+):([^:]+)"))
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _parse_instance_connection_name(connection_name: str) -> Tuple[str, str, str]:
|
|
47
|
-
if CONN_NAME_REGEX.fullmatch(connection_name) is None:
|
|
48
|
-
raise ValueError(
|
|
49
|
-
"Arg `instance_connection_string` must have "
|
|
50
|
-
"format: PROJECT:REGION:INSTANCE, "
|
|
51
|
-
f"got {connection_name}."
|
|
52
|
-
)
|
|
53
|
-
connection_name_split = CONN_NAME_REGEX.split(connection_name)
|
|
54
|
-
return connection_name_split[1], connection_name_split[3], connection_name_split[4]
|
|
55
|
-
|
|
56
37
|
|
|
57
38
|
class RefreshAheadCache:
|
|
58
39
|
"""Cache that refreshes connection info in the background prior to expiration.
|
|
@@ -64,7 +45,7 @@ class RefreshAheadCache:
|
|
|
64
45
|
|
|
65
46
|
def __init__(
|
|
66
47
|
self,
|
|
67
|
-
|
|
48
|
+
conn_name: ConnectionName,
|
|
68
49
|
client: CloudSQLClient,
|
|
69
50
|
keys: asyncio.Future,
|
|
70
51
|
enable_iam_auth: bool = False,
|
|
@@ -72,8 +53,8 @@ class RefreshAheadCache:
|
|
|
72
53
|
"""Initializes a RefreshAheadCache instance.
|
|
73
54
|
|
|
74
55
|
Args:
|
|
75
|
-
|
|
76
|
-
connection
|
|
56
|
+
conn_name (ConnectionName): The Cloud SQL instance's
|
|
57
|
+
connection name.
|
|
77
58
|
client (CloudSQLClient): The Cloud SQL Client instance.
|
|
78
59
|
keys (asyncio.Future): A future to the client's public-private key
|
|
79
60
|
pair.
|
|
@@ -81,11 +62,7 @@ class RefreshAheadCache:
|
|
|
81
62
|
(Postgres and MySQL) as the default authentication method for all
|
|
82
63
|
connections.
|
|
83
64
|
"""
|
|
84
|
-
|
|
85
|
-
self._project, self._region, self._instance = _parse_instance_connection_name(
|
|
86
|
-
instance_connection_string
|
|
87
|
-
)
|
|
88
|
-
self._instance_connection_string = instance_connection_string
|
|
65
|
+
self._conn_name = conn_name
|
|
89
66
|
|
|
90
67
|
self._enable_iam_auth = enable_iam_auth
|
|
91
68
|
self._keys = keys
|
|
@@ -122,40 +99,27 @@ class RefreshAheadCache:
|
|
|
122
99
|
"""
|
|
123
100
|
self._refresh_in_progress.set()
|
|
124
101
|
logger.debug(
|
|
125
|
-
f"['{self.
|
|
126
|
-
"operation started"
|
|
102
|
+
f"['{self._conn_name}']: Connection info refresh operation started"
|
|
127
103
|
)
|
|
128
104
|
|
|
129
105
|
try:
|
|
130
106
|
await self._refresh_rate_limiter.acquire()
|
|
131
107
|
connection_info = await self._client.get_connection_info(
|
|
132
|
-
self.
|
|
133
|
-
self._region,
|
|
134
|
-
self._instance,
|
|
108
|
+
self._conn_name,
|
|
135
109
|
self._keys,
|
|
136
110
|
self._enable_iam_auth,
|
|
137
111
|
)
|
|
138
112
|
logger.debug(
|
|
139
|
-
f"['{self.
|
|
140
|
-
"refresh operation complete"
|
|
113
|
+
f"['{self._conn_name}']: Connection info refresh operation complete"
|
|
141
114
|
)
|
|
142
115
|
logger.debug(
|
|
143
|
-
f"['{self.
|
|
116
|
+
f"['{self._conn_name}']: Current certificate "
|
|
144
117
|
f"expiration = {connection_info.expiration.isoformat()}"
|
|
145
118
|
)
|
|
146
119
|
|
|
147
|
-
except aiohttp.ClientResponseError as e:
|
|
148
|
-
logger.debug(
|
|
149
|
-
f"['{self._instance_connection_string}']: Connection info "
|
|
150
|
-
f"refresh operation failed: {str(e)}"
|
|
151
|
-
)
|
|
152
|
-
if e.status == 403:
|
|
153
|
-
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."
|
|
154
|
-
raise
|
|
155
|
-
|
|
156
120
|
except Exception as e:
|
|
157
121
|
logger.debug(
|
|
158
|
-
f"['{self.
|
|
122
|
+
f"['{self._conn_name}']: Connection info "
|
|
159
123
|
f"refresh operation failed: {str(e)}"
|
|
160
124
|
)
|
|
161
125
|
raise
|
|
@@ -189,18 +153,17 @@ class RefreshAheadCache:
|
|
|
189
153
|
# check that refresh is valid
|
|
190
154
|
if not await _is_valid(refresh_task):
|
|
191
155
|
raise RefreshNotValidError(
|
|
192
|
-
f"['{self.
|
|
156
|
+
f"['{self._conn_name}']: Invalid refresh operation. Certficate appears to be expired."
|
|
193
157
|
)
|
|
194
158
|
except asyncio.CancelledError:
|
|
195
159
|
logger.debug(
|
|
196
|
-
f"['{self.
|
|
197
|
-
" operation cancelled"
|
|
160
|
+
f"['{self._conn_name}']: Scheduled refresh" " operation cancelled"
|
|
198
161
|
)
|
|
199
162
|
raise
|
|
200
163
|
# bad refresh attempt
|
|
201
164
|
except Exception as e:
|
|
202
165
|
logger.exception(
|
|
203
|
-
f"['{self.
|
|
166
|
+
f"['{self._conn_name}']: "
|
|
204
167
|
"An error occurred while performing refresh. "
|
|
205
168
|
"Scheduling another refresh attempt immediately",
|
|
206
169
|
exc_info=e,
|
|
@@ -217,7 +180,7 @@ class RefreshAheadCache:
|
|
|
217
180
|
# calculate refresh delay based on certificate expiration
|
|
218
181
|
delay = _seconds_until_refresh(refresh_data.expiration)
|
|
219
182
|
logger.debug(
|
|
220
|
-
f"['{self.
|
|
183
|
+
f"['{self._conn_name}']: Connection info refresh"
|
|
221
184
|
" operation scheduled for "
|
|
222
185
|
f"{(datetime.now(timezone.utc) + timedelta(seconds=delay)).isoformat(timespec='seconds')} "
|
|
223
186
|
f"(now + {timedelta(seconds=delay)})"
|
|
@@ -241,7 +204,7 @@ class RefreshAheadCache:
|
|
|
241
204
|
graceful exit.
|
|
242
205
|
"""
|
|
243
206
|
logger.debug(
|
|
244
|
-
f"['{self.
|
|
207
|
+
f"['{self._conn_name}']: Canceling connection info "
|
|
245
208
|
"refresh operation tasks"
|
|
246
209
|
)
|
|
247
210
|
self._current.cancel()
|
|
@@ -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.
|
|
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,12 +55,7 @@ class LazyRefreshCache:
|
|
|
55
55
|
(Postgres and MySQL) as the default authentication method for all
|
|
56
56
|
connections.
|
|
57
57
|
"""
|
|
58
|
-
|
|
59
|
-
self._project, self._region, self._instance = _parse_instance_connection_name(
|
|
60
|
-
instance_connection_string
|
|
61
|
-
)
|
|
62
|
-
self._instance_connection_string = instance_connection_string
|
|
63
|
-
|
|
58
|
+
self._conn_name = conn_name
|
|
64
59
|
self._enable_iam_auth = enable_iam_auth
|
|
65
60
|
self._keys = keys
|
|
66
61
|
self._client = client
|
|
@@ -91,34 +86,31 @@ class LazyRefreshCache:
|
|
|
91
86
|
< (self._cached.expiration - timedelta(seconds=_refresh_buffer))
|
|
92
87
|
):
|
|
93
88
|
logger.debug(
|
|
94
|
-
f"['{self.
|
|
89
|
+
f"['{self._conn_name}']: Connection info "
|
|
95
90
|
"is still valid, using cached info"
|
|
96
91
|
)
|
|
97
92
|
return self._cached
|
|
98
93
|
logger.debug(
|
|
99
|
-
f"['{self.
|
|
100
|
-
"refresh operation started"
|
|
94
|
+
f"['{self._conn_name}']: Connection info " "refresh operation started"
|
|
101
95
|
)
|
|
102
96
|
try:
|
|
103
97
|
conn_info = await self._client.get_connection_info(
|
|
104
|
-
self.
|
|
105
|
-
self._region,
|
|
106
|
-
self._instance,
|
|
98
|
+
self._conn_name,
|
|
107
99
|
self._keys,
|
|
108
100
|
self._enable_iam_auth,
|
|
109
101
|
)
|
|
110
102
|
except Exception as e:
|
|
111
103
|
logger.debug(
|
|
112
|
-
f"['{self.
|
|
104
|
+
f"['{self._conn_name}']: Connection info "
|
|
113
105
|
f"refresh operation failed: {str(e)}"
|
|
114
106
|
)
|
|
115
107
|
raise
|
|
116
108
|
logger.debug(
|
|
117
|
-
f"['{self.
|
|
109
|
+
f"['{self._conn_name}']: Connection info "
|
|
118
110
|
"refresh operation completed successfully"
|
|
119
111
|
)
|
|
120
112
|
logger.debug(
|
|
121
|
-
f"['{self.
|
|
113
|
+
f"['{self._conn_name}']: Current certificate "
|
|
122
114
|
f"expiration = {str(conn_info.expiration)}"
|
|
123
115
|
)
|
|
124
116
|
self._cached = conn_info
|
|
@@ -21,7 +21,7 @@ import copy
|
|
|
21
21
|
import datetime
|
|
22
22
|
import logging
|
|
23
23
|
import random
|
|
24
|
-
from typing import Any, Callable
|
|
24
|
+
from typing import Any, Callable
|
|
25
25
|
|
|
26
26
|
import aiohttp
|
|
27
27
|
from google.auth.credentials import Credentials
|
|
@@ -77,7 +77,7 @@ async def _is_valid(task: asyncio.Task) -> bool:
|
|
|
77
77
|
|
|
78
78
|
def _downscope_credentials(
|
|
79
79
|
credentials: Credentials,
|
|
80
|
-
scopes:
|
|
80
|
+
scopes: list[str] = ["https://www.googleapis.com/auth/sqlservice.login"],
|
|
81
81
|
) -> Credentials:
|
|
82
82
|
"""Generate a down-scoped credential.
|
|
83
83
|
|
|
@@ -85,7 +85,7 @@ def _downscope_credentials(
|
|
|
85
85
|
:param credentials
|
|
86
86
|
Credentials object used to generate down-scoped credentials.
|
|
87
87
|
|
|
88
|
-
:type scopes:
|
|
88
|
+
:type scopes: list[str]
|
|
89
89
|
:param scopes
|
|
90
90
|
List of Google scopes to include in down-scoped credentials object.
|
|
91
91
|
|
|
@@ -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
|
|
@@ -14,15 +14,13 @@ See the License for the specific language governing permissions and
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
from typing import Tuple
|
|
18
|
-
|
|
19
17
|
import aiofiles
|
|
20
18
|
from cryptography.hazmat.backends import default_backend
|
|
21
19
|
from cryptography.hazmat.primitives import serialization
|
|
22
20
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
23
21
|
|
|
24
22
|
|
|
25
|
-
async def generate_keys() ->
|
|
23
|
+
async def generate_keys() -> tuple[bytes, str]:
|
|
26
24
|
"""A helper function to generate the private and public keys.
|
|
27
25
|
|
|
28
26
|
backend - The value specified is default_backend(). This is because the
|
|
@@ -61,7 +59,7 @@ async def generate_keys() -> Tuple[bytes, str]:
|
|
|
61
59
|
|
|
62
60
|
async def write_to_file(
|
|
63
61
|
dir_path: str, serverCaCert: str, ephemeralCert: str, priv_key: bytes
|
|
64
|
-
) ->
|
|
62
|
+
) -> tuple[str, str, str]:
|
|
65
63
|
"""
|
|
66
64
|
Helper function to write the serverCaCert, ephemeral certificate and
|
|
67
65
|
private key to .pem files in a given directory
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
google/cloud/sql/connector/__init__.py,sha256=x8ui7u3gMlMI-LLJkKTtUU9yvzZvQkTJ-yzr2FH4VTU,983
|
|
2
|
-
google/cloud/sql/connector/asyncpg.py,sha256=c4lAlvEgGf_eBq0G8MVNPRAJQQvo6DcwybiXDW36qkU,1838
|
|
3
|
-
google/cloud/sql/connector/client.py,sha256=dqUYJdUdusV_s2Xs0uWFmKqqoOU4mpHkrgl0qqbx9Vw,11713
|
|
4
|
-
google/cloud/sql/connector/connection_info.py,sha256=exxhtb0-rk8J3OIotf8drEPMx8pke17hAX_Mbt7gOts,4336
|
|
5
|
-
google/cloud/sql/connector/connector.py,sha256=NWyuwUoAsCuPxxzjB9OiD19tD1LdcQtqfbxJOLlS5-A,20731
|
|
6
|
-
google/cloud/sql/connector/enums.py,sha256=44tfMm8D_t0ls6GfKwiQ6YkslWM21PZR40RhJPWntkc,2882
|
|
7
|
-
google/cloud/sql/connector/exceptions.py,sha256=OVenqBkkn98xFeytHuOXFTWa9R5bCmemBtbSIm8caoM,1665
|
|
8
|
-
google/cloud/sql/connector/instance.py,sha256=EySszNNHMFjAFskf6-G6Upikvb-tYjEwXx9A_OJBl14,10136
|
|
9
|
-
google/cloud/sql/connector/lazy.py,sha256=og4UPiJxeuLgN7tyqrFAnQF3895Yqdf6kkd4HZSzVJA,5093
|
|
10
|
-
google/cloud/sql/connector/pg8000.py,sha256=EZuO7XgCkG_Y79NQlh3Vv8Ff2w85BFJyFVO237hx1GM,1814
|
|
11
|
-
google/cloud/sql/connector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
google/cloud/sql/connector/pymysql.py,sha256=wldUgwDPlPa8Nj5F3JaipuP8hJyvzlgOAJbRnOoaB1U,2105
|
|
13
|
-
google/cloud/sql/connector/pytds.py,sha256=vKJoE7E2P0GrhZojg8bGZkqCMLc8dgKAcXE4ee7vEVc,2452
|
|
14
|
-
google/cloud/sql/connector/rate_limiter.py,sha256=bPTupfM4f5QRSCmCAWMqjQylEB7bUM-50plVBU3W2CA,2993
|
|
15
|
-
google/cloud/sql/connector/refresh_utils.py,sha256=CajL-MOcQBBRUkywowdHB3GNOr0WpQn37DXG7dRpkMk,5069
|
|
16
|
-
google/cloud/sql/connector/utils.py,sha256=nQVZVfEijbO3XEj1oU68cR-u8Nx99rRM2a8aXaygOg0,3534
|
|
17
|
-
google/cloud/sql/connector/version.py,sha256=EWwYfhVJ-gfn9Us5uJphJ-6hyY5hIvZ1lMkYVSAinqo,598
|
|
18
|
-
cloud_sql_python_connector-1.13.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
19
|
-
cloud_sql_python_connector-1.13.0.dist-info/METADATA,sha256=4D0iy9DJ08BNblMe2HaliEjMi6Wee_eQVQY_JT7nb4o,25950
|
|
20
|
-
cloud_sql_python_connector-1.13.0.dist-info/WHEEL,sha256=bb2Ot9scclHKMOLDEHY6B2sicWOgugjFKaJsT7vwMQo,110
|
|
21
|
-
cloud_sql_python_connector-1.13.0.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
|
|
22
|
-
cloud_sql_python_connector-1.13.0.dist-info/RECORD,,
|
{cloud_sql_python_connector-1.13.0.dist-info → cloud_sql_python_connector-1.15.0.dist-info}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|