airbyte-internal-ops 0.1.9__py3-none-any.whl → 0.1.11__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.
@@ -11,7 +11,9 @@ from __future__ import annotations
11
11
 
12
12
  import json
13
13
  import os
14
- import traceback
14
+ import shutil
15
+ import socket
16
+ import subprocess
15
17
  from typing import Any, Callable
16
18
 
17
19
  import sqlalchemy
@@ -21,9 +23,127 @@ from google.cloud.sql.connector.enums import IPTypes
21
23
 
22
24
  from airbyte_ops_mcp.constants import (
23
25
  CONNECTION_RETRIEVER_PG_CONNECTION_DETAILS_SECRET_ID,
26
+ DEFAULT_CLOUD_SQL_PROXY_PORT,
24
27
  )
25
28
 
26
29
  PG_DRIVER = "pg8000"
30
+ PROXY_CHECK_TIMEOUT = 0.5 # seconds
31
+ DIRECT_CONNECTION_TIMEOUT = 5 # seconds - timeout for direct VPC/Tailscale connections
32
+
33
+
34
+ class CloudSqlProxyNotRunningError(Exception):
35
+ """Raised when proxy mode is enabled but the Cloud SQL Proxy is not running."""
36
+
37
+ pass
38
+
39
+
40
+ class VpnNotConnectedError(Exception):
41
+ """Raised when direct connection mode requires VPN but it's not connected."""
42
+
43
+ pass
44
+
45
+
46
+ def _is_tailscale_connected() -> bool:
47
+ """Check if Tailscale VPN is likely connected.
48
+
49
+ This is a best-effort check that works on Linux and macOS.
50
+ Returns True if Tailscale appears to be connected, False otherwise.
51
+
52
+ Detection methods:
53
+ 1. Check for tailscale0 network interface (Linux)
54
+ 2. Run 'tailscale status --json' and check backend state (cross-platform)
55
+ """
56
+ # Method 1: Check for tailscale0 interface (Linux)
57
+ try:
58
+ interfaces = [name for _, name in socket.if_nameindex()]
59
+ if "tailscale0" in interfaces:
60
+ return True
61
+ except (OSError, AttributeError):
62
+ pass # if_nameindex not available on this platform
63
+
64
+ # Method 2: Check tailscale CLI status
65
+ tailscale_path = shutil.which("tailscale")
66
+ if tailscale_path:
67
+ try:
68
+ result = subprocess.run(
69
+ [tailscale_path, "status", "--json"],
70
+ capture_output=True,
71
+ text=True,
72
+ timeout=2,
73
+ )
74
+ if result.returncode == 0:
75
+ import json as json_module
76
+
77
+ status = json_module.loads(result.stdout)
78
+ # BackendState "Running" indicates connected
79
+ return status.get("BackendState") == "Running"
80
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError, ValueError):
81
+ pass
82
+
83
+ return False
84
+
85
+
86
+ def _check_vpn_or_proxy_available() -> None:
87
+ """Check if either VPN or proxy is available for database access.
88
+
89
+ This function checks if the environment is properly configured for
90
+ database access. It fails fast with a helpful error message if neither
91
+ Tailscale VPN nor the Cloud SQL Proxy appears to be available.
92
+
93
+ Raises:
94
+ VpnNotConnectedError: If no VPN or proxy is detected
95
+ """
96
+ # If proxy mode is explicitly enabled, don't check VPN
97
+ if os.getenv("CI") or os.getenv("USE_CLOUD_SQL_PROXY"):
98
+ return
99
+
100
+ # Check if Tailscale is connected
101
+ if _is_tailscale_connected():
102
+ return
103
+
104
+ # Neither proxy mode nor Tailscale detected
105
+ raise VpnNotConnectedError(
106
+ "No VPN or proxy detected for database access.\n\n"
107
+ "To connect to the Airbyte Cloud Prod DB Replica, you need either:\n\n"
108
+ "1. Tailscale VPN connected (for direct VPC access)\n"
109
+ " - Install Tailscale: https://tailscale.com/download\n"
110
+ " - Connect to the Airbyte network\n\n"
111
+ "2. Cloud SQL Proxy running locally\n"
112
+ " - Start the proxy:\n"
113
+ " airbyte-ops cloud db start-proxy\n"
114
+ " uvx --from=airbyte-internal-ops airbyte-ops cloud db start-proxy\n"
115
+ " - Set env vars: export USE_CLOUD_SQL_PROXY=1 DB_PORT=15432\n"
116
+ )
117
+
118
+
119
+ def _check_proxy_is_running(host: str, port: int) -> None:
120
+ """Check if the Cloud SQL Proxy is running and accepting connections.
121
+
122
+ This performs a quick socket connection check to fail fast if the proxy
123
+ is not running, rather than waiting for a long connection timeout.
124
+
125
+ Args:
126
+ host: The host to connect to (typically 127.0.0.1)
127
+ port: The port to connect to
128
+
129
+ Raises:
130
+ CloudSqlProxyNotRunningError: If the proxy is not accepting connections
131
+ """
132
+ try:
133
+ with socket.create_connection((host, port), timeout=PROXY_CHECK_TIMEOUT):
134
+ pass # Connection successful, proxy is running
135
+ except (OSError, TimeoutError, ConnectionRefusedError) as e:
136
+ raise CloudSqlProxyNotRunningError(
137
+ f"Cloud SQL Proxy is not running on {host}:{port}. "
138
+ f"Proxy mode is enabled (CI or USE_CLOUD_SQL_PROXY env var is set), "
139
+ f"but nothing is listening on the expected port.\n\n"
140
+ f"To start the proxy, run:\n"
141
+ f" airbyte-ops cloud db start-proxy --port {port}\n"
142
+ f" uvx --from=airbyte-internal-ops airbyte-ops cloud db start-proxy --port {port}\n\n"
143
+ f"Or unset USE_CLOUD_SQL_PROXY to use direct VPC connection.\n\n"
144
+ f"Original error: {e}"
145
+ ) from e
146
+
27
147
 
28
148
  # Lazy-initialized to avoid import-time GCP auth
29
149
  _connector: Connector | None = None
@@ -81,16 +201,20 @@ def get_pool(
81
201
  """Get a SQLAlchemy connection pool for the Airbyte Cloud database.
82
202
 
83
203
  This function supports two connection modes:
84
- 1. Direct connection via Cloud SQL Python Connector (default, requires VPC access)
204
+ 1. Direct connection via Cloud SQL Python Connector (default, requires VPC/Tailscale)
85
205
  2. Connection via Cloud SQL Auth Proxy (when CI or USE_CLOUD_SQL_PROXY env var is set)
86
206
 
87
207
  For proxy mode, start the proxy with:
88
- cloud-sql-proxy prod-ab-cloud-proj:us-west3:prod-pgsql-replica --port=<port>
208
+ airbyte-ops cloud db start-proxy
89
209
 
90
210
  Environment variables:
91
211
  CI: If set, uses proxy connection mode
92
212
  USE_CLOUD_SQL_PROXY: If set, uses proxy connection mode
93
- DB_PORT: Port for proxy connection (default: 5432)
213
+ DB_PORT: Port for proxy connection (default: 15432)
214
+
215
+ Raises:
216
+ VpnNotConnectedError: If direct mode is used but no VPN/proxy is detected
217
+ CloudSqlProxyNotRunningError: If proxy mode is enabled but the proxy is not running
94
218
 
95
219
  Args:
96
220
  gsm_client: GCP Secret Manager client for retrieving credentials
@@ -98,6 +222,9 @@ def get_pool(
98
222
  Returns:
99
223
  SQLAlchemy Engine connected to the Prod DB Replica
100
224
  """
225
+ # Fail fast if no VPN or proxy is available
226
+ _check_vpn_or_proxy_available()
227
+
101
228
  pg_connection_details = json.loads(
102
229
  _get_secret_value(
103
230
  gsm_client, CONNECTION_RETRIEVER_PG_CONNECTION_DETAILS_SECRET_ID
@@ -106,21 +233,21 @@ def get_pool(
106
233
 
107
234
  if os.getenv("CI") or os.getenv("USE_CLOUD_SQL_PROXY"):
108
235
  # Connect via Cloud SQL Auth Proxy, running on localhost
109
- # Port can be configured via DB_PORT env var (default: 5432)
236
+ # Port can be configured via DB_PORT env var (default: DEFAULT_CLOUD_SQL_PROXY_PORT)
110
237
  host = "127.0.0.1"
111
- port = os.getenv("DB_PORT", "5432")
112
- try:
113
- return sqlalchemy.create_engine(
114
- f"postgresql+{PG_DRIVER}://{pg_connection_details['pg_user']}:{pg_connection_details['pg_password']}@{host}:{port}/{pg_connection_details['database_name']}",
115
- )
116
- except Exception as e:
117
- raise AssertionError(
118
- f"sqlalchemy.create_engine exception; could not connect to the proxy at {host}:{port}. "
119
- f"Error: {traceback.format_exception(e)}"
120
- ) from e
238
+ port = int(os.getenv("DB_PORT", str(DEFAULT_CLOUD_SQL_PROXY_PORT)))
239
+
240
+ # Fail fast if proxy is not running
241
+ _check_proxy_is_running(host, port)
242
+
243
+ return sqlalchemy.create_engine(
244
+ f"postgresql+{PG_DRIVER}://{pg_connection_details['pg_user']}:{pg_connection_details['pg_password']}@{host}:{port}/{pg_connection_details['database_name']}",
245
+ )
121
246
 
122
- # Default: Connect via Cloud SQL Python Connector (requires VPC access)
247
+ # Default: Connect via Cloud SQL Python Connector (requires VPC/Tailscale access)
248
+ # Use a timeout to fail faster if the connection can't be established
123
249
  return sqlalchemy.create_engine(
124
250
  f"postgresql+{PG_DRIVER}://",
125
251
  creator=get_database_creator(pg_connection_details),
252
+ connect_args={"timeout": DIRECT_CONNECTION_TIMEOUT},
126
253
  )