cloud-sql-python-connector 1.17.0.dev0__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.
@@ -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=emahfH9ZSrbooFBV-m6gcwGJ7yYQwaUJuCJPz0DHfHA,22547
7
+ google/cloud/sql/connector/enums.py,sha256=dVBS9eEWNrHtzze0AJlmtbdUogG3EocNWOQcI_8qPd0,2937
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=YfNpSkpcPhRwrFt2dV7ut5xGe0KjgvNinweha483X2w,603
20
+ cloud_sql_python_connector-1.17.0.dev0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
21
+ cloud_sql_python_connector-1.17.0.dev0.dist-info/METADATA,sha256=qlls3nyZF0ZxHsLxEvorF9ukUEyY5pQpWUhvbQ0H2tk,30494
22
+ cloud_sql_python_connector-1.17.0.dev0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
23
+ cloud_sql_python_connector-1.17.0.dev0.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
24
+ cloud_sql_python_connector-1.17.0.dev0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.8.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,33 @@
1
+ """"
2
+ Copyright 2019 Google LLC
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ https://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ """
16
+
17
+ from google.cloud.sql.connector.connector import Connector
18
+ from google.cloud.sql.connector.connector import create_async_connector
19
+ from google.cloud.sql.connector.enums import IPTypes
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
23
+ from google.cloud.sql.connector.version import __version__
24
+
25
+ __all__ = [
26
+ "__version__",
27
+ "create_async_connector",
28
+ "Connector",
29
+ "DefaultResolver",
30
+ "DnsResolver",
31
+ "IPTypes",
32
+ "RefreshStrategy",
33
+ ]
@@ -0,0 +1,65 @@
1
+ """
2
+ Copyright 2022 Google LLC
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ https://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ """
16
+
17
+ import ssl
18
+ from typing import Any, TYPE_CHECKING
19
+
20
+ SERVER_PROXY_PORT = 3307
21
+
22
+ if TYPE_CHECKING:
23
+ import asyncpg
24
+
25
+
26
+ async def connect(
27
+ ip_address: str, ctx: ssl.SSLContext, **kwargs: Any
28
+ ) -> "asyncpg.Connection":
29
+ """Helper function to create an asyncpg DB-API connection object.
30
+
31
+ :type ip_address: str
32
+ :param ip_address: A string containing an IP address for the Cloud SQL
33
+ instance.
34
+
35
+ :type ctx: ssl.SSLContext
36
+ :param ctx: An SSLContext object created from the Cloud SQL server CA
37
+ cert and ephemeral cert.
38
+
39
+ :type kwargs: Any
40
+ :param kwargs: Keyword arguments for establishing asyncpg connection
41
+ object to Cloud SQL instance.
42
+
43
+ :rtype: asyncpg.Connection
44
+ :returns: An asyncpg.Connection object to a Cloud SQL instance.
45
+ """
46
+ try:
47
+ import asyncpg
48
+ except ImportError:
49
+ raise ImportError(
50
+ 'Unable to import module "asyncpg." Please install and try again.'
51
+ )
52
+ user = kwargs.pop("user")
53
+ db = kwargs.pop("db")
54
+ passwd = kwargs.pop("password", None)
55
+
56
+ return await asyncpg.connect(
57
+ user=user,
58
+ database=db,
59
+ password=passwd,
60
+ host=ip_address,
61
+ port=SERVER_PROXY_PORT,
62
+ ssl=ctx,
63
+ direct_tls=True,
64
+ **kwargs,
65
+ )
@@ -0,0 +1,324 @@
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 __future__ import annotations
16
+
17
+ import asyncio
18
+ import datetime
19
+ import logging
20
+ from typing import Any, Optional, TYPE_CHECKING
21
+
22
+ import aiohttp
23
+ from cryptography.hazmat.backends import default_backend
24
+ from cryptography.x509 import load_pem_x509_certificate
25
+ from google.auth.credentials import TokenState
26
+ from google.auth.transport import requests
27
+
28
+ from google.cloud.sql.connector.connection_info import ConnectionInfo
29
+ from google.cloud.sql.connector.connection_name import ConnectionName
30
+ from google.cloud.sql.connector.exceptions import AutoIAMAuthNotSupported
31
+ from google.cloud.sql.connector.refresh_utils import _downscope_credentials
32
+ from google.cloud.sql.connector.refresh_utils import retry_50x
33
+ from google.cloud.sql.connector.version import __version__ as version
34
+
35
+ if TYPE_CHECKING:
36
+ from google.auth.credentials import Credentials
37
+
38
+ USER_AGENT: str = f"cloud-sql-python-connector/{version}"
39
+ API_VERSION: str = "v1beta4"
40
+ DEFAULT_SERVICE_ENDPOINT: str = "https://sqladmin.googleapis.com"
41
+
42
+ logger = logging.getLogger(name=__name__)
43
+
44
+
45
+ def _format_user_agent(driver: Optional[str], custom: Optional[str]) -> str:
46
+ agent = f"{USER_AGENT}+{driver}" if driver else USER_AGENT
47
+ if custom and isinstance(custom, str):
48
+ agent = f"{agent} {custom}"
49
+ return agent
50
+
51
+
52
+ class CloudSQLClient:
53
+ def __init__(
54
+ self,
55
+ sqladmin_api_endpoint: Optional[str],
56
+ quota_project: Optional[str],
57
+ credentials: Credentials,
58
+ client: Optional[aiohttp.ClientSession] = None,
59
+ driver: Optional[str] = None,
60
+ user_agent: Optional[str] = None,
61
+ ) -> None:
62
+ """
63
+ Establish the client to be used for Cloud SQL Admin API requests.
64
+
65
+ Args:
66
+ sqladmin_api_endpoint (str): Base URL to use when calling
67
+ the Cloud SQL Admin API endpoints.
68
+ quota_project (str): The Project ID for an existing Google Cloud
69
+ project. The project specified is used for quota and
70
+ billing purposes.
71
+ credentials (google.auth.credentials.Credentials):
72
+ A credentials object created from the google-auth Python library.
73
+ Must have the Cloud SQL Admin scopes. For more info check out
74
+ https://google-auth.readthedocs.io/en/latest/.
75
+ client (aiohttp.ClientSession): Async client used to make requests to
76
+ Cloud SQL Admin APIs.
77
+ Optional, defaults to None and creates new client.
78
+ driver (str): Database driver to be used by the client.
79
+ """
80
+ user_agent = _format_user_agent(driver, user_agent)
81
+ headers = {
82
+ "x-goog-api-client": user_agent,
83
+ "User-Agent": user_agent,
84
+ "Content-Type": "application/json",
85
+ }
86
+ if quota_project:
87
+ headers["x-goog-user-project"] = quota_project
88
+
89
+ self._client = client if client else aiohttp.ClientSession(headers=headers)
90
+ self._credentials = credentials
91
+ if sqladmin_api_endpoint is None:
92
+ self._sqladmin_api_endpoint = DEFAULT_SERVICE_ENDPOINT
93
+ else:
94
+ self._sqladmin_api_endpoint = sqladmin_api_endpoint
95
+ self._user_agent = user_agent
96
+
97
+ async def _get_metadata(
98
+ self,
99
+ project: str,
100
+ region: str,
101
+ instance: str,
102
+ ) -> dict[str, Any]:
103
+ """Requests metadata from the Cloud SQL Instance
104
+ and returns a dictionary containing the IP addresses and certificate
105
+ authority of the Cloud SQL Instance.
106
+
107
+ :type project: str
108
+ :param project:
109
+ A string representing the name of the project.
110
+
111
+ :type region: str
112
+ :param region : A string representing the name of the region.
113
+
114
+ :type instance: str
115
+ :param instance: A string representing the name of the instance.
116
+
117
+ :rtype: dict[str: Union[dict, str]]
118
+ :returns: Returns a dictionary containing a dictionary of all IP
119
+ addresses and their type and a string representing the
120
+ certificate authority.
121
+ """
122
+
123
+ headers = {
124
+ "Authorization": f"Bearer {self._credentials.token}",
125
+ }
126
+
127
+ url = f"{self._sqladmin_api_endpoint}/sql/{API_VERSION}/projects/{project}/instances/{instance}/connectSettings"
128
+
129
+ resp = await self._client.get(url, headers=headers)
130
+ if resp.status >= 500:
131
+ resp = await retry_50x(self._client.get, url, headers=headers)
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()
145
+
146
+ if ret_dict["region"] != region:
147
+ raise ValueError(
148
+ f'[{project}:{region}:{instance}]: Provided region was mismatched - got region {region}, expected {ret_dict["region"]}.'
149
+ )
150
+
151
+ ip_addresses = (
152
+ {ip["type"]: ip["ipAddress"] for ip in ret_dict["ipAddresses"]}
153
+ if "ipAddresses" in ret_dict
154
+ else {}
155
+ )
156
+ # resolve dnsName into IP address for PSC
157
+ # Note that we have to check for PSC enablement also because CAS
158
+ # instances also set the dnsName field.
159
+ # Remove trailing period from DNS name. Required for SSL in Python
160
+ dns_name = ret_dict.get("dnsName", "").rstrip(".")
161
+ if dns_name and ret_dict.get("pscEnabled"):
162
+ ip_addresses["PSC"] = dns_name
163
+
164
+ return {
165
+ "ip_addresses": ip_addresses,
166
+ "server_ca_cert": ret_dict["serverCaCert"]["cert"],
167
+ "database_version": ret_dict["databaseVersion"],
168
+ }
169
+
170
+ async def _get_ephemeral(
171
+ self,
172
+ project: str,
173
+ instance: str,
174
+ pub_key: str,
175
+ enable_iam_auth: bool = False,
176
+ ) -> tuple[str, datetime.datetime]:
177
+ """Asynchronously requests an ephemeral certificate from the Cloud SQL Instance.
178
+
179
+ :type project: str
180
+ :param project : A string representing the name of the project.
181
+
182
+ :type instance: str
183
+ :param instance: A string representing the name of the instance.
184
+
185
+ :type pub_key:
186
+ :param str: A string representing PEM-encoded RSA public key.
187
+
188
+ :type enable_iam_auth: bool
189
+ :param enable_iam_auth
190
+ Enables automatic IAM database authentication for Postgres or MySQL
191
+ instances.
192
+
193
+ :rtype: str
194
+ :returns: An ephemeral certificate from the Cloud SQL instance that allows
195
+ authorized connections to the instance.
196
+ """
197
+ headers = {
198
+ "Authorization": f"Bearer {self._credentials.token}",
199
+ }
200
+
201
+ url = f"{self._sqladmin_api_endpoint}/sql/{API_VERSION}/projects/{project}/instances/{instance}:generateEphemeralCert"
202
+
203
+ data = {"public_key": pub_key}
204
+
205
+ if enable_iam_auth:
206
+ # down-scope credentials with only IAM login scope (refreshes them too)
207
+ login_creds = _downscope_credentials(self._credentials)
208
+ data["access_token"] = login_creds.token
209
+
210
+ resp = await self._client.post(url, headers=headers, json=data)
211
+ if resp.status >= 500:
212
+ resp = await retry_50x(self._client.post, url, headers=headers, json=data)
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()
226
+
227
+ ephemeral_cert: str = ret_dict["ephemeralCert"]["cert"]
228
+
229
+ # decode cert to read expiration
230
+ x509 = load_pem_x509_certificate(
231
+ ephemeral_cert.encode("UTF-8"), default_backend()
232
+ )
233
+ expiration = x509.not_valid_after_utc
234
+ # for IAM authentication OAuth2 token is embedded in cert so it
235
+ # must still be valid for successful connection
236
+ if enable_iam_auth:
237
+ token_expiration: datetime.datetime = login_creds.expiry
238
+ # google.auth library strips timezone info for backwards compatibality
239
+ # reasons with Python 2. Add it back to allow timezone aware datetimes.
240
+ # Ref: https://github.com/googleapis/google-auth-library-python/blob/49a5ff7411a2ae4d32a7d11700f9f961c55406a9/google/auth/_helpers.py#L93-L99
241
+ token_expiration = token_expiration.replace(tzinfo=datetime.timezone.utc)
242
+
243
+ if expiration > token_expiration:
244
+ expiration = token_expiration
245
+ return ephemeral_cert, expiration
246
+
247
+ async def get_connection_info(
248
+ self,
249
+ conn_name: ConnectionName,
250
+ keys: asyncio.Future,
251
+ enable_iam_auth: bool,
252
+ ) -> ConnectionInfo:
253
+ """Immediately performs a full refresh operation using the Cloud SQL
254
+ Admin API.
255
+
256
+ Args:
257
+ conn_name (ConnectionName): The Cloud SQL instance's
258
+ connection name.
259
+ keys (asyncio.Future): A future to the client's public-private key
260
+ pair.
261
+ enable_iam_auth (bool): Whether an automatic IAM database
262
+ authentication connection is being requested (Postgres and MySQL).
263
+
264
+ Returns:
265
+ ConnectionInfo: All the information required to connect securely to
266
+ the Cloud SQL instance.
267
+ Raises:
268
+ AutoIAMAuthNotSupported: Database engine does not support automatic
269
+ IAM authentication.
270
+ """
271
+ priv_key, pub_key = await keys
272
+ # before making Cloud SQL Admin API calls, refresh creds if required
273
+ if not self._credentials.token_state == TokenState.FRESH:
274
+ self._credentials.refresh(requests.Request())
275
+
276
+ metadata_task = asyncio.create_task(
277
+ self._get_metadata(
278
+ conn_name.project,
279
+ conn_name.region,
280
+ conn_name.instance_name,
281
+ )
282
+ )
283
+
284
+ ephemeral_task = asyncio.create_task(
285
+ self._get_ephemeral(
286
+ conn_name.project,
287
+ conn_name.instance_name,
288
+ pub_key,
289
+ enable_iam_auth,
290
+ )
291
+ )
292
+ try:
293
+ metadata = await metadata_task
294
+ # check if automatic IAM database authn is supported for database engine
295
+ if enable_iam_auth and not metadata["database_version"].startswith(
296
+ ("POSTGRES", "MYSQL")
297
+ ):
298
+ raise AutoIAMAuthNotSupported(
299
+ f"'{metadata['database_version']}' does not support "
300
+ "automatic IAM authentication. It is only supported with "
301
+ "Cloud SQL Postgres or MySQL instances."
302
+ )
303
+ except Exception:
304
+ # cancel ephemeral cert task if exception occurs before it is awaited
305
+ ephemeral_task.cancel()
306
+ raise
307
+
308
+ ephemeral_cert, expiration = await ephemeral_task
309
+
310
+ return ConnectionInfo(
311
+ conn_name,
312
+ ephemeral_cert,
313
+ metadata["server_ca_cert"],
314
+ priv_key,
315
+ metadata["ip_addresses"],
316
+ metadata["database_version"],
317
+ expiration,
318
+ )
319
+
320
+ async def close(self) -> None:
321
+ """Close CloudSQLClient gracefully."""
322
+ logger.debug("Waiting for Connector's http client to close")
323
+ await self._client.close()
324
+ logger.debug("Closed Connector's http client")
@@ -0,0 +1,108 @@
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 __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+ import logging
19
+ import ssl
20
+ from typing import Any, Optional, TYPE_CHECKING
21
+
22
+ from aiofiles.tempfile import TemporaryDirectory
23
+
24
+ from google.cloud.sql.connector.connection_name import ConnectionName
25
+ from google.cloud.sql.connector.exceptions import CloudSQLIPTypeError
26
+ from google.cloud.sql.connector.exceptions import TLSVersionError
27
+ from google.cloud.sql.connector.utils import write_to_file
28
+
29
+ if TYPE_CHECKING:
30
+ import datetime
31
+
32
+ from google.cloud.sql.connector.enums import IPTypes
33
+
34
+ logger = logging.getLogger(name=__name__)
35
+
36
+
37
+ @dataclass
38
+ class ConnectionInfo:
39
+ """Contains all necessary information to connect securely to the
40
+ server-side Proxy running on a Cloud SQL instance."""
41
+
42
+ conn_name: ConnectionName
43
+ client_cert: str
44
+ server_ca_cert: str
45
+ private_key: bytes
46
+ ip_addrs: dict[str, Any]
47
+ database_version: str
48
+ expiration: datetime.datetime
49
+ context: Optional[ssl.SSLContext] = None
50
+
51
+ async def create_ssl_context(self, enable_iam_auth: bool = False) -> ssl.SSLContext:
52
+ """Constructs a SSL/TLS context for the given connection info.
53
+
54
+ Cache the SSL context to ensure we don't read from disk repeatedly when
55
+ configuring a secure connection.
56
+ """
57
+ # if SSL context is cached, use it
58
+ if self.context is not None:
59
+ return self.context
60
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
61
+
62
+ # update ssl.PROTOCOL_TLS_CLIENT default
63
+ context.check_hostname = False
64
+
65
+ # TODO: remove if/else when Python 3.10 is min version. PEP 644 has been
66
+ # implemented. The ssl module requires OpenSSL 1.1.1 or newer.
67
+ # verify OpenSSL version supports TLSv1.3
68
+ if ssl.HAS_TLSv1_3:
69
+ # force TLSv1.3 if supported by client
70
+ context.minimum_version = ssl.TLSVersion.TLSv1_3
71
+ # fallback to TLSv1.2 for older versions of OpenSSL
72
+ else:
73
+ if enable_iam_auth:
74
+ raise TLSVersionError(
75
+ f"Your current version of OpenSSL ({ssl.OPENSSL_VERSION}) does not "
76
+ "support TLSv1.3, which is required to use IAM Authentication.\n"
77
+ "Upgrade your OpenSSL version to 1.1.1 for TLSv1.3 support."
78
+ )
79
+ logger.warning(
80
+ "TLSv1.3 is not supported with your version of OpenSSL "
81
+ f"({ssl.OPENSSL_VERSION}), falling back to TLSv1.2\n"
82
+ "Upgrade your OpenSSL version to 1.1.1 for TLSv1.3 support."
83
+ )
84
+ context.minimum_version = ssl.TLSVersion.TLSv1_2
85
+
86
+ # tmpdir and its contents are automatically deleted after the CA cert
87
+ # and ephemeral cert are loaded into the SSLcontext. The values
88
+ # need to be written to files in order to be loaded by the SSLContext
89
+ async with TemporaryDirectory() as tmpdir:
90
+ ca_filename, cert_filename, key_filename = await write_to_file(
91
+ tmpdir, self.server_ca_cert, self.client_cert, self.private_key
92
+ )
93
+ context.load_cert_chain(cert_filename, keyfile=key_filename)
94
+ context.load_verify_locations(cafile=ca_filename)
95
+ # set class attribute to cache context for subsequent calls
96
+ self.context = context
97
+ return context
98
+
99
+ def get_preferred_ip(self, ip_type: IPTypes) -> str:
100
+ """Returns the first IP address for the instance, according to the preference
101
+ supplied by ip_type. If no IP addressess with the given preference are found,
102
+ an error is raised."""
103
+ if ip_type.value in self.ip_addrs:
104
+ return self.ip_addrs[ip_type.value]
105
+ raise CloudSQLIPTypeError(
106
+ "Cloud SQL instance does not have any IP addresses matching "
107
+ f"preference: {ip_type.value}"
108
+ )
@@ -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
+ )