cloud-sql-python-connector 1.22.0__py3-none-any.whl → 1.23.0__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.22.0.dist-info → cloud_sql_python_connector-1.23.0.dist-info}/METADATA +10 -9
- cloud_sql_python_connector-1.23.0.dist-info/RECORD +27 -0
- google/cloud/sql/connector/client.py +15 -3
- google/cloud/sql/connector/connection_info.py +8 -1
- google/cloud/sql/connector/connector.py +349 -134
- google/cloud/sql/connector/enums.py +3 -0
- google/cloud/sql/connector/exceptions.py +33 -0
- google/cloud/sql/connector/instance.py +2 -2
- google/cloud/sql/connector/lazy.py +2 -2
- google/cloud/sql/connector/monitored_cache.py +10 -9
- google/cloud/sql/connector/psycopg.py +241 -153
- google/cloud/sql/connector/refresh_utils.py +4 -3
- google/cloud/sql/connector/sqldata_client.py +654 -0
- google/cloud/sql/connector/version.py +1 -1
- cloud_sql_python_connector-1.22.0.dist-info/RECORD +0 -26
- {cloud_sql_python_connector-1.22.0.dist-info → cloud_sql_python_connector-1.23.0.dist-info}/WHEEL +0 -0
- {cloud_sql_python_connector-1.22.0.dist-info → cloud_sql_python_connector-1.23.0.dist-info}/licenses/LICENSE +0 -0
- {cloud_sql_python_connector-1.22.0.dist-info → cloud_sql_python_connector-1.23.0.dist-info}/top_level.txt +0 -0
|
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
17
19
|
|
|
18
20
|
class ConnectorLoopError(Exception):
|
|
19
21
|
"""
|
|
@@ -91,3 +93,34 @@ class ClosedConnectorError(Exception):
|
|
|
91
93
|
Exception to be raised when a Connector is closed and connect method is
|
|
92
94
|
called on it.
|
|
93
95
|
"""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class CloudSQLConnectionError(Exception):
|
|
99
|
+
"""
|
|
100
|
+
Exception to be raised when a connection cannot be established to a Cloud SQL instance.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ResourceExhaustedError(CloudSQLConnectionError):
|
|
105
|
+
"""
|
|
106
|
+
Exception to be raised when a connection cannot be established because
|
|
107
|
+
the SQL Data Service is busy / in cooldown due to resource exhaustion.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
message: str,
|
|
113
|
+
connection_name: str | None = None,
|
|
114
|
+
raw_error: Exception | None = None,
|
|
115
|
+
) -> None:
|
|
116
|
+
self.message = message
|
|
117
|
+
self.connection_name = connection_name
|
|
118
|
+
self.raw_error = raw_error
|
|
119
|
+
super().__init__(
|
|
120
|
+
f"[{connection_name}] {message}: {raw_error}"
|
|
121
|
+
if connection_name and raw_error
|
|
122
|
+
else f"[{connection_name}] {message}"
|
|
123
|
+
if connection_name
|
|
124
|
+
else message
|
|
125
|
+
)
|
|
126
|
+
|
|
@@ -19,7 +19,7 @@ from __future__ import annotations
|
|
|
19
19
|
import asyncio
|
|
20
20
|
from datetime import datetime
|
|
21
21
|
from datetime import timedelta
|
|
22
|
-
from datetime import
|
|
22
|
+
from datetime import UTC
|
|
23
23
|
import logging
|
|
24
24
|
|
|
25
25
|
from google.cloud.sql.connector.client import CloudSQLClient
|
|
@@ -192,7 +192,7 @@ class RefreshAheadCache(ConnectionInfoCache):
|
|
|
192
192
|
logger.debug(
|
|
193
193
|
f"['{self._conn_name}']: Connection info refresh"
|
|
194
194
|
" operation scheduled for "
|
|
195
|
-
f"{(datetime.now(
|
|
195
|
+
f"{(datetime.now(UTC) + timedelta(seconds=delay)).isoformat(timespec='seconds')} "
|
|
196
196
|
f"(now + {timedelta(seconds=delay)})"
|
|
197
197
|
)
|
|
198
198
|
self._next = self._schedule_refresh(delay)
|
|
@@ -17,7 +17,7 @@ from __future__ import annotations
|
|
|
17
17
|
import asyncio
|
|
18
18
|
from datetime import datetime
|
|
19
19
|
from datetime import timedelta
|
|
20
|
-
from datetime import
|
|
20
|
+
from datetime import UTC
|
|
21
21
|
import logging
|
|
22
22
|
|
|
23
23
|
from google.cloud.sql.connector.client import CloudSQLClient
|
|
@@ -93,7 +93,7 @@ class LazyRefreshCache(ConnectionInfoCache):
|
|
|
93
93
|
if (
|
|
94
94
|
self._cached
|
|
95
95
|
and not self._needs_refresh
|
|
96
|
-
and datetime.now(
|
|
96
|
+
and datetime.now(UTC)
|
|
97
97
|
< (self._cached.expiration - timedelta(seconds=_refresh_buffer))
|
|
98
98
|
):
|
|
99
99
|
logger.debug(
|
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import asyncio
|
|
18
|
+
from collections.abc import Callable
|
|
18
19
|
import logging
|
|
19
|
-
import
|
|
20
|
-
from typing import Any
|
|
20
|
+
import socket
|
|
21
|
+
from typing import Any
|
|
21
22
|
|
|
22
23
|
import aiohttp
|
|
23
24
|
|
|
@@ -44,7 +45,7 @@ class MonitoredCache(ConnectionInfoCache):
|
|
|
44
45
|
self.resolver = resolver
|
|
45
46
|
self.cache = cache
|
|
46
47
|
self.domain_name_ticker: asyncio.Task | None = None
|
|
47
|
-
self.sockets: list[
|
|
48
|
+
self.sockets: list[socket.socket] = []
|
|
48
49
|
|
|
49
50
|
# If domain name is configured for instance and failover period is set,
|
|
50
51
|
# poll for DNS record changes.
|
|
@@ -77,11 +78,11 @@ class MonitoredCache(ConnectionInfoCache):
|
|
|
77
78
|
list of sockets.
|
|
78
79
|
"""
|
|
79
80
|
open_sockets = []
|
|
80
|
-
for
|
|
81
|
+
for sock in self.sockets:
|
|
81
82
|
# Check fileno for if socket is closed. Will return
|
|
82
83
|
# -1 on failure, which will be used to signal socket closed.
|
|
83
|
-
if
|
|
84
|
-
open_sockets.append(
|
|
84
|
+
if sock.fileno() != -1:
|
|
85
|
+
open_sockets.append(sock)
|
|
85
86
|
self.sockets = open_sockets
|
|
86
87
|
|
|
87
88
|
async def _check_domain_name(self) -> None:
|
|
@@ -159,11 +160,11 @@ class MonitoredCache(ConnectionInfoCache):
|
|
|
159
160
|
await self.cache.close()
|
|
160
161
|
|
|
161
162
|
# Close any still open sockets
|
|
162
|
-
for
|
|
163
|
+
for sock in self.sockets:
|
|
163
164
|
# Check fileno for if socket is closed. Will return
|
|
164
165
|
# -1 on failure, which will be used to signal socket closed.
|
|
165
|
-
if
|
|
166
|
-
|
|
166
|
+
if sock.fileno() != -1:
|
|
167
|
+
sock.close()
|
|
167
168
|
|
|
168
169
|
|
|
169
170
|
async def ticker(interval: int, function: Callable, *args: Any, **kwargs: Any) -> None:
|
|
@@ -12,20 +12,128 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
+
import contextlib
|
|
15
16
|
import logging
|
|
16
17
|
import os
|
|
18
|
+
import platform
|
|
17
19
|
import selectors
|
|
18
20
|
import socket
|
|
19
21
|
import ssl
|
|
20
22
|
import tempfile
|
|
21
23
|
import threading
|
|
22
|
-
from
|
|
24
|
+
from types import TracebackType
|
|
25
|
+
from typing import Any, Self, TYPE_CHECKING
|
|
26
|
+
|
|
27
|
+
from google.cloud.sql.connector.sqldata_client import SqlDataSocket
|
|
23
28
|
|
|
24
29
|
if TYPE_CHECKING:
|
|
25
30
|
import psycopg
|
|
26
31
|
|
|
27
32
|
logger = logging.getLogger(name=__name__)
|
|
28
33
|
|
|
34
|
+
_BUFFER_SIZE = 8192
|
|
35
|
+
_DEFAULT_PGSQL_PORT = 5432
|
|
36
|
+
_LOOPBACK_HOST = "127.0.0.1"
|
|
37
|
+
_UNIX_SOCKET_NAME = ".s.PGSQL.5432"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _close_socket(sock: socket.socket, *, shutdown: bool = True) -> None:
|
|
41
|
+
"""Safely shut down and close a socket, ignoring OSErrors."""
|
|
42
|
+
if shutdown:
|
|
43
|
+
try:
|
|
44
|
+
sock.shutdown(socket.SHUT_RDWR)
|
|
45
|
+
except OSError:
|
|
46
|
+
pass
|
|
47
|
+
try:
|
|
48
|
+
sock.close()
|
|
49
|
+
except OSError:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _forward_bytes(
|
|
54
|
+
src: socket.socket,
|
|
55
|
+
dst: socket.socket,
|
|
56
|
+
src_name: str,
|
|
57
|
+
dst_name: str,
|
|
58
|
+
*,
|
|
59
|
+
nonblocking: bool = False,
|
|
60
|
+
) -> bool:
|
|
61
|
+
"""Read a chunk of bytes from src and send it to dst.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
bool: True if bytes were forwarded, False on EOF or OSError.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
if nonblocking:
|
|
68
|
+
src.setblocking(False)
|
|
69
|
+
try:
|
|
70
|
+
data = src.recv(_BUFFER_SIZE)
|
|
71
|
+
finally:
|
|
72
|
+
if nonblocking:
|
|
73
|
+
with contextlib.suppress(OSError):
|
|
74
|
+
src.setblocking(True)
|
|
75
|
+
except OSError as e:
|
|
76
|
+
if nonblocking and isinstance(e, (ssl.SSLWantReadError, BlockingIOError)):
|
|
77
|
+
return True
|
|
78
|
+
logger.debug(f"Failed to receive from {src_name} socket: {e}")
|
|
79
|
+
return False
|
|
80
|
+
if not data:
|
|
81
|
+
logger.debug(f"EOF reached on {src_name} socket")
|
|
82
|
+
return False
|
|
83
|
+
try:
|
|
84
|
+
dst.sendall(data)
|
|
85
|
+
except OSError as e:
|
|
86
|
+
logger.debug(f"Failed to send to {dst_name} socket: {e}")
|
|
87
|
+
return False
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _forward_pending(local: socket.socket, remote: "ssl.SSLSocket") -> bool:
|
|
92
|
+
"""Read any pending decrypted data from SSL buffer and forward it to local.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
bool: True if EOF was reached or error occurred (should exit).
|
|
96
|
+
"""
|
|
97
|
+
if not hasattr(remote, "pending"):
|
|
98
|
+
return False
|
|
99
|
+
pending_bytes = remote.pending()
|
|
100
|
+
if not isinstance(pending_bytes, int):
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
while pending_bytes > 0:
|
|
104
|
+
if not _forward_bytes(remote, local, "remote", "local"):
|
|
105
|
+
return True
|
|
106
|
+
try:
|
|
107
|
+
pending_bytes = remote.pending()
|
|
108
|
+
except OSError:
|
|
109
|
+
break
|
|
110
|
+
if not isinstance(pending_bytes, int):
|
|
111
|
+
break
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _forward_local_to_remote(local: socket.socket, remote: SqlDataSocket) -> None:
|
|
116
|
+
"""Forward bytes from local socket to remote SqlDataSocket until EOF or error."""
|
|
117
|
+
try:
|
|
118
|
+
while _forward_bytes(local, remote, "local", "remote"):
|
|
119
|
+
pass
|
|
120
|
+
finally:
|
|
121
|
+
_close_socket(remote, shutdown=False)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _proxy_sqldata(local: socket.socket, remote: SqlDataSocket) -> None:
|
|
125
|
+
"""Bidirectional thread-based proxy for queue-backed SqlDataSocket."""
|
|
126
|
+
remote.settimeout(None)
|
|
127
|
+
threading.Thread(
|
|
128
|
+
target=_forward_local_to_remote, args=(local, remote), daemon=True
|
|
129
|
+
).start()
|
|
130
|
+
try:
|
|
131
|
+
while _forward_bytes(remote, local, "remote", "local"):
|
|
132
|
+
pass
|
|
133
|
+
finally:
|
|
134
|
+
_close_socket(local)
|
|
135
|
+
_close_socket(remote, shutdown=False)
|
|
136
|
+
|
|
29
137
|
|
|
30
138
|
def _proxy(local: socket.socket, remote: "ssl.SSLSocket") -> None:
|
|
31
139
|
"""Single-threaded selectors-based proxy to avoid SSLSocket thread-safety issues."""
|
|
@@ -33,104 +141,145 @@ def _proxy(local: socket.socket, remote: "ssl.SSLSocket") -> None:
|
|
|
33
141
|
sel.register(local, selectors.EVENT_READ, data="local")
|
|
34
142
|
sel.register(remote, selectors.EVENT_READ, data="remote")
|
|
35
143
|
|
|
36
|
-
def forward_pending() -> bool:
|
|
37
|
-
"""Read any pending decrypted data from SSL buffer and forward it.
|
|
38
|
-
Returns True if EOF was reached or error occurred (should exit).
|
|
39
|
-
"""
|
|
40
|
-
if not hasattr(remote, "pending"):
|
|
41
|
-
return False
|
|
42
|
-
pending_bytes = remote.pending()
|
|
43
|
-
if not isinstance(pending_bytes, int):
|
|
44
|
-
return False
|
|
45
|
-
|
|
46
|
-
while pending_bytes > 0:
|
|
47
|
-
try:
|
|
48
|
-
data = remote.recv(8192)
|
|
49
|
-
except OSError as e:
|
|
50
|
-
logger.debug("psycopg proxy: remote recv pending error: %s", e)
|
|
51
|
-
return True
|
|
52
|
-
if not data:
|
|
53
|
-
logger.debug("psycopg proxy: remote pending EOF")
|
|
54
|
-
return True
|
|
55
|
-
try:
|
|
56
|
-
local.sendall(data)
|
|
57
|
-
except OSError as e:
|
|
58
|
-
logger.debug("psycopg proxy: local send pending error: %s", e)
|
|
59
|
-
return True
|
|
60
|
-
try:
|
|
61
|
-
pending_bytes = remote.pending()
|
|
62
|
-
except OSError:
|
|
63
|
-
break
|
|
64
|
-
if not isinstance(pending_bytes, int):
|
|
65
|
-
break
|
|
66
|
-
return False
|
|
67
|
-
|
|
68
144
|
try:
|
|
69
145
|
while True:
|
|
70
146
|
# First check if there is any pending data in SSL buffer
|
|
71
|
-
if
|
|
147
|
+
if _forward_pending(local, remote):
|
|
72
148
|
break
|
|
73
149
|
|
|
74
150
|
events = sel.select()
|
|
75
151
|
|
|
76
|
-
for key,
|
|
77
|
-
if key.data == "local"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return
|
|
86
|
-
try:
|
|
87
|
-
remote.sendall(data)
|
|
88
|
-
except OSError as e:
|
|
89
|
-
logger.debug("psycopg proxy: remote send error: %s", e)
|
|
90
|
-
return
|
|
91
|
-
elif key.data == "remote":
|
|
92
|
-
try:
|
|
93
|
-
data = remote.recv(8192)
|
|
94
|
-
except OSError as e:
|
|
95
|
-
logger.debug("psycopg proxy: remote recv error: %s", e)
|
|
96
|
-
return
|
|
97
|
-
if not data:
|
|
98
|
-
logger.debug("psycopg proxy: remote EOF")
|
|
99
|
-
return
|
|
100
|
-
try:
|
|
101
|
-
local.sendall(data)
|
|
102
|
-
except OSError as e:
|
|
103
|
-
logger.debug("psycopg proxy: local send error: %s", e)
|
|
104
|
-
return
|
|
152
|
+
for key, _ in events:
|
|
153
|
+
if key.data == "local" and not _forward_bytes(
|
|
154
|
+
local, remote, "local", "remote"
|
|
155
|
+
):
|
|
156
|
+
return
|
|
157
|
+
if key.data == "remote" and not _forward_bytes(
|
|
158
|
+
remote, local, "remote", "local", nonblocking=True
|
|
159
|
+
):
|
|
160
|
+
return
|
|
105
161
|
except OSError as e:
|
|
106
|
-
logger.debug("
|
|
162
|
+
logger.debug(f"Selector loop error in proxy: {e}")
|
|
107
163
|
finally:
|
|
108
164
|
sel.close()
|
|
109
|
-
|
|
165
|
+
_close_socket(local)
|
|
166
|
+
_close_socket(remote)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class _PsycopgProxy:
|
|
170
|
+
"""Context manager that sets up a local listener socket and background proxy.
|
|
171
|
+
|
|
172
|
+
Encapsulates the platform-specific listener creation (Unix domain socket on
|
|
173
|
+
POSIX, TCP loopback on Windows), background connection acceptance, and
|
|
174
|
+
deterministic cleanup of temporary filesystem resources.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
def __init__(
|
|
178
|
+
self, ip_address: str, remote_sock: "ssl.SSLSocket | SqlDataSocket"
|
|
179
|
+
) -> None:
|
|
180
|
+
self._ip_address = ip_address
|
|
181
|
+
self._remote_sock = remote_sock
|
|
182
|
+
self._tmpdir: str | None = None
|
|
183
|
+
self._socket_path: str | None = None
|
|
184
|
+
self._local_sock: socket.socket
|
|
185
|
+
self.host: str = ""
|
|
186
|
+
self.port: int = _DEFAULT_PGSQL_PORT
|
|
187
|
+
|
|
188
|
+
def __enter__(self) -> Self:
|
|
189
|
+
self._local_sock = self._create_local_socket()
|
|
190
|
+
threading.Thread(target=self._accept_and_proxy, daemon=True).start()
|
|
191
|
+
return self
|
|
192
|
+
|
|
193
|
+
def __exit__(
|
|
194
|
+
self,
|
|
195
|
+
exc_type: type[BaseException] | None,
|
|
196
|
+
exc_val: BaseException | None,
|
|
197
|
+
exc_tb: TracebackType | None,
|
|
198
|
+
) -> None:
|
|
199
|
+
if exc_type is not None:
|
|
200
|
+
# psycopg never connected (or failed mid-handshake); close the server
|
|
201
|
+
# socket so the proxy thread unblocks and exits cleanly.
|
|
202
|
+
_close_socket(self._local_sock, shutdown=False)
|
|
203
|
+
_close_socket(self._remote_sock, shutdown=False)
|
|
204
|
+
self._cleanup_socket_files()
|
|
205
|
+
|
|
206
|
+
def _create_local_socket(self) -> socket.socket:
|
|
207
|
+
"""Determine the platform and create a bound local listener socket."""
|
|
208
|
+
if platform.system() == "Windows":
|
|
209
|
+
# On Windows, libpq does not support Unix domain sockets.
|
|
210
|
+
# Use a local TCP loopback socket on an ephemeral port.
|
|
211
|
+
local_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
212
|
+
local_sock.bind((_LOOPBACK_HOST, 0))
|
|
213
|
+
local_sock.listen(1)
|
|
214
|
+
self.host = _LOOPBACK_HOST
|
|
215
|
+
self.port = local_sock.getsockname()[1]
|
|
216
|
+
logger.debug(
|
|
217
|
+
f"['{self._ip_address}']: Created TCP loopback listener "
|
|
218
|
+
f"on {self.host}:{self.port}"
|
|
219
|
+
)
|
|
220
|
+
return local_sock
|
|
221
|
+
|
|
222
|
+
self._tmpdir = tempfile.mkdtemp()
|
|
223
|
+
self._socket_path = os.path.join(self._tmpdir, _UNIX_SOCKET_NAME)
|
|
224
|
+
logger.debug(
|
|
225
|
+
f"['{self._ip_address}']: Created Unix socket at {self._socket_path}"
|
|
226
|
+
)
|
|
227
|
+
local_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
228
|
+
local_sock.bind(self._socket_path)
|
|
229
|
+
local_sock.listen(1)
|
|
230
|
+
self.host = self._tmpdir
|
|
231
|
+
self.port = _DEFAULT_PGSQL_PORT
|
|
232
|
+
return local_sock
|
|
233
|
+
|
|
234
|
+
def _accept_and_proxy(self) -> None:
|
|
235
|
+
"""Accept one connection then proxy bytes until the connection closes."""
|
|
236
|
+
local_conn: socket.socket | None = None
|
|
237
|
+
try:
|
|
238
|
+
local_conn, _ = self._local_sock.accept()
|
|
239
|
+
self._local_sock.close()
|
|
240
|
+
logger.debug(
|
|
241
|
+
f"['{self._ip_address}']: Accepted local connection, starting proxy"
|
|
242
|
+
)
|
|
243
|
+
if isinstance(self._remote_sock, SqlDataSocket):
|
|
244
|
+
_proxy_sqldata(local_conn, self._remote_sock)
|
|
245
|
+
else:
|
|
246
|
+
_proxy(local_conn, self._remote_sock)
|
|
247
|
+
except Exception as e: # noqa: BLE001
|
|
248
|
+
logger.debug(f"['{self._ip_address}']: Error in accept/proxy thread: {e}")
|
|
249
|
+
if local_conn is not None:
|
|
250
|
+
_close_socket(local_conn)
|
|
251
|
+
_close_socket(self._remote_sock)
|
|
252
|
+
|
|
253
|
+
def _cleanup_socket_files(self) -> None:
|
|
254
|
+
"""Remove temporary Unix socket file and directory if created."""
|
|
255
|
+
if self._socket_path:
|
|
110
256
|
try:
|
|
111
|
-
|
|
257
|
+
os.remove(self._socket_path)
|
|
112
258
|
except OSError:
|
|
113
259
|
pass
|
|
260
|
+
if self._tmpdir:
|
|
114
261
|
try:
|
|
115
|
-
|
|
262
|
+
os.rmdir(self._tmpdir)
|
|
116
263
|
except OSError:
|
|
117
264
|
pass
|
|
118
265
|
|
|
119
266
|
|
|
120
267
|
def connect(
|
|
121
|
-
ip_address: str,
|
|
268
|
+
ip_address: str,
|
|
269
|
+
remote_sock: "ssl.SSLSocket | SqlDataSocket",
|
|
270
|
+
**kwargs: Any,
|
|
122
271
|
) -> "psycopg.Connection":
|
|
123
272
|
"""Create a psycopg DBAPI connection object.
|
|
124
273
|
|
|
125
274
|
Because psycopg does not accept a pre-connected socket, this function
|
|
126
|
-
creates a
|
|
127
|
-
and runs a background proxy that forwards bytes
|
|
128
|
-
the already-established Cloud SQL
|
|
275
|
+
creates a local listener (Unix domain socket on Unix, TCP loopback on Windows),
|
|
276
|
+
tells psycopg to connect there, and runs a background proxy that forwards bytes
|
|
277
|
+
between that socket and the already-established Cloud SQL connection.
|
|
129
278
|
|
|
130
279
|
Args:
|
|
131
280
|
ip_address (str): IP address of the Cloud SQL instance.
|
|
132
|
-
remote_sock (ssl.SSLSocket):
|
|
133
|
-
Cloud SQL
|
|
281
|
+
remote_sock (ssl.SSLSocket | SqlDataSocket): Secure socket stream
|
|
282
|
+
connected to the Cloud SQL instance.
|
|
134
283
|
|
|
135
284
|
Returns:
|
|
136
285
|
psycopg.Connection: A psycopg Connection object for the Cloud SQL instance.
|
|
@@ -142,94 +291,33 @@ def connect(
|
|
|
142
291
|
'Unable to import module "psycopg." Please install and try again.'
|
|
143
292
|
)
|
|
144
293
|
|
|
145
|
-
if not hasattr(socket, "AF_UNIX"):
|
|
146
|
-
raise NotImplementedError(
|
|
147
|
-
"Unix domain sockets (AF_UNIX) are not supported on this platform"
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
tmpdir = tempfile.mkdtemp()
|
|
151
|
-
socket_path = os.path.join(tmpdir, ".s.PGSQL.5432")
|
|
152
|
-
logger.debug("psycopg: created Unix socket at %s", socket_path)
|
|
153
|
-
|
|
154
|
-
local_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
155
|
-
local_sock.bind(socket_path)
|
|
156
|
-
local_sock.listen(1)
|
|
157
|
-
|
|
158
|
-
def _accept_and_proxy() -> None:
|
|
159
|
-
"""Accept one connection then proxy bytes until the connection closes."""
|
|
160
|
-
unix_conn = None
|
|
161
|
-
try:
|
|
162
|
-
unix_conn, _ = local_sock.accept()
|
|
163
|
-
local_sock.close()
|
|
164
|
-
logger.debug("psycopg proxy: accepted connection, starting proxy")
|
|
165
|
-
_proxy(unix_conn, remote_sock)
|
|
166
|
-
except Exception as e: # noqa: BLE001
|
|
167
|
-
logger.debug("psycopg proxy: error in accept/proxy thread: %s", e)
|
|
168
|
-
# Ensure cleanup on any exception
|
|
169
|
-
if unix_conn:
|
|
170
|
-
try:
|
|
171
|
-
unix_conn.shutdown(socket.SHUT_RDWR)
|
|
172
|
-
except OSError:
|
|
173
|
-
pass
|
|
174
|
-
try:
|
|
175
|
-
unix_conn.close()
|
|
176
|
-
except OSError:
|
|
177
|
-
pass
|
|
178
|
-
try:
|
|
179
|
-
remote_sock.shutdown(socket.SHUT_RDWR)
|
|
180
|
-
except OSError:
|
|
181
|
-
pass
|
|
182
|
-
try:
|
|
183
|
-
remote_sock.close()
|
|
184
|
-
except OSError:
|
|
185
|
-
pass
|
|
186
|
-
|
|
187
|
-
threading.Thread(target=_accept_and_proxy, daemon=True).start()
|
|
188
|
-
|
|
189
294
|
user = kwargs.pop("user")
|
|
190
295
|
db = kwargs.pop("db")
|
|
191
296
|
passwd = kwargs.pop("password", None)
|
|
192
297
|
# SSL is already handled by the underlying SSLSocket; disable it on the
|
|
193
|
-
#
|
|
298
|
+
# local socket so psycopg does not attempt a second TLS handshake.
|
|
194
299
|
kwargs.pop("sslmode", None)
|
|
195
300
|
timeout = kwargs.pop("timeout", None)
|
|
196
301
|
if timeout is not None:
|
|
197
302
|
kwargs["connect_timeout"] = int(timeout)
|
|
198
303
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
dbname=db,
|
|
204
|
-
password=passwd,
|
|
205
|
-
host=tmpdir,
|
|
206
|
-
port=5432,
|
|
207
|
-
sslmode="disable",
|
|
208
|
-
**kwargs,
|
|
304
|
+
with _PsycopgProxy(ip_address, remote_sock) as proxy:
|
|
305
|
+
logger.debug(
|
|
306
|
+
f"['{ip_address}']: Connecting as user={user} dbname={db} "
|
|
307
|
+
f"to {proxy.host}:{proxy.port}"
|
|
209
308
|
)
|
|
210
|
-
logger.debug("psycopg: connection established")
|
|
211
|
-
return conn
|
|
212
|
-
except Exception as e:
|
|
213
|
-
logger.debug("psycopg: connection failed: %s", e)
|
|
214
|
-
# psycopg never connected (or failed mid-handshake); close the server
|
|
215
|
-
# socket so the proxy thread unblocks and exits cleanly.
|
|
216
309
|
try:
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
pass
|
|
232
|
-
try:
|
|
233
|
-
os.rmdir(tmpdir)
|
|
234
|
-
except OSError:
|
|
235
|
-
pass
|
|
310
|
+
conn = psycopg.connect(
|
|
311
|
+
user=user,
|
|
312
|
+
dbname=db,
|
|
313
|
+
password=passwd,
|
|
314
|
+
host=proxy.host,
|
|
315
|
+
port=proxy.port,
|
|
316
|
+
sslmode="disable",
|
|
317
|
+
**kwargs,
|
|
318
|
+
)
|
|
319
|
+
logger.debug(f"['{ip_address}']: Connection established")
|
|
320
|
+
return conn
|
|
321
|
+
except Exception as e:
|
|
322
|
+
logger.debug(f"['{ip_address}']: Connection failed: {e}")
|
|
323
|
+
raise
|
|
@@ -17,11 +17,12 @@ limitations under the License.
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import asyncio
|
|
20
|
+
from collections.abc import Callable
|
|
20
21
|
import copy
|
|
21
22
|
import datetime
|
|
22
23
|
import logging
|
|
23
24
|
import random
|
|
24
|
-
from typing import Any
|
|
25
|
+
from typing import Any
|
|
25
26
|
|
|
26
27
|
import aiohttp
|
|
27
28
|
from google.auth.credentials import Credentials
|
|
@@ -52,7 +53,7 @@ def _seconds_until_refresh(
|
|
|
52
53
|
"""
|
|
53
54
|
|
|
54
55
|
duration = int(
|
|
55
|
-
(expiration - datetime.datetime.now(datetime.
|
|
56
|
+
(expiration - datetime.datetime.now(datetime.UTC)).total_seconds()
|
|
56
57
|
)
|
|
57
58
|
|
|
58
59
|
# if certificate duration is less than 1 hour
|
|
@@ -70,7 +71,7 @@ async def _is_valid(task: asyncio.Task) -> bool:
|
|
|
70
71
|
try:
|
|
71
72
|
metadata = await task
|
|
72
73
|
# only valid if now is before the cert expires
|
|
73
|
-
if datetime.datetime.now(datetime.
|
|
74
|
+
if datetime.datetime.now(datetime.UTC) < metadata.expiration:
|
|
74
75
|
return True
|
|
75
76
|
except Exception: # noqa: BLE001
|
|
76
77
|
# supress any errors from task
|