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.
- {airbyte_internal_ops-0.1.9.dist-info → airbyte_internal_ops-0.1.11.dist-info}/METADATA +1 -1
- {airbyte_internal_ops-0.1.9.dist-info → airbyte_internal_ops-0.1.11.dist-info}/RECORD +14 -12
- airbyte_ops_mcp/cli/cloud.py +279 -3
- airbyte_ops_mcp/constants.py +9 -0
- airbyte_ops_mcp/github_actions.py +197 -0
- airbyte_ops_mcp/live_tests/cdk_secrets.py +90 -0
- airbyte_ops_mcp/live_tests/ci_output.py +55 -5
- airbyte_ops_mcp/live_tests/connector_runner.py +3 -0
- airbyte_ops_mcp/mcp/github.py +2 -21
- airbyte_ops_mcp/mcp/live_tests.py +44 -84
- airbyte_ops_mcp/mcp/prerelease.py +9 -31
- airbyte_ops_mcp/prod_db_access/db_engine.py +143 -16
- {airbyte_internal_ops-0.1.9.dist-info → airbyte_internal_ops-0.1.11.dist-info}/WHEEL +0 -0
- {airbyte_internal_ops-0.1.9.dist-info → airbyte_internal_ops-0.1.11.dist-info}/entry_points.txt +0 -0
|
@@ -11,7 +11,9 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import json
|
|
13
13
|
import os
|
|
14
|
-
import
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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",
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
)
|
|
File without changes
|
{airbyte_internal_ops-0.1.9.dist-info → airbyte_internal_ops-0.1.11.dist-info}/entry_points.txt
RENAMED
|
File without changes
|