buildai-cli 0.3.67__tar.gz → 0.3.69__tar.gz
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.
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/.gitignore +1 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/CLAUDE.md +6 -1
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/PKG-INFO +1 -1
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/commands/db/__init__.py +2 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/commands/db/common.py +136 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/commands/db/migrate.py +19 -1
- buildai_cli-0.3.69/cli/commands/db/tunnel.py +115 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/commands/dev.py +22 -1
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/db_broker.py +29 -5
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/main.py +6 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/ops_init.py +5 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/pyproject.toml +1 -1
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/AGENTS.md +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/buildai_bootstrap.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/__init__.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/_has_core.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/auth_local.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/commands/__init__.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/commands/api_proxy.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/commands/auth.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/commands/db/broker.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/commands/db/query.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/commands/db/schema.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/commands/db/status.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/commands/doctor.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/commands/gigcamera.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/commands/processing.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/config.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/console.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/context.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/guard.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/internal_api.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/nl_query/__init__.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/nl_query/dataset_tools.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/output.py +0 -0
- {buildai_cli-0.3.67 → buildai_cli-0.3.69}/cli/pagination.py +0 -0
|
@@ -14,6 +14,8 @@ Typer-based CLI with two modes: standalone (PyPI, API-backed) and workspace (rep
|
|
|
14
14
|
```bash
|
|
15
15
|
buildai auth whoami # API auth inspection
|
|
16
16
|
buildai db query "SELECT count(*) FROM core.clips" # DB-direct
|
|
17
|
+
buildai db --env staging tunnel # IAP/SOCKS route for private staging DB
|
|
18
|
+
buildai db --env staging --all-proxy socks5://127.0.0.1:1080 query "SELECT 1" # staging query
|
|
17
19
|
buildai db broker status # Active local DB broker state
|
|
18
20
|
buildai db schema tables # Schema introspection
|
|
19
21
|
buildai db schema describe core.clips # Table details
|
|
@@ -31,7 +33,10 @@ buildai db status # Migration status
|
|
|
31
33
|
- Writes require `--write` flag.
|
|
32
34
|
- Production migrations prompt for confirmation.
|
|
33
35
|
- Worktree app/runtime targeting comes from explicit env vars and the repo Makefile, not a saved CLI context layer.
|
|
34
|
-
- `buildai db --env` selects the explicit DB lane: `production`, `dev`.
|
|
36
|
+
- `buildai db --env` selects the explicit DB lane: `production`, `staging`, `dev`.
|
|
37
|
+
- Staging is private-IP only. Start an IAP/VPN/SOCKS route first, then pass it
|
|
38
|
+
with `--all-proxy` so the local AlloyDB broker can reach the private address.
|
|
39
|
+
The standard route is `buildai db --env staging tunnel`.
|
|
35
40
|
|
|
36
41
|
## Reference
|
|
37
42
|
|
|
@@ -9,6 +9,7 @@ from . import migrate as migrate_mod
|
|
|
9
9
|
from . import query as query_mod
|
|
10
10
|
from . import schema as schema_mod
|
|
11
11
|
from . import status as status_mod
|
|
12
|
+
from . import tunnel as tunnel_mod
|
|
12
13
|
|
|
13
14
|
app = typer.Typer(
|
|
14
15
|
name="db",
|
|
@@ -19,5 +20,6 @@ app = typer.Typer(
|
|
|
19
20
|
app.command("query")(query_mod.query)
|
|
20
21
|
app.command("status")(status_mod.status)
|
|
21
22
|
app.command("migrate")(migrate_mod.migrate)
|
|
23
|
+
app.command("tunnel")(tunnel_mod.tunnel)
|
|
22
24
|
app.add_typer(broker_mod.app, name="broker")
|
|
23
25
|
app.add_typer(schema_mod.app, name="schema")
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import re
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Any
|
|
@@ -20,6 +21,11 @@ MIGRATIONS_SA_DB_USER = "buildai-migrations-sa@data-470400.iam"
|
|
|
20
21
|
REPO_ROOT = Path(__file__).resolve().parents[5]
|
|
21
22
|
MIGRATIONS_DIR = REPO_ROOT / "migrations"
|
|
22
23
|
MIGRATIONS_TABLE = "public._migrations"
|
|
24
|
+
_NO_TRANSACTION_MARKER = "-- requires-no-transaction: true"
|
|
25
|
+
_CONCURRENT_DDL_PATTERN = re.compile(
|
|
26
|
+
r"\b(?:CREATE|DROP|REINDEX|REFRESH)\s+(?:MATERIALIZED\s+VIEW\s+)?(?:INDEX|TABLE|VIEW)?\s*CONCURRENTLY\b",
|
|
27
|
+
flags=re.IGNORECASE,
|
|
28
|
+
)
|
|
23
29
|
|
|
24
30
|
|
|
25
31
|
@dataclass
|
|
@@ -140,3 +146,133 @@ def migration_requires_system_admin(path: Path) -> bool:
|
|
|
140
146
|
"""Detect migrations that must run with the DB admin connection."""
|
|
141
147
|
|
|
142
148
|
return "requires-system-admin" in path.read_text(encoding="utf-8", errors="ignore")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def migration_requires_manual_apply(path: Path) -> bool:
|
|
152
|
+
"""Detect migrations that must be explicitly selected by an operator."""
|
|
153
|
+
|
|
154
|
+
for line in path.read_text(encoding="utf-8", errors="ignore").splitlines()[:10]:
|
|
155
|
+
if line.strip().lower() == "-- requires-manual-apply: true":
|
|
156
|
+
return True
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def migration_requires_no_transaction(path: Path, sql: str) -> bool:
|
|
161
|
+
"""Return whether a migration must be executed statement-by-statement."""
|
|
162
|
+
|
|
163
|
+
header = "\n".join(sql.splitlines()[:10]).lower()
|
|
164
|
+
if _NO_TRANSACTION_MARKER in header:
|
|
165
|
+
return True
|
|
166
|
+
return _CONCURRENT_DDL_PATTERN.search(sql) is not None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def split_sql_statements(sql: str) -> list[str]:
|
|
170
|
+
"""Split SQL into executable statements while respecting quotes and DO blocks."""
|
|
171
|
+
|
|
172
|
+
statements: list[str] = []
|
|
173
|
+
chunk: list[str] = []
|
|
174
|
+
in_single_quote = False
|
|
175
|
+
in_double_quote = False
|
|
176
|
+
in_line_comment = False
|
|
177
|
+
in_block_comment = False
|
|
178
|
+
dollar_tag: str | None = None
|
|
179
|
+
i = 0
|
|
180
|
+
|
|
181
|
+
while i < len(sql):
|
|
182
|
+
char = sql[i]
|
|
183
|
+
next_char = sql[i + 1] if i + 1 < len(sql) else ""
|
|
184
|
+
|
|
185
|
+
if in_line_comment:
|
|
186
|
+
chunk.append(char)
|
|
187
|
+
if char == "\n":
|
|
188
|
+
in_line_comment = False
|
|
189
|
+
i += 1
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
if in_block_comment:
|
|
193
|
+
chunk.append(char)
|
|
194
|
+
if char == "*" and next_char == "/":
|
|
195
|
+
chunk.append(next_char)
|
|
196
|
+
in_block_comment = False
|
|
197
|
+
i += 2
|
|
198
|
+
else:
|
|
199
|
+
i += 1
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
if dollar_tag is not None:
|
|
203
|
+
if sql.startswith(dollar_tag, i):
|
|
204
|
+
chunk.append(dollar_tag)
|
|
205
|
+
i += len(dollar_tag)
|
|
206
|
+
dollar_tag = None
|
|
207
|
+
else:
|
|
208
|
+
chunk.append(char)
|
|
209
|
+
i += 1
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
if in_single_quote:
|
|
213
|
+
chunk.append(char)
|
|
214
|
+
if char == "'" and next_char == "'":
|
|
215
|
+
chunk.append(next_char)
|
|
216
|
+
i += 2
|
|
217
|
+
continue
|
|
218
|
+
if char == "'":
|
|
219
|
+
in_single_quote = False
|
|
220
|
+
i += 1
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
if in_double_quote:
|
|
224
|
+
chunk.append(char)
|
|
225
|
+
if char == '"':
|
|
226
|
+
in_double_quote = False
|
|
227
|
+
i += 1
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
if char == "-" and next_char == "-":
|
|
231
|
+
chunk.append(char)
|
|
232
|
+
chunk.append(next_char)
|
|
233
|
+
in_line_comment = True
|
|
234
|
+
i += 2
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
if char == "/" and next_char == "*":
|
|
238
|
+
chunk.append(char)
|
|
239
|
+
chunk.append(next_char)
|
|
240
|
+
in_block_comment = True
|
|
241
|
+
i += 2
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
if char == "'":
|
|
245
|
+
chunk.append(char)
|
|
246
|
+
in_single_quote = True
|
|
247
|
+
i += 1
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
if char == '"':
|
|
251
|
+
chunk.append(char)
|
|
252
|
+
in_double_quote = True
|
|
253
|
+
i += 1
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
if char == "$":
|
|
257
|
+
match = re.match(r"\$[A-Za-z_][A-Za-z0-9_]*\$|\$\$", sql[i:])
|
|
258
|
+
if match:
|
|
259
|
+
dollar_tag = match.group(0)
|
|
260
|
+
chunk.append(dollar_tag)
|
|
261
|
+
i += len(dollar_tag)
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
if char == ";":
|
|
265
|
+
statement = "".join(chunk).strip()
|
|
266
|
+
if statement:
|
|
267
|
+
statements.append(statement)
|
|
268
|
+
chunk = []
|
|
269
|
+
i += 1
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
chunk.append(char)
|
|
273
|
+
i += 1
|
|
274
|
+
|
|
275
|
+
trailing = "".join(chunk).strip()
|
|
276
|
+
if trailing:
|
|
277
|
+
statements.append(trailing)
|
|
278
|
+
return statements
|
|
@@ -20,8 +20,11 @@ from .common import (
|
|
|
20
20
|
get_migration_status,
|
|
21
21
|
get_migrations_connection,
|
|
22
22
|
migration_label,
|
|
23
|
+
migration_requires_manual_apply,
|
|
24
|
+
migration_requires_no_transaction,
|
|
23
25
|
migration_requires_system_admin,
|
|
24
26
|
set_owner_role,
|
|
27
|
+
split_sql_statements,
|
|
25
28
|
)
|
|
26
29
|
|
|
27
30
|
_MIGRATION_WRITE_PROFILES = frozenset(
|
|
@@ -125,6 +128,17 @@ def migrate(
|
|
|
125
128
|
if target == "all"
|
|
126
129
|
else [f for f in migration_files if target and target in f.name]
|
|
127
130
|
)
|
|
131
|
+
if target == "all":
|
|
132
|
+
manual_apply = [f for f in to_run if migration_requires_manual_apply(f)]
|
|
133
|
+
if manual_apply:
|
|
134
|
+
warning(
|
|
135
|
+
"Skipping manual-apply migration(s): "
|
|
136
|
+
+ ", ".join(item.name for item in manual_apply)
|
|
137
|
+
)
|
|
138
|
+
to_run = [f for f in to_run if f not in manual_apply]
|
|
139
|
+
elif any(migration_requires_manual_apply(f) for f in to_run):
|
|
140
|
+
warning("Selected manual-apply migration explicitly; proceeding.")
|
|
141
|
+
|
|
128
142
|
if not to_run:
|
|
129
143
|
success("No migrations to run")
|
|
130
144
|
return
|
|
@@ -157,7 +171,11 @@ def migrate(
|
|
|
157
171
|
await conn.execute("SET statement_timeout = '0'")
|
|
158
172
|
else:
|
|
159
173
|
await conn.execute(f"SET statement_timeout = '{timeout}s'")
|
|
160
|
-
|
|
174
|
+
if migration_requires_no_transaction(migration, sql):
|
|
175
|
+
for statement in split_sql_statements(sql):
|
|
176
|
+
await conn.execute(statement, timeout=None)
|
|
177
|
+
else:
|
|
178
|
+
await conn.execute(sql, timeout=None)
|
|
161
179
|
await conn.execute(
|
|
162
180
|
f"INSERT INTO {MIGRATIONS_TABLE} (filename) VALUES ($1) ON CONFLICT DO NOTHING",
|
|
163
181
|
migration.name,
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""IAP tunnel helpers for private-only DB lanes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from infra.settings import Settings
|
|
10
|
+
from typing_extensions import Annotated
|
|
11
|
+
|
|
12
|
+
from cli.console import error, info, warning
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _build_iap_socks_command(
|
|
16
|
+
*,
|
|
17
|
+
host: str,
|
|
18
|
+
project: str,
|
|
19
|
+
zone: str,
|
|
20
|
+
local_host: str,
|
|
21
|
+
local_port: int,
|
|
22
|
+
) -> list[str]:
|
|
23
|
+
"""Build the gcloud command that opens one local SOCKS proxy over IAP SSH."""
|
|
24
|
+
|
|
25
|
+
return [
|
|
26
|
+
"gcloud",
|
|
27
|
+
"compute",
|
|
28
|
+
"ssh",
|
|
29
|
+
host,
|
|
30
|
+
f"--project={project}",
|
|
31
|
+
f"--zone={zone}",
|
|
32
|
+
"--tunnel-through-iap",
|
|
33
|
+
"--",
|
|
34
|
+
"-N",
|
|
35
|
+
"-D",
|
|
36
|
+
f"{local_host}:{local_port}",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def tunnel(
|
|
41
|
+
ctx: typer.Context,
|
|
42
|
+
local_port: Annotated[
|
|
43
|
+
int,
|
|
44
|
+
typer.Option(
|
|
45
|
+
"--local-port",
|
|
46
|
+
help="Local SOCKS port for the DB auth proxy to use.",
|
|
47
|
+
),
|
|
48
|
+
] = 1080,
|
|
49
|
+
local_host: Annotated[
|
|
50
|
+
str,
|
|
51
|
+
typer.Option(
|
|
52
|
+
"--local-host",
|
|
53
|
+
help="Local bind address for the SOCKS listener.",
|
|
54
|
+
),
|
|
55
|
+
] = "127.0.0.1",
|
|
56
|
+
host: Annotated[
|
|
57
|
+
str,
|
|
58
|
+
typer.Option(
|
|
59
|
+
"--host",
|
|
60
|
+
help="IAP-reachable VM that can route to the private AlloyDB address.",
|
|
61
|
+
),
|
|
62
|
+
] = "atlas-control-plane",
|
|
63
|
+
project: Annotated[
|
|
64
|
+
str | None,
|
|
65
|
+
typer.Option(
|
|
66
|
+
"--project",
|
|
67
|
+
help="GCP project for the tunnel host. Defaults to the active DB settings project.",
|
|
68
|
+
),
|
|
69
|
+
] = None,
|
|
70
|
+
zone: Annotated[
|
|
71
|
+
str,
|
|
72
|
+
typer.Option(
|
|
73
|
+
"--zone",
|
|
74
|
+
help="Compute zone for the tunnel host.",
|
|
75
|
+
),
|
|
76
|
+
] = "asia-south1-a",
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Open an IAP-backed SOCKS tunnel for private-only DB access.
|
|
79
|
+
|
|
80
|
+
Keep this command running in one terminal, then pass
|
|
81
|
+
``--all-proxy socks5://127.0.0.1:1080`` to ``buildai db`` commands in
|
|
82
|
+
another terminal.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
settings: Settings = ctx.obj["settings"]
|
|
86
|
+
resolved_project = project or settings.gcp_project_id
|
|
87
|
+
|
|
88
|
+
if shutil.which("gcloud") is None:
|
|
89
|
+
error("gcloud is not installed or not on PATH.")
|
|
90
|
+
raise typer.Exit(1)
|
|
91
|
+
|
|
92
|
+
if not settings.use_private_ip:
|
|
93
|
+
warning(
|
|
94
|
+
"The selected DB lane is not configured for private-IP access. "
|
|
95
|
+
"This tunnel is normally needed only for staging/private lanes."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
proxy_url = f"socks5://{local_host}:{local_port}"
|
|
99
|
+
info(f"Opening IAP SOCKS tunnel on {proxy_url}. Stop with Ctrl-C.")
|
|
100
|
+
info(
|
|
101
|
+
"In another terminal, run: "
|
|
102
|
+
f'uv run buildai db --env {settings.app_env.value} --all-proxy {proxy_url} query "SELECT 1"'
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
command = _build_iap_socks_command(
|
|
106
|
+
host=host,
|
|
107
|
+
project=resolved_project,
|
|
108
|
+
zone=zone,
|
|
109
|
+
local_host=local_host,
|
|
110
|
+
local_port=local_port,
|
|
111
|
+
)
|
|
112
|
+
try:
|
|
113
|
+
raise typer.Exit(subprocess.run(command, check=False).returncode)
|
|
114
|
+
except KeyboardInterrupt:
|
|
115
|
+
raise typer.Exit(130) from None
|
|
@@ -24,6 +24,11 @@ _DEFAULT_DEV_INSTANCE_URI = (
|
|
|
24
24
|
"instances/buildai-india-dev-primary"
|
|
25
25
|
)
|
|
26
26
|
_DEFAULT_DEV_DATABASE = "buildai_dev"
|
|
27
|
+
_DEFAULT_STAGING_INSTANCE_URI = (
|
|
28
|
+
"projects/data-470400/locations/asia-south1/clusters/buildai-india-staging/"
|
|
29
|
+
"instances/buildai-india-staging-primary"
|
|
30
|
+
)
|
|
31
|
+
_DEFAULT_STAGING_DATABASE = "buildai_staging"
|
|
27
32
|
|
|
28
33
|
|
|
29
34
|
def _build_proxy_command(
|
|
@@ -54,7 +59,9 @@ def _db_info_payload(
|
|
|
54
59
|
try:
|
|
55
60
|
resolved_env = Environment(env)
|
|
56
61
|
except ValueError as exc:
|
|
57
|
-
raise typer.BadParameter(
|
|
62
|
+
raise typer.BadParameter(
|
|
63
|
+
"Use one of: development, staging, preview, production, test."
|
|
64
|
+
) from exc
|
|
58
65
|
|
|
59
66
|
profile = resolve_sanctioned_profile(profile_name, service=service, environment=env)
|
|
60
67
|
settings = Settings(app_env=resolved_env, execution_context=ExecutionContext.HUMAN)
|
|
@@ -69,6 +76,8 @@ def _db_info_payload(
|
|
|
69
76
|
except RuntimeError:
|
|
70
77
|
if resolved_env == Environment.DEVELOPMENT:
|
|
71
78
|
instance_uri = _DEFAULT_DEV_INSTANCE_URI
|
|
79
|
+
elif resolved_env == Environment.STAGING:
|
|
80
|
+
instance_uri = _DEFAULT_STAGING_INSTANCE_URI
|
|
72
81
|
else:
|
|
73
82
|
raise
|
|
74
83
|
|
|
@@ -77,6 +86,8 @@ def _db_info_payload(
|
|
|
77
86
|
except RuntimeError:
|
|
78
87
|
if resolved_env == Environment.DEVELOPMENT:
|
|
79
88
|
database_name = _DEFAULT_DEV_DATABASE
|
|
89
|
+
elif resolved_env == Environment.STAGING:
|
|
90
|
+
database_name = _DEFAULT_STAGING_DATABASE
|
|
80
91
|
else:
|
|
81
92
|
raise
|
|
82
93
|
|
|
@@ -87,6 +98,12 @@ def _db_info_payload(
|
|
|
87
98
|
"If you are outside its VPC, run the Auth Proxy through an approved "
|
|
88
99
|
"intermediary path such as ALL_PROXY / PSC / VPN."
|
|
89
100
|
)
|
|
101
|
+
elif resolved_env == Environment.STAGING:
|
|
102
|
+
private_only_note = (
|
|
103
|
+
"The staging AlloyDB cluster is private-only. Run "
|
|
104
|
+
"`buildai db --env staging tunnel`, then pass "
|
|
105
|
+
"`--all-proxy socks5://127.0.0.1:1080` to staging DB commands."
|
|
106
|
+
)
|
|
90
107
|
|
|
91
108
|
proxy_command = _build_proxy_command(
|
|
92
109
|
instance_uri=instance_uri,
|
|
@@ -102,6 +119,10 @@ def _db_info_payload(
|
|
|
102
119
|
"ALLOYDB_AUTH_PROXY_HOST": proxy_host,
|
|
103
120
|
f"ALLOYDB_AUTH_PROXY_PORT_{suffix}": str(proxy_port),
|
|
104
121
|
}
|
|
122
|
+
if resolved_env == Environment.STAGING:
|
|
123
|
+
proxy_env["USE_PRIVATE_IP"] = "true"
|
|
124
|
+
if all_proxy:
|
|
125
|
+
proxy_env["ALLOYDB_AUTH_PROXY_ALL_PROXY"] = all_proxy
|
|
105
126
|
|
|
106
127
|
return {
|
|
107
128
|
"profile": profile.name,
|
|
@@ -56,6 +56,7 @@ class BrokerConfig:
|
|
|
56
56
|
host: str
|
|
57
57
|
port: int
|
|
58
58
|
use_private_ip: bool
|
|
59
|
+
all_proxy: str | None = None
|
|
59
60
|
password: str = ""
|
|
60
61
|
|
|
61
62
|
@property
|
|
@@ -73,6 +74,7 @@ class BrokerConfig:
|
|
|
73
74
|
"host": self.host,
|
|
74
75
|
"port": self.port,
|
|
75
76
|
"use_private_ip": self.use_private_ip,
|
|
77
|
+
"all_proxy": self.all_proxy,
|
|
76
78
|
}
|
|
77
79
|
raw = json.dumps(payload, sort_keys=True, separators=(",", ":"))
|
|
78
80
|
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]
|
|
@@ -502,6 +504,10 @@ def _is_matching_proxy_process(process: ListenerProcess, config: BrokerConfig) -
|
|
|
502
504
|
"""Return True when a listener command is the broker this profile needs."""
|
|
503
505
|
|
|
504
506
|
command = process.command
|
|
507
|
+
if config.all_proxy:
|
|
508
|
+
# The proxy transport override is in the child environment, not argv.
|
|
509
|
+
# Only reuse env-proxied brokers through the machine-global state file.
|
|
510
|
+
return False
|
|
505
511
|
if not command:
|
|
506
512
|
return False
|
|
507
513
|
executable_name = Path(command[0].strip('"')).name.lower()
|
|
@@ -569,6 +575,15 @@ def _proxy_popen_kwargs() -> dict[str, Any]:
|
|
|
569
575
|
return {"creationflags": creationflags}
|
|
570
576
|
|
|
571
577
|
|
|
578
|
+
def _proxy_environment(config: BrokerConfig) -> dict[str, str]:
|
|
579
|
+
"""Return the auth-proxy child environment for this broker config."""
|
|
580
|
+
|
|
581
|
+
env = os.environ.copy()
|
|
582
|
+
if config.all_proxy:
|
|
583
|
+
env["ALL_PROXY"] = config.all_proxy
|
|
584
|
+
return env
|
|
585
|
+
|
|
586
|
+
|
|
572
587
|
def _recent_broker_log(config: BrokerConfig, *, max_bytes: int = 12000) -> str:
|
|
573
588
|
"""Return the recent proxy log text that explains connection resets."""
|
|
574
589
|
|
|
@@ -626,10 +641,16 @@ def _broker_failure_message(config: BrokerConfig, exc: BaseException) -> str:
|
|
|
626
641
|
"Run `uv run buildai doctor auth` for the exact IAM/auth check."
|
|
627
642
|
)
|
|
628
643
|
elif "tls handshake timeout" in lower_detail or "context deadline exceeded" in lower_detail:
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
644
|
+
if config.use_private_ip and not config.all_proxy:
|
|
645
|
+
lines.append(
|
|
646
|
+
"Cause: this database lane is private-only. Start a VPN/IAP/SOCKS route "
|
|
647
|
+
"or pass `--all-proxy socks5://HOST:PORT`, then retry."
|
|
648
|
+
)
|
|
649
|
+
else:
|
|
650
|
+
lines.append(
|
|
651
|
+
"Cause: the proxy could not reach Google or AlloyDB before its deadline. "
|
|
652
|
+
"Check network/VPN connectivity, then retry."
|
|
653
|
+
)
|
|
633
654
|
else:
|
|
634
655
|
lines.append(
|
|
635
656
|
"Cause: the local proxy accepted TCP but closed the Postgres startup handshake."
|
|
@@ -699,7 +720,7 @@ def ensure_broker(config: BrokerConfig) -> BrokerState:
|
|
|
699
720
|
command,
|
|
700
721
|
stdout=log_file,
|
|
701
722
|
stderr=log_file,
|
|
702
|
-
env=
|
|
723
|
+
env=_proxy_environment(config),
|
|
703
724
|
**_proxy_popen_kwargs(),
|
|
704
725
|
)
|
|
705
726
|
|
|
@@ -790,6 +811,8 @@ def broker_status(config: BrokerConfig) -> dict[str, Any]:
|
|
|
790
811
|
"instance_uri": config.instance_uri,
|
|
791
812
|
"host": config.host,
|
|
792
813
|
"port": config.port,
|
|
814
|
+
"use_private_ip": config.use_private_ip,
|
|
815
|
+
"all_proxy": config.all_proxy,
|
|
793
816
|
"state_path": str(config.state_path),
|
|
794
817
|
"log_path": state.log_path if state else str(config.log_path),
|
|
795
818
|
"pid": pid,
|
|
@@ -831,6 +854,7 @@ def broker_config_for_identity(
|
|
|
831
854
|
host=settings.alloydb_auth_proxy_host,
|
|
832
855
|
port=_profile_port(settings, profile, db_user=db_user),
|
|
833
856
|
use_private_ip=settings.use_private_ip,
|
|
857
|
+
all_proxy=(os.getenv("ALLOYDB_AUTH_PROXY_ALL_PROXY") or os.getenv("ALL_PROXY") or None),
|
|
834
858
|
password=password,
|
|
835
859
|
)
|
|
836
860
|
|
|
@@ -289,6 +289,11 @@ def db_callback(
|
|
|
289
289
|
"-p",
|
|
290
290
|
help="Auth workflow profile.",
|
|
291
291
|
),
|
|
292
|
+
all_proxy: str | None = typer.Option(
|
|
293
|
+
None,
|
|
294
|
+
"--all-proxy",
|
|
295
|
+
help="Proxy URL passed to alloydb-auth-proxy for private-only DB lanes.",
|
|
296
|
+
),
|
|
292
297
|
write: bool = typer.Option(False, "--write", help="Allow write operations."),
|
|
293
298
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose logging."),
|
|
294
299
|
) -> None:
|
|
@@ -300,6 +305,7 @@ def db_callback(
|
|
|
300
305
|
ctx.obj["_env"] = env
|
|
301
306
|
ctx.obj["_auth"] = auth
|
|
302
307
|
ctx.obj["_user"] = user
|
|
308
|
+
ctx.obj["_all_proxy"] = all_proxy
|
|
303
309
|
ctx.obj["_verbose"] = verbose
|
|
304
310
|
ctx.obj["_ops_ready"] = False
|
|
305
311
|
|
|
@@ -60,6 +60,7 @@ def _apply_default_db_target(*, env_prefix: str) -> None:
|
|
|
60
60
|
os.environ.setdefault("ALLOYDB_CLUSTER", "buildai-india-staging")
|
|
61
61
|
os.environ.setdefault("ALLOYDB_INSTANCE", "buildai-india-staging-primary")
|
|
62
62
|
os.environ.setdefault("DB_NAME", "buildai_staging")
|
|
63
|
+
os.environ.setdefault("USE_PRIVATE_IP", "true")
|
|
63
64
|
else:
|
|
64
65
|
os.environ.setdefault("ALLOYDB_CLUSTER", "buildai-india")
|
|
65
66
|
os.environ.setdefault("ALLOYDB_INSTANCE", "buildai-india-primary")
|
|
@@ -104,8 +105,12 @@ def init_ops_context(ctx: typer.Context):
|
|
|
104
105
|
env_flag = ctx.obj.get("_env")
|
|
105
106
|
auth_flag = ctx.obj.get("_auth")
|
|
106
107
|
user_flag = ctx.obj.get("_user")
|
|
108
|
+
all_proxy_flag = ctx.obj.get("_all_proxy")
|
|
107
109
|
profile = ctx.obj.get("cli_profile") or os.getenv("BUILDAI_CLI_PROFILE") or "engineers-dev"
|
|
108
110
|
|
|
111
|
+
if all_proxy_flag:
|
|
112
|
+
os.environ["ALLOYDB_AUTH_PROXY_ALL_PROXY"] = str(all_proxy_flag)
|
|
113
|
+
|
|
109
114
|
# --- resolve environment ------------------------------------------------
|
|
110
115
|
if env_flag is not None:
|
|
111
116
|
try:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|