Skip to content

Commit 608250a

Browse files
chore: Merge branch 'main' into mdx
2 parents f87cb73 + 5e73fc5 commit 608250a

16 files changed

+274
-101
lines changed

.ci/cloudbuild.yaml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Copyright 2025 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+
steps:
16+
- id: run integration tests
17+
name: python:${_VERSION}
18+
entrypoint: bash
19+
env:
20+
- "IP_TYPE=${_IP_TYPE}"
21+
secretEnv: ["MYSQL_CONNECTION_NAME", "MYSQL_USER", "MYSQL_IAM_USER", "MYSQL_PASS", "MYSQL_DB", "POSTGRES_CONNECTION_NAME", "POSTGRES_USER", "POSTGRES_IAM_USER", "POSTGRES_PASS", "POSTGRES_DB", "POSTGRES_CAS_CONNECTION_NAME", "POSTGRES_CAS_PASS", "POSTGRES_CUSTOMER_CAS_CONNECTION_NAME", "POSTGRES_CUSTOMER_CAS_PASS", "POSTGRES_CUSTOMER_CAS_PASS_VALID_DOMAIN_NAME","SQLSERVER_CONNECTION_NAME", "SQLSERVER_USER", "SQLSERVER_PASS", "SQLSERVER_DB"]
22+
args:
23+
- "-c"
24+
- |
25+
pip install nox
26+
nox -s system-${_VERSION}
27+
availableSecrets:
28+
secretManager:
29+
- versionName: 'projects/$PROJECT_ID/secrets/MYSQL_CONNECTION_NAME/versions/latest'
30+
env: 'MYSQL_CONNECTION_NAME'
31+
- versionName: 'projects/$PROJECT_ID/secrets/MYSQL_USER/versions/latest'
32+
env: 'MYSQL_USER'
33+
- versionName: 'projects/$PROJECT_ID/secrets/CLOUD_BUILD_MYSQL_IAM_USER/versions/latest'
34+
env: 'MYSQL_IAM_USER'
35+
- versionName: 'projects/$PROJECT_ID/secrets/MYSQL_PASS/versions/latest'
36+
env: 'MYSQL_PASS'
37+
- versionName: 'projects/$PROJECT_ID/secrets/MYSQL_DB/versions/latest'
38+
env: 'MYSQL_DB'
39+
- versionName: 'projects/$PROJECT_ID/secrets/POSTGRES_CONNECTION_NAME/versions/latest'
40+
env: 'POSTGRES_CONNECTION_NAME'
41+
- versionName: 'projects/$PROJECT_ID/secrets/POSTGRES_USER/versions/latest'
42+
env: 'POSTGRES_USER'
43+
- versionName: 'projects/$PROJECT_ID/secrets/CLOUD_BUILD_POSTGRES_IAM_USER/versions/latest'
44+
env: 'POSTGRES_IAM_USER'
45+
- versionName: 'projects/$PROJECT_ID/secrets/POSTGRES_PASS/versions/latest'
46+
env: 'POSTGRES_PASS'
47+
- versionName: 'projects/$PROJECT_ID/secrets/POSTGRES_DB/versions/latest'
48+
env: 'POSTGRES_DB'
49+
- versionName: 'projects/$PROJECT_ID/secrets/POSTGRES_CAS_CONNECTION_NAME/versions/latest'
50+
env: 'POSTGRES_CAS_CONNECTION_NAME'
51+
- versionName: 'projects/$PROJECT_ID/secrets/POSTGRES_CAS_PASS/versions/latest'
52+
env: 'POSTGRES_CAS_PASS'
53+
- versionName: 'projects/$PROJECT_ID/secrets/POSTGRES_CUSTOMER_CAS_CONNECTION_NAME/versions/latest'
54+
env: 'POSTGRES_CUSTOMER_CAS_CONNECTION_NAME'
55+
- versionName: 'projects/$PROJECT_ID/secrets/POSTGRES_CUSTOMER_CAS_PASS/versions/latest'
56+
env: 'POSTGRES_CUSTOMER_CAS_PASS'
57+
- versionName: 'projects/$PROJECT_ID/secrets/POSTGRES_CUSTOMER_CAS_PASS_VALID_DOMAIN_NAME/versions/latest'
58+
env: 'POSTGRES_CUSTOMER_CAS_PASS_VALID_DOMAIN_NAME'
59+
- versionName: 'projects/$PROJECT_ID/secrets/SQLSERVER_CONNECTION_NAME/versions/latest'
60+
env: 'SQLSERVER_CONNECTION_NAME'
61+
- versionName: 'projects/$PROJECT_ID/secrets/SQLSERVER_USER/versions/latest'
62+
env: 'SQLSERVER_USER'
63+
- versionName: 'projects/$PROJECT_ID/secrets/SQLSERVER_PASS/versions/latest'
64+
env: 'SQLSERVER_PASS'
65+
- versionName: 'projects/$PROJECT_ID/secrets/SQLSERVER_DB/versions/latest'
66+
env: 'SQLSERVER_DB'
67+
substitutions:
68+
_VERSION: ${_VERSION}
69+
_IP_TYPE: ${_IP_TYPE}
70+
71+
options:
72+
dynamicSubstitutions: true
73+
pool:
74+
name: ${_POOL_NAME}
75+
logging: CLOUD_LOGGING_ONLY

tests/conftest.py

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,23 @@
1717
import asyncio
1818
import os
1919
import socket
20+
import ssl
2021
from threading import Thread
21-
from typing import Any, AsyncGenerator, Generator
22+
from typing import Any, AsyncGenerator
2223

24+
from aiofiles.tempfile import TemporaryDirectory
2325
from aiohttp import web
26+
from cryptography.hazmat.primitives import serialization
2427
import pytest # noqa F401 Needed to run the tests
28+
from unit.mocks import create_ssl_context # type: ignore
2529
from unit.mocks import FakeCredentials # type: ignore
2630
from unit.mocks import FakeCSQLInstance # type: ignore
2731

2832
from google.cloud.sql.connector.client import CloudSQLClient
2933
from google.cloud.sql.connector.connection_name import ConnectionName
3034
from google.cloud.sql.connector.instance import RefreshAheadCache
3135
from google.cloud.sql.connector.utils import generate_keys
36+
from google.cloud.sql.connector.utils import write_to_file
3237

3338
SCOPES = ["https://www.googleapis.com/auth/sqlservice.admin"]
3439

@@ -79,25 +84,60 @@ def fake_credentials() -> FakeCredentials:
7984
return FakeCredentials()
8085

8186

82-
def mock_server(server_sock: socket.socket) -> None:
83-
"""Create mock server listening on specified ip_address and port."""
87+
async def start_proxy_server(instance: FakeCSQLInstance) -> None:
88+
"""Run local proxy server capable of performing mTLS"""
8489
ip_address = "127.0.0.1"
8590
port = 3307
86-
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
87-
server_sock.bind((ip_address, port))
88-
server_sock.listen(0)
89-
server_sock.accept()
91+
# create socket
92+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
93+
# create SSL/TLS context
94+
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
95+
context.minimum_version = ssl.TLSVersion.TLSv1_3
96+
# tmpdir and its contents are automatically deleted after the CA cert
97+
# and cert chain are loaded into the SSLcontext. The values
98+
# need to be written to files in order to be loaded by the SSLContext
99+
server_key_bytes = instance.server_key.private_bytes(
100+
encoding=serialization.Encoding.PEM,
101+
format=serialization.PrivateFormat.TraditionalOpenSSL,
102+
encryption_algorithm=serialization.NoEncryption(),
103+
)
104+
async with TemporaryDirectory() as tmpdir:
105+
server_filename, _, key_filename = await write_to_file(
106+
tmpdir, instance.server_cert_pem, "", server_key_bytes
107+
)
108+
context.load_cert_chain(server_filename, key_filename)
109+
# allow socket to be re-used
110+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
111+
# bind socket to Cloud SQL proxy server port on localhost
112+
sock.bind((ip_address, port))
113+
# listen for incoming connections
114+
sock.listen(5)
115+
116+
with context.wrap_socket(sock, server_side=True) as ssock:
117+
while True:
118+
conn, _ = ssock.accept()
119+
conn.close()
120+
121+
122+
@pytest.fixture(scope="session")
123+
def proxy_server(fake_instance: FakeCSQLInstance) -> None:
124+
"""Run local proxy server capable of performing mTLS"""
125+
thread = Thread(
126+
target=asyncio.run,
127+
args=(
128+
start_proxy_server(
129+
fake_instance,
130+
),
131+
),
132+
daemon=True,
133+
)
134+
thread.start()
135+
thread.join(1.0) # add a delay to allow the proxy server to start
90136

91137

92138
@pytest.fixture
93-
def server() -> Generator:
94-
"""Create thread with server listening on proper port"""
95-
server_sock = socket.socket()
96-
thread = Thread(target=mock_server, args=(server_sock,), daemon=True)
97-
thread.start()
98-
yield thread
99-
server_sock.close()
100-
thread.join()
139+
async def context(fake_instance: FakeCSQLInstance) -> ssl.SSLContext:
140+
return await create_ssl_context(fake_instance)
101141

102142

103143
@pytest.fixture
@@ -107,7 +147,7 @@ def kwargs() -> Any:
107147
return kwargs
108148

109149

110-
@pytest.fixture
150+
@pytest.fixture(scope="session")
111151
def fake_instance() -> FakeCSQLInstance:
112152
return FakeCSQLInstance()
113153

tests/system/test_asyncpg_connection.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ async def create_sqlalchemy_engine(
3232
user: str,
3333
password: str,
3434
db: str,
35+
ip_type: str = "public",
3536
refresh_strategy: str = "background",
3637
resolver: Union[type[DefaultResolver], type[DnsResolver]] = DefaultResolver,
3738
) -> tuple[sqlalchemy.ext.asyncio.engine.AsyncEngine, Connector]:
@@ -63,6 +64,9 @@ async def create_sqlalchemy_engine(
6364
The database user's password, e.g., secret-password
6465
db (str):
6566
The name of the database, e.g., mydb
67+
ip_type (str):
68+
The IP type of the Cloud SQL instance to connect to. Can be one
69+
of "public", "private", or "psc".
6670
refresh_strategy (Optional[str]):
6771
Refresh strategy for the Cloud SQL Connector. Can be one of "lazy"
6872
or "background". For serverless environments use "lazy" to avoid
@@ -87,7 +91,7 @@ async def create_sqlalchemy_engine(
8791
user=user,
8892
password=password,
8993
db=db,
90-
ip_type="public", # can also be "private" or "psc"
94+
ip_type=ip_type, # can be "public", "private" or "psc"
9195
),
9296
execution_options={"isolation_level": "AUTOCOMMIT"},
9397
)
@@ -99,6 +103,7 @@ async def create_asyncpg_pool(
99103
user: str,
100104
password: str,
101105
db: str,
106+
ip_type: str = "public",
102107
refresh_strategy: str = "background",
103108
) -> tuple[asyncpg.Pool, Connector]:
104109
"""Creates a native asyncpg connection pool for a Cloud SQL instance and
@@ -128,6 +133,9 @@ async def create_asyncpg_pool(
128133
The database user's password, e.g., secret-password
129134
db (str):
130135
The name of the database, e.g., mydb
136+
ip_type (str):
137+
The IP type of the Cloud SQL instance to connect to. Can be one
138+
of "public", "private", or "psc".
131139
refresh_strategy (Optional[str]):
132140
Refresh strategy for the Cloud SQL Connector. Can be one of "lazy"
133141
or "background". For serverless environments use "lazy" to avoid
@@ -145,7 +153,7 @@ async def getconn(
145153
user=user,
146154
password=password,
147155
db=db,
148-
ip_type="public", # can also be "private" or "psc",
156+
ip_type=ip_type, # can be "public", "private" or "psc"
149157
**kwargs,
150158
)
151159
return conn
@@ -161,8 +169,11 @@ async def test_sqlalchemy_connection_with_asyncpg() -> None:
161169
user = os.environ["POSTGRES_USER"]
162170
password = os.environ["POSTGRES_PASS"]
163171
db = os.environ["POSTGRES_DB"]
172+
ip_type = os.environ.get("IP_TYPE", "public")
164173

165-
pool, connector = await create_sqlalchemy_engine(inst_conn_name, user, password, db)
174+
pool, connector = await create_sqlalchemy_engine(
175+
inst_conn_name, user, password, db, ip_type
176+
)
166177

167178
async with pool.connect() as conn:
168179
res = (await conn.execute(sqlalchemy.text("SELECT 1"))).fetchone()
@@ -177,9 +188,10 @@ async def test_lazy_sqlalchemy_connection_with_asyncpg() -> None:
177188
user = os.environ["POSTGRES_USER"]
178189
password = os.environ["POSTGRES_PASS"]
179190
db = os.environ["POSTGRES_DB"]
191+
ip_type = os.environ.get("IP_TYPE", "public")
180192

181193
pool, connector = await create_sqlalchemy_engine(
182-
inst_conn_name, user, password, db, "lazy"
194+
inst_conn_name, user, password, db, ip_type, "lazy"
183195
)
184196

185197
async with pool.connect() as conn:
@@ -195,9 +207,10 @@ async def test_custom_SAN_with_dns_sqlalchemy_connection_with_asyncpg() -> None:
195207
user = os.environ["POSTGRES_USER"]
196208
password = os.environ["POSTGRES_CUSTOMER_CAS_PASS"]
197209
db = os.environ["POSTGRES_DB"]
210+
ip_type = os.environ.get("IP_TYPE", "public")
198211

199212
pool, connector = await create_sqlalchemy_engine(
200-
inst_conn_name, user, password, db, resolver=DnsResolver
213+
inst_conn_name, user, password, db, ip_type, resolver=DnsResolver
201214
)
202215

203216
async with pool.connect() as conn:
@@ -213,8 +226,11 @@ async def test_connection_with_asyncpg() -> None:
213226
user = os.environ["POSTGRES_USER"]
214227
password = os.environ["POSTGRES_PASS"]
215228
db = os.environ["POSTGRES_DB"]
229+
ip_type = os.environ.get("IP_TYPE", "public")
216230

217-
pool, connector = await create_asyncpg_pool(inst_conn_name, user, password, db)
231+
pool, connector = await create_asyncpg_pool(
232+
inst_conn_name, user, password, db, ip_type
233+
)
218234

219235
async with pool.acquire() as conn:
220236
res = await conn.fetch("SELECT 1")
@@ -229,9 +245,10 @@ async def test_lazy_connection_with_asyncpg() -> None:
229245
user = os.environ["POSTGRES_USER"]
230246
password = os.environ["POSTGRES_PASS"]
231247
db = os.environ["POSTGRES_DB"]
248+
ip_type = os.environ.get("IP_TYPE", "public")
232249

233250
pool, connector = await create_asyncpg_pool(
234-
inst_conn_name, user, password, db, "lazy"
251+
inst_conn_name, user, password, db, ip_type, "lazy"
235252
)
236253

237254
async with pool.acquire() as conn:

tests/system/test_asyncpg_iam_auth.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ async def create_sqlalchemy_engine(
2727
instance_connection_name: str,
2828
user: str,
2929
db: str,
30+
ip_type: str = "public",
3031
refresh_strategy: str = "background",
3132
) -> tuple[sqlalchemy.ext.asyncio.engine.AsyncEngine, Connector]:
3233
"""Creates a connection pool for a Cloud SQL instance and returns the pool
@@ -55,6 +56,9 @@ async def create_sqlalchemy_engine(
5556
5657
db (str):
5758
The name of the database, e.g., mydb
59+
ip_type (str):
60+
The IP type of the Cloud SQL instance to connect to. Can be one
61+
of "public", "private", or "psc".
5862
refresh_strategy (Optional[str]):
5963
Refresh strategy for the Cloud SQL Connector. Can be one of "lazy"
6064
or "background". For serverless environments use "lazy" to avoid
@@ -71,7 +75,7 @@ async def create_sqlalchemy_engine(
7175
"asyncpg",
7276
user=user,
7377
db=db,
74-
ip_type="public", # can also be "private" or "psc"
78+
ip_type=ip_type, # can be "public", "private" or "psc"
7579
enable_iam_auth=True,
7680
),
7781
execution_options={"isolation_level": "AUTOCOMMIT"},
@@ -84,8 +88,9 @@ async def test_iam_authn_connection_with_asyncpg() -> None:
8488
inst_conn_name = os.environ["POSTGRES_CONNECTION_NAME"]
8589
user = os.environ["POSTGRES_IAM_USER"]
8690
db = os.environ["POSTGRES_DB"]
91+
ip_type = os.environ.get("IP_TYPE", "public")
8792

88-
pool, connector = await create_sqlalchemy_engine(inst_conn_name, user, db)
93+
pool, connector = await create_sqlalchemy_engine(inst_conn_name, user, db, ip_type)
8994

9095
async with pool.connect() as conn:
9196
res = (await conn.execute(sqlalchemy.text("SELECT 1"))).fetchone()
@@ -99,8 +104,11 @@ async def test_lazy_iam_authn_connection_with_asyncpg() -> None:
99104
inst_conn_name = os.environ["POSTGRES_CONNECTION_NAME"]
100105
user = os.environ["POSTGRES_IAM_USER"]
101106
db = os.environ["POSTGRES_DB"]
107+
ip_type = os.environ.get("IP_TYPE", "public")
102108

103-
pool, connector = await create_sqlalchemy_engine(inst_conn_name, user, db, "lazy")
109+
pool, connector = await create_sqlalchemy_engine(
110+
inst_conn_name, user, db, ip_type, "lazy"
111+
)
104112

105113
async with pool.connect() as conn:
106114
res = (await conn.execute(sqlalchemy.text("SELECT 1"))).fetchone()

tests/system/test_connector_object.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def getconn() -> pymysql.connections.Connection:
3838
user=os.environ["MYSQL_USER"],
3939
password=os.environ["MYSQL_PASS"],
4040
db=os.environ["MYSQL_DB"],
41+
ip_type=os.environ.get("IP_TYPE", "public"),
4142
)
4243
return conn
4344

0 commit comments

Comments
 (0)