google-cloud-alloydb-connector 1.8.1.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.
- google/api/field_behavior_pb2.py +52 -0
- google/cloud/alloydb/connector/__init__.py +26 -0
- google/cloud/alloydb/connector/async_connector.py +245 -0
- google/cloud/alloydb/connector/asyncpg.py +63 -0
- google/cloud/alloydb/connector/client.py +279 -0
- google/cloud/alloydb/connector/connection_info.py +87 -0
- google/cloud/alloydb/connector/connector.py +399 -0
- google/cloud/alloydb/connector/enums.py +50 -0
- google/cloud/alloydb/connector/exceptions.py +25 -0
- google/cloud/alloydb/connector/instance.py +242 -0
- google/cloud/alloydb/connector/lazy.py +127 -0
- google/cloud/alloydb/connector/pg8000.py +49 -0
- google/cloud/alloydb/connector/py.typed +0 -0
- google/cloud/alloydb/connector/rate_limiter.py +77 -0
- google/cloud/alloydb/connector/refresh_utils.py +62 -0
- google/cloud/alloydb/connector/static.py +102 -0
- google/cloud/alloydb/connector/types.py +23 -0
- google/cloud/alloydb/connector/utils.py +72 -0
- google/cloud/alloydb/connector/version.py +15 -0
- google/cloud/alloydb_connectors_v1/proto/__init__.py +13 -0
- google/cloud/alloydb_connectors_v1/proto/resources_pb2.py +61 -0
- google/cloud/alloydb_connectors_v1/proto/resources_pb2.pyi +66 -0
- google_cloud_alloydb_connector-1.8.1.dev0.dist-info/METADATA +572 -0
- google_cloud_alloydb_connector-1.8.1.dev0.dist-info/RECORD +27 -0
- google_cloud_alloydb_connector-1.8.1.dev0.dist-info/WHEEL +5 -0
- google_cloud_alloydb_connector-1.8.1.dev0.dist-info/licenses/LICENSE +201 -0
- google_cloud_alloydb_connector-1.8.1.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
# type: ignore
|
|
16
|
+
# -*- coding: utf-8 -*-
|
|
17
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
18
|
+
# source: google/api/field_behavior.proto
|
|
19
|
+
# isort: skip_file
|
|
20
|
+
"""Generated protocol buffer code."""
|
|
21
|
+
from google.protobuf import descriptor as _descriptor
|
|
22
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
23
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
24
|
+
from google.protobuf.internal import builder as _builder
|
|
25
|
+
|
|
26
|
+
# @@protoc_insertion_point(imports)
|
|
27
|
+
|
|
28
|
+
_sym_db = _symbol_database.Default()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
|
|
35
|
+
b"\n\x1fgoogle/api/field_behavior.proto\x12\ngoogle.api\x1a google/protobuf/descriptor.proto*\xb6\x01\n\rFieldBehavior\x12\x1e\n\x1a\x46IELD_BEHAVIOR_UNSPECIFIED\x10\x00\x12\x0c\n\x08OPTIONAL\x10\x01\x12\x0c\n\x08REQUIRED\x10\x02\x12\x0f\n\x0bOUTPUT_ONLY\x10\x03\x12\x0e\n\nINPUT_ONLY\x10\x04\x12\r\n\tIMMUTABLE\x10\x05\x12\x12\n\x0eUNORDERED_LIST\x10\x06\x12\x15\n\x11NON_EMPTY_DEFAULT\x10\x07\x12\x0e\n\nIDENTIFIER\x10\x08:Q\n\x0e\x66ield_behavior\x12\x1d.google.protobuf.FieldOptions\x18\x9c\x08 \x03(\x0e\x32\x19.google.api.FieldBehaviorBp\n\x0e\x63om.google.apiB\x12\x46ieldBehaviorProtoP\x01ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\xa2\x02\x04GAPIb\x06proto3"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
_globals = globals()
|
|
39
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
40
|
+
_builder.BuildTopDescriptorsAndMessages(
|
|
41
|
+
DESCRIPTOR, "google.api.field_behavior_pb2", _globals
|
|
42
|
+
)
|
|
43
|
+
if _descriptor._USE_C_DESCRIPTORS == False:
|
|
44
|
+
google_dot_protobuf_dot_descriptor__pb2.FieldOptions.RegisterExtension(
|
|
45
|
+
field_behavior
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
DESCRIPTOR._options = None
|
|
49
|
+
DESCRIPTOR._serialized_options = b"\n\016com.google.apiB\022FieldBehaviorProtoP\001ZAgoogle.golang.org/genproto/googleapis/api/annotations;annotations\242\002\004GAPI"
|
|
50
|
+
_globals["_FIELDBEHAVIOR"]._serialized_start = 82
|
|
51
|
+
_globals["_FIELDBEHAVIOR"]._serialized_end = 264
|
|
52
|
+
# @@protoc_insertion_point(module_scope)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Copyright 2023 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
|
+
from google.cloud.alloydb.connector.async_connector import AsyncConnector
|
|
15
|
+
from google.cloud.alloydb.connector.connector import Connector
|
|
16
|
+
from google.cloud.alloydb.connector.enums import IPTypes
|
|
17
|
+
from google.cloud.alloydb.connector.enums import RefreshStrategy
|
|
18
|
+
from google.cloud.alloydb.connector.version import __version__
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"__version__",
|
|
22
|
+
"Connector",
|
|
23
|
+
"AsyncConnector",
|
|
24
|
+
"IPTypes",
|
|
25
|
+
"RefreshStrategy",
|
|
26
|
+
]
|
|
@@ -0,0 +1,245 @@
|
|
|
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 logging
|
|
19
|
+
from types import TracebackType
|
|
20
|
+
from typing import Any, Optional, TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
import google.auth
|
|
23
|
+
from google.auth.credentials import with_scopes_if_required
|
|
24
|
+
import google.auth.transport.requests
|
|
25
|
+
|
|
26
|
+
import google.cloud.alloydb.connector.asyncpg as asyncpg
|
|
27
|
+
from google.cloud.alloydb.connector.client import AlloyDBClient
|
|
28
|
+
from google.cloud.alloydb.connector.enums import IPTypes
|
|
29
|
+
from google.cloud.alloydb.connector.enums import RefreshStrategy
|
|
30
|
+
from google.cloud.alloydb.connector.exceptions import ClosedConnectorError
|
|
31
|
+
from google.cloud.alloydb.connector.instance import RefreshAheadCache
|
|
32
|
+
from google.cloud.alloydb.connector.lazy import LazyRefreshCache
|
|
33
|
+
from google.cloud.alloydb.connector.types import CacheTypes
|
|
34
|
+
from google.cloud.alloydb.connector.utils import generate_keys
|
|
35
|
+
from google.cloud.alloydb.connector.utils import strip_http_prefix
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from google.auth.credentials import Credentials
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(name=__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AsyncConnector:
|
|
44
|
+
"""A class to configure and create connections to Cloud SQL instances
|
|
45
|
+
asynchronously.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
credentials (google.auth.credentials.Credentials):
|
|
49
|
+
A credentials object created from the google-auth Python library.
|
|
50
|
+
If not specified, Application Default Credentials are used.
|
|
51
|
+
quota_project (str): The Project ID for an existing Google Cloud
|
|
52
|
+
project. The project specified is used for quota and
|
|
53
|
+
billing purposes.
|
|
54
|
+
Defaults to None, picking up project from environment.
|
|
55
|
+
alloydb_api_endpoint (str): Base URL to use when calling
|
|
56
|
+
the AlloyDB API endpoint. Defaults to "alloydb.googleapis.com".
|
|
57
|
+
enable_iam_auth (bool): Enables automatic IAM database authentication.
|
|
58
|
+
ip_type (str | IPTypes): Default IP type for all AlloyDB connections.
|
|
59
|
+
Defaults to IPTypes.PRIVATE ("PRIVATE") for private IP connections.
|
|
60
|
+
refresh_strategy (str | RefreshStrategy): The default refresh strategy
|
|
61
|
+
used to refresh SSL/TLS cert and instance metadata. Can be one
|
|
62
|
+
of the following: RefreshStrategy.LAZY ("LAZY") or
|
|
63
|
+
RefreshStrategy.BACKGROUND ("BACKGROUND").
|
|
64
|
+
Default: RefreshStrategy.BACKGROUND
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
credentials: Optional[Credentials] = None,
|
|
70
|
+
quota_project: Optional[str] = None,
|
|
71
|
+
alloydb_api_endpoint: str = "alloydb.googleapis.com",
|
|
72
|
+
enable_iam_auth: bool = False,
|
|
73
|
+
ip_type: str | IPTypes = IPTypes.PRIVATE,
|
|
74
|
+
user_agent: Optional[str] = None,
|
|
75
|
+
refresh_strategy: str | RefreshStrategy = RefreshStrategy.BACKGROUND,
|
|
76
|
+
) -> None:
|
|
77
|
+
self._cache: dict[str, CacheTypes] = {}
|
|
78
|
+
# initialize default params
|
|
79
|
+
self._quota_project = quota_project
|
|
80
|
+
self._alloydb_api_endpoint = strip_http_prefix(alloydb_api_endpoint)
|
|
81
|
+
self._enable_iam_auth = enable_iam_auth
|
|
82
|
+
# if ip_type is str, convert to IPTypes enum
|
|
83
|
+
if isinstance(ip_type, str):
|
|
84
|
+
ip_type = IPTypes(ip_type.upper())
|
|
85
|
+
self._ip_type = ip_type
|
|
86
|
+
# if refresh_strategy is str, convert to RefreshStrategy enum
|
|
87
|
+
if isinstance(refresh_strategy, str):
|
|
88
|
+
refresh_strategy = RefreshStrategy(refresh_strategy.upper())
|
|
89
|
+
self._refresh_strategy = refresh_strategy
|
|
90
|
+
self._user_agent = user_agent
|
|
91
|
+
# initialize credentials
|
|
92
|
+
scopes = ["https://www.googleapis.com/auth/cloud-platform"]
|
|
93
|
+
if credentials:
|
|
94
|
+
self._credentials = with_scopes_if_required(credentials, scopes=scopes)
|
|
95
|
+
# otherwise use application default credentials
|
|
96
|
+
else:
|
|
97
|
+
self._credentials, _ = google.auth.default(scopes=scopes)
|
|
98
|
+
|
|
99
|
+
# check if AsyncConnector is being initialized with event loop running
|
|
100
|
+
# Otherwise we will lazy init keys
|
|
101
|
+
try:
|
|
102
|
+
self._keys: Optional[asyncio.Task] = asyncio.create_task(generate_keys())
|
|
103
|
+
except RuntimeError:
|
|
104
|
+
self._keys = None
|
|
105
|
+
self._client: Optional[AlloyDBClient] = None
|
|
106
|
+
self._closed = False
|
|
107
|
+
|
|
108
|
+
async def connect(
|
|
109
|
+
self,
|
|
110
|
+
instance_uri: str,
|
|
111
|
+
driver: str,
|
|
112
|
+
**kwargs: Any,
|
|
113
|
+
) -> Any:
|
|
114
|
+
"""
|
|
115
|
+
Asynchronously prepares and returns a database connection object.
|
|
116
|
+
|
|
117
|
+
Starts tasks to refresh the certificates and get
|
|
118
|
+
AlloyDB instance IP address. Creates a secure TLS connection
|
|
119
|
+
to establish connection to AlloyDB instance.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
instance_uri (str): The instance URI of the AlloyDB instance.
|
|
123
|
+
ex. projects/<PROJECT>/locations/<REGION>/clusters/<CLUSTER>/instances/<INSTANCE>
|
|
124
|
+
driver (str): A string representing the database driver to connect
|
|
125
|
+
with. Supported drivers are asyncpg.
|
|
126
|
+
**kwargs: Pass in any database driver-specific arguments needed
|
|
127
|
+
to fine tune connection.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
connection: A DBAPI connection to the specified AlloyDB instance.
|
|
131
|
+
"""
|
|
132
|
+
if self._closed:
|
|
133
|
+
raise ClosedConnectorError(
|
|
134
|
+
"Connection attempt failed because the connector has already been closed."
|
|
135
|
+
)
|
|
136
|
+
if self._keys is None:
|
|
137
|
+
self._keys = asyncio.create_task(generate_keys())
|
|
138
|
+
if self._client is None:
|
|
139
|
+
# lazy init client as it has to be initialized in async context
|
|
140
|
+
self._client = AlloyDBClient(
|
|
141
|
+
self._alloydb_api_endpoint,
|
|
142
|
+
self._quota_project,
|
|
143
|
+
self._credentials,
|
|
144
|
+
user_agent=self._user_agent,
|
|
145
|
+
driver=driver,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
enable_iam_auth = kwargs.pop("enable_iam_auth", self._enable_iam_auth)
|
|
149
|
+
|
|
150
|
+
# use existing connection info if possible
|
|
151
|
+
if instance_uri in self._cache:
|
|
152
|
+
cache = self._cache[instance_uri]
|
|
153
|
+
else:
|
|
154
|
+
if self._refresh_strategy == RefreshStrategy.LAZY:
|
|
155
|
+
logger.debug(
|
|
156
|
+
f"['{instance_uri}']: Refresh strategy is set to lazy refresh"
|
|
157
|
+
)
|
|
158
|
+
cache = LazyRefreshCache(instance_uri, self._client, self._keys)
|
|
159
|
+
else:
|
|
160
|
+
logger.debug(
|
|
161
|
+
f"['{instance_uri}']: Refresh strategy is set to background"
|
|
162
|
+
" refresh"
|
|
163
|
+
)
|
|
164
|
+
cache = RefreshAheadCache(instance_uri, self._client, self._keys)
|
|
165
|
+
self._cache[instance_uri] = cache
|
|
166
|
+
logger.debug(f"['{instance_uri}']: Connection info added to cache")
|
|
167
|
+
|
|
168
|
+
connect_func = {
|
|
169
|
+
"asyncpg": asyncpg.connect,
|
|
170
|
+
}
|
|
171
|
+
# only accept supported database drivers
|
|
172
|
+
try:
|
|
173
|
+
connector = connect_func[driver]
|
|
174
|
+
except KeyError:
|
|
175
|
+
raise ValueError(f"Driver '{driver}' is not a supported database driver.")
|
|
176
|
+
|
|
177
|
+
# Host and ssl options come from the certificates and instance IP
|
|
178
|
+
# address so we don't want the user to specify them.
|
|
179
|
+
kwargs.pop("host", None)
|
|
180
|
+
kwargs.pop("ssl", None)
|
|
181
|
+
kwargs.pop("port", None)
|
|
182
|
+
|
|
183
|
+
# get connection info for AlloyDB instance
|
|
184
|
+
ip_type: str | IPTypes = kwargs.pop("ip_type", self._ip_type)
|
|
185
|
+
# if ip_type is str, convert to IPTypes enum
|
|
186
|
+
if isinstance(ip_type, str):
|
|
187
|
+
ip_type = IPTypes(ip_type.upper())
|
|
188
|
+
try:
|
|
189
|
+
conn_info = await cache.connect_info()
|
|
190
|
+
ip_address = conn_info.get_preferred_ip(ip_type)
|
|
191
|
+
except Exception:
|
|
192
|
+
# with an error from AlloyDB API call or IP type, invalidate the
|
|
193
|
+
# cache and re-raise the error
|
|
194
|
+
await self._remove_cached(instance_uri)
|
|
195
|
+
raise
|
|
196
|
+
logger.debug(f"['{instance_uri}']: Connecting to {ip_address}:5433")
|
|
197
|
+
|
|
198
|
+
# callable to be used for auto IAM authn
|
|
199
|
+
def get_authentication_token() -> str:
|
|
200
|
+
"""Get OAuth2 access token to be used for IAM database authentication"""
|
|
201
|
+
# refresh credentials if expired
|
|
202
|
+
if not self._credentials.valid:
|
|
203
|
+
request = google.auth.transport.requests.Request()
|
|
204
|
+
self._credentials.refresh(request)
|
|
205
|
+
return self._credentials.token
|
|
206
|
+
|
|
207
|
+
# if enable_iam_auth is set, use auth token as database password
|
|
208
|
+
if enable_iam_auth:
|
|
209
|
+
kwargs["password"] = get_authentication_token
|
|
210
|
+
try:
|
|
211
|
+
return await connector(
|
|
212
|
+
ip_address, await conn_info.create_ssl_context(), **kwargs
|
|
213
|
+
)
|
|
214
|
+
except Exception:
|
|
215
|
+
# we attempt a force refresh, then throw the error
|
|
216
|
+
await cache.force_refresh()
|
|
217
|
+
raise
|
|
218
|
+
|
|
219
|
+
async def _remove_cached(self, instance_uri: str) -> None:
|
|
220
|
+
"""Stops all background refreshes and deletes the connection
|
|
221
|
+
info cache from the map of caches.
|
|
222
|
+
"""
|
|
223
|
+
logger.debug(f"['{instance_uri}']: Removing connection info from cache")
|
|
224
|
+
# remove cache from stored caches and close it
|
|
225
|
+
cache = self._cache.pop(instance_uri)
|
|
226
|
+
await cache.close()
|
|
227
|
+
|
|
228
|
+
async def __aenter__(self) -> Any:
|
|
229
|
+
"""Enter async context manager by returning Connector object"""
|
|
230
|
+
return self
|
|
231
|
+
|
|
232
|
+
async def __aexit__(
|
|
233
|
+
self,
|
|
234
|
+
exc_type: Optional[type[BaseException]],
|
|
235
|
+
exc_val: Optional[BaseException],
|
|
236
|
+
exc_tb: Optional[TracebackType],
|
|
237
|
+
) -> None:
|
|
238
|
+
"""Exit async context manager by closing Connector"""
|
|
239
|
+
await self.close()
|
|
240
|
+
|
|
241
|
+
async def close(self) -> None:
|
|
242
|
+
"""Helper function to cancel RefreshAheadCaches' tasks
|
|
243
|
+
and close client."""
|
|
244
|
+
await asyncio.gather(*[cache.close() for cache in self._cache.values()])
|
|
245
|
+
self._closed = True
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
# https://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 ssl
|
|
16
|
+
from typing import Any, TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
SERVER_PROXY_PORT = 5433
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
import asyncpg
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def connect(
|
|
25
|
+
ip_address: str, ctx: ssl.SSLContext, **kwargs: Any
|
|
26
|
+
) -> "asyncpg.Connection":
|
|
27
|
+
"""Helper function to create an asyncpg DB-API connection object.
|
|
28
|
+
|
|
29
|
+
:type ip_address: str
|
|
30
|
+
:param ip_address: A string containing an IP address for the AlloyDB
|
|
31
|
+
instance.
|
|
32
|
+
|
|
33
|
+
:type ctx: ssl.SSLContext
|
|
34
|
+
:param ctx: An SSLContext object created from the AlloyDB server CA
|
|
35
|
+
cert and ephemeral cert.
|
|
36
|
+
|
|
37
|
+
:type kwargs: Any
|
|
38
|
+
:param kwargs: Keyword arguments for establishing asyncpg connection
|
|
39
|
+
object to AlloyDB instance.
|
|
40
|
+
|
|
41
|
+
:rtype: asyncpg.Connection
|
|
42
|
+
:returns: An asyncpg.Connection object to an AlloyDB instance.
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
import asyncpg
|
|
46
|
+
except ImportError:
|
|
47
|
+
raise ImportError(
|
|
48
|
+
'Unable to import module "asyncpg." Please install and try again.'
|
|
49
|
+
)
|
|
50
|
+
user = kwargs.pop("user")
|
|
51
|
+
db = kwargs.pop("db")
|
|
52
|
+
passwd = kwargs.pop("password")
|
|
53
|
+
|
|
54
|
+
return await asyncpg.connect(
|
|
55
|
+
user=user,
|
|
56
|
+
database=db,
|
|
57
|
+
password=passwd,
|
|
58
|
+
host=ip_address,
|
|
59
|
+
port=SERVER_PROXY_PORT,
|
|
60
|
+
ssl=ctx,
|
|
61
|
+
direct_tls=True,
|
|
62
|
+
**kwargs,
|
|
63
|
+
)
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# Copyright 2023 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 logging
|
|
19
|
+
from typing import Optional, TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
from cryptography import x509
|
|
22
|
+
from google.api_core.client_options import ClientOptions
|
|
23
|
+
from google.api_core.gapic_v1.client_info import ClientInfo
|
|
24
|
+
from google.auth.credentials import TokenState
|
|
25
|
+
from google.auth.transport import requests
|
|
26
|
+
import google.cloud.alloydb_v1beta as v1beta
|
|
27
|
+
from google.protobuf import duration_pb2
|
|
28
|
+
|
|
29
|
+
from google.cloud.alloydb.connector.connection_info import ConnectionInfo
|
|
30
|
+
from google.cloud.alloydb.connector.version import __version__ as version
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from google.auth.credentials import Credentials
|
|
34
|
+
|
|
35
|
+
USER_AGENT: str = f"alloydb-python-connector/{version}"
|
|
36
|
+
API_VERSION: str = "v1beta"
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(name=__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _format_user_agent(
|
|
42
|
+
driver: Optional[str],
|
|
43
|
+
custom_user_agent: Optional[str],
|
|
44
|
+
) -> str:
|
|
45
|
+
"""
|
|
46
|
+
Appends user-defined user agents to the base default agent.
|
|
47
|
+
"""
|
|
48
|
+
agent = f"{USER_AGENT}+{driver}" if driver else USER_AGENT
|
|
49
|
+
if custom_user_agent and isinstance(custom_user_agent, str):
|
|
50
|
+
agent = f"{agent} {custom_user_agent}"
|
|
51
|
+
|
|
52
|
+
return agent
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AlloyDBClient:
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
alloydb_api_endpoint: str,
|
|
59
|
+
quota_project: Optional[str],
|
|
60
|
+
credentials: Credentials,
|
|
61
|
+
client: Optional[v1beta.AlloyDBAdminAsyncClient] = None,
|
|
62
|
+
driver: Optional[str] = None,
|
|
63
|
+
user_agent: Optional[str] = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Establish the client to be used for AlloyDB API requests.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
alloydb_api_endpoint (str): Base URL to use when calling
|
|
70
|
+
the AlloyDB API endpoint.
|
|
71
|
+
quota_project (str): The Project ID for an existing Google Cloud
|
|
72
|
+
project. The project specified is used for quota and
|
|
73
|
+
billing purposes.
|
|
74
|
+
credentials (google.auth.credentials.Credentials):
|
|
75
|
+
A credentials object created from the google-auth Python library.
|
|
76
|
+
Must have the AlloyDB Admin scopes. For more info check out
|
|
77
|
+
https://google-auth.readthedocs.io/en/latest/.
|
|
78
|
+
client (v1beta.AlloyDBAdminAsyncClient): Async client used to make
|
|
79
|
+
requests to AlloyDB APIs.
|
|
80
|
+
Optional, defaults to None and creates new client.
|
|
81
|
+
driver (str): Database driver to be used by the client.
|
|
82
|
+
user_agent (str): The custom user-agent string to use in the HTTP
|
|
83
|
+
header when making requests to AlloyDB APIs.
|
|
84
|
+
Optional, defaults to None and uses a pre-defined one.
|
|
85
|
+
"""
|
|
86
|
+
user_agent = _format_user_agent(driver, user_agent)
|
|
87
|
+
|
|
88
|
+
# TODO(rhatgadkar-goog): Rollback the PR of deciding between creating
|
|
89
|
+
# AlloyDBAdminClient or AlloyDBAdminAsyncClient when either
|
|
90
|
+
# https://github.com/grpc/grpc/issues/25364 is resolved or an async REST
|
|
91
|
+
# transport for AlloyDBAdminAsyncClient gets introduced.
|
|
92
|
+
# The issue is that the async gRPC transport does not work with multiple
|
|
93
|
+
# event loops in the same process. So all calls to the AlloyDB Admin
|
|
94
|
+
# API, even from multiple threads, need to be made to a single-event
|
|
95
|
+
# loop. See https://github.com/GoogleCloudPlatform/alloydb-python-connector/issues/435
|
|
96
|
+
# for more details.
|
|
97
|
+
self._is_sync = False
|
|
98
|
+
if client:
|
|
99
|
+
self._client = client
|
|
100
|
+
elif driver == "pg8000":
|
|
101
|
+
self._client = v1beta.AlloyDBAdminClient(
|
|
102
|
+
credentials=credentials,
|
|
103
|
+
transport="grpc",
|
|
104
|
+
client_options=ClientOptions(
|
|
105
|
+
api_endpoint=alloydb_api_endpoint,
|
|
106
|
+
quota_project_id=quota_project,
|
|
107
|
+
),
|
|
108
|
+
client_info=ClientInfo(
|
|
109
|
+
user_agent=user_agent,
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
self._is_sync = True
|
|
113
|
+
else:
|
|
114
|
+
self._client = v1beta.AlloyDBAdminAsyncClient(
|
|
115
|
+
credentials=credentials,
|
|
116
|
+
transport="grpc_asyncio",
|
|
117
|
+
client_options=ClientOptions(
|
|
118
|
+
api_endpoint=alloydb_api_endpoint,
|
|
119
|
+
quota_project_id=quota_project,
|
|
120
|
+
),
|
|
121
|
+
client_info=ClientInfo(
|
|
122
|
+
user_agent=user_agent,
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
self._credentials = credentials
|
|
127
|
+
# asyncpg does not currently support using metadata exchange
|
|
128
|
+
# only use metadata exchange for pg8000 driver
|
|
129
|
+
self._use_metadata = True if driver == "pg8000" else False
|
|
130
|
+
self._user_agent = user_agent
|
|
131
|
+
|
|
132
|
+
async def _get_metadata(
|
|
133
|
+
self,
|
|
134
|
+
project: str,
|
|
135
|
+
region: str,
|
|
136
|
+
cluster: str,
|
|
137
|
+
name: str,
|
|
138
|
+
) -> dict[str, Optional[str]]:
|
|
139
|
+
"""
|
|
140
|
+
Fetch the metadata for a given AlloyDB instance.
|
|
141
|
+
|
|
142
|
+
Call the AlloyDB APIs connectInfo method to retrieve the
|
|
143
|
+
information about an AlloyDB instance that is used to create secure
|
|
144
|
+
connections.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
project (str): Google Cloud project ID that the AlloyDB instance
|
|
148
|
+
resides in.
|
|
149
|
+
region (str): Google Cloud region of the AlloyDB instance.
|
|
150
|
+
cluster (str): The name of the AlloyDB cluster.
|
|
151
|
+
name (str): The name of the AlloyDB instance.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
dict: IP addresses of the AlloyDB instance.
|
|
155
|
+
"""
|
|
156
|
+
parent = (
|
|
157
|
+
f"projects/{project}/locations/{region}/clusters/{cluster}/instances/{name}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
req = v1beta.GetConnectionInfoRequest(parent=parent)
|
|
161
|
+
if self._is_sync:
|
|
162
|
+
resp = self._client.get_connection_info(request=req)
|
|
163
|
+
else:
|
|
164
|
+
resp = await self._client.get_connection_info(request=req)
|
|
165
|
+
|
|
166
|
+
# Remove trailing period from PSC DNS name.
|
|
167
|
+
psc_dns = resp.psc_dns_name
|
|
168
|
+
if psc_dns:
|
|
169
|
+
psc_dns = psc_dns.rstrip(".")
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
"PRIVATE": resp.ip_address,
|
|
173
|
+
"PUBLIC": resp.public_ip_address,
|
|
174
|
+
"PSC": psc_dns,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async def _get_client_certificate(
|
|
178
|
+
self,
|
|
179
|
+
project: str,
|
|
180
|
+
region: str,
|
|
181
|
+
cluster: str,
|
|
182
|
+
pub_key: str,
|
|
183
|
+
) -> tuple[str, list[str]]:
|
|
184
|
+
"""
|
|
185
|
+
Fetch a client certificate for the given AlloyDB cluster.
|
|
186
|
+
|
|
187
|
+
Call the AlloyDB API's generateClientCertificate
|
|
188
|
+
method to create a signed TLS certificate that is authorized to connect via the
|
|
189
|
+
AlloyDB instance's serverside proxy. The cert is valid for twenty-four hours.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
project (str): Google Cloud project ID that the AlloyDB instance
|
|
193
|
+
resides in.
|
|
194
|
+
region (str): Google Cloud region of the AlloyDB instance.
|
|
195
|
+
cluster (str): The name of the AlloyDB cluster.
|
|
196
|
+
pub_key (str): PEM-encoded client public key.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
tuple[str, list[str]]: tuple containing the CA certificate
|
|
200
|
+
and certificate chain for the AlloyDB instance.
|
|
201
|
+
"""
|
|
202
|
+
parent = f"projects/{project}/locations/{region}/clusters/{cluster}"
|
|
203
|
+
dur = duration_pb2.Duration()
|
|
204
|
+
dur.seconds = 3600
|
|
205
|
+
req = v1beta.GenerateClientCertificateRequest(
|
|
206
|
+
parent=parent,
|
|
207
|
+
cert_duration=dur,
|
|
208
|
+
public_key=pub_key,
|
|
209
|
+
use_metadata_exchange=self._use_metadata,
|
|
210
|
+
)
|
|
211
|
+
if self._is_sync:
|
|
212
|
+
resp = self._client.generate_client_certificate(request=req)
|
|
213
|
+
else:
|
|
214
|
+
resp = await self._client.generate_client_certificate(request=req)
|
|
215
|
+
return (resp.ca_cert, resp.pem_certificate_chain)
|
|
216
|
+
|
|
217
|
+
async def get_connection_info(
|
|
218
|
+
self,
|
|
219
|
+
project: str,
|
|
220
|
+
region: str,
|
|
221
|
+
cluster: str,
|
|
222
|
+
name: str,
|
|
223
|
+
keys: asyncio.Future,
|
|
224
|
+
) -> ConnectionInfo:
|
|
225
|
+
"""Immediately performs a full refresh operation using the AlloyDB API.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
project (str): The name of the project the AlloyDB instance is
|
|
229
|
+
located in.
|
|
230
|
+
region (str): The region the AlloyDB instance is located in.
|
|
231
|
+
cluster (str): The cluster the AlloyDB instance is located in.
|
|
232
|
+
name (str): Name of the AlloyDB instance.
|
|
233
|
+
keys (asyncio.Future): A future to the client's public-private key
|
|
234
|
+
pair.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
ConnectionInfo: All the information required to connect securely to
|
|
238
|
+
the AlloyDB instance.
|
|
239
|
+
"""
|
|
240
|
+
priv_key, pub_key = await keys
|
|
241
|
+
|
|
242
|
+
# before making AlloyDB API calls, refresh creds if required
|
|
243
|
+
if not self._credentials.token_state == TokenState.FRESH:
|
|
244
|
+
self._credentials.refresh(requests.Request())
|
|
245
|
+
|
|
246
|
+
# fetch metadata
|
|
247
|
+
metadata_task = asyncio.create_task(
|
|
248
|
+
self._get_metadata(
|
|
249
|
+
project,
|
|
250
|
+
region,
|
|
251
|
+
cluster,
|
|
252
|
+
name,
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
# generate client and CA certs
|
|
256
|
+
certs_task = asyncio.create_task(
|
|
257
|
+
self._get_client_certificate(
|
|
258
|
+
project,
|
|
259
|
+
region,
|
|
260
|
+
cluster,
|
|
261
|
+
pub_key,
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
ip_addrs, certs = await asyncio.gather(metadata_task, certs_task)
|
|
266
|
+
|
|
267
|
+
# unpack certs
|
|
268
|
+
ca_cert, cert_chain = certs
|
|
269
|
+
# get expiration from client certificate
|
|
270
|
+
cert_obj = x509.load_pem_x509_certificate(cert_chain[0].encode("UTF-8"))
|
|
271
|
+
expiration = cert_obj.not_valid_after_utc
|
|
272
|
+
|
|
273
|
+
return ConnectionInfo(
|
|
274
|
+
cert_chain,
|
|
275
|
+
ca_cert,
|
|
276
|
+
priv_key,
|
|
277
|
+
ip_addrs,
|
|
278
|
+
expiration,
|
|
279
|
+
)
|