Skip to content

Commit db7dc2a

Browse files
committed
cache external postgres port in Branch model
1 parent 5fc2dcd commit db7dc2a

3 files changed

Lines changed: 140 additions & 1 deletion

File tree

src/api/organization/project/branch/__init__.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,20 @@ async def _persist_branch_status(branch_id: Identifier, status: BranchServiceSta
158158
await session.commit()
159159

160160

161+
async def _persist_branch_db_port(branch_id: Identifier) -> None:
162+
port = await _resolve_branch_db_port(branch_id)
163+
if port is None:
164+
logger.warning("Could not resolve db port for branch %s", branch_id)
165+
return
166+
async with AsyncSessionLocal() as session:
167+
branch = await session.get(Branch, branch_id)
168+
if branch is None:
169+
logger.warning("Branch %s missing while persisting db_port", branch_id)
170+
return
171+
branch.db_port = port
172+
await session.commit()
173+
174+
161175
async def _cleanup_failed_branch_deployment(branch_id: Identifier) -> None:
162176
"""Best-effort cleanup when provisioning fails so allocations aren't stranded."""
163177
try:
@@ -715,6 +729,7 @@ async def _deploy_branch_environment_task(
715729
branch_slug,
716730
)
717731
return
732+
await _persist_branch_db_port(branch_id)
718733
await _persist_branch_status(branch_id, BranchServiceStatus.STARTING)
719734

720735

@@ -788,6 +803,7 @@ async def _clone_branch_environment_task(
788803
branch_slug,
789804
)
790805
return
806+
await _persist_branch_db_port(branch_id)
791807
await _persist_branch_status(branch_id, BranchServiceStatus.STARTING)
792808

793809

@@ -868,6 +884,7 @@ async def _restore_branch_environment_task(
868884
branch_slug,
869885
)
870886
return
887+
await _persist_branch_db_port(branch_id)
871888
await _persist_branch_status(branch_id, BranchServiceStatus.STARTING)
872889

873890

@@ -1009,7 +1026,7 @@ async def _public(branch: Branch) -> BranchPublic:
10091026
project = await branch.awaitable_attrs.project
10101027

10111028
db_host = _resolve_db_host(branch) or ""
1012-
port = (await _resolve_branch_db_port(branch.id)) or 0
1029+
port = branch.db_port or (await _resolve_branch_db_port(branch.id)) or 0
10131030

10141031
# pg-meta and pg are in the same network. So password is not required in connection string.
10151032
connection_string = _build_connection_string("vela", "postgres", 5432)

src/models/branch.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from enum import StrEnum
44
from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Literal, Optional
55

6+
import sqlalchemy as sa
67
from pydantic import BaseModel, model_validator
78
from pydantic import Field as PydanticField
89
from sqlalchemy import BigInteger, Boolean, Column, String, Text, UniqueConstraint, text
@@ -108,6 +109,7 @@ class Branch(AsyncAttrs, Model, table=True):
108109
)
109110
pitr_enabled: bool = Field(default=False, sa_column=Column(Boolean, nullable=False, server_default=text("false")))
110111
resize_task_id: uuid.UUID | None = Field(default=None, nullable=True)
112+
db_port: int | None = Field(default=None, sa_column=Column(sa.Integer, nullable=True))
111113

112114
__table_args__ = (UniqueConstraint("project_id", "name", name="unique_branch_name_per_project"),)
113115

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""Add and backfill branch.db_port
2+
3+
Revision ID: 2b4e8f1a6c03
4+
Revises: f4f677e4e9b9
5+
Create Date: 2026-03-19 00:00:00.000000
6+
7+
"""
8+
import logging
9+
import os
10+
from typing import Sequence, Union
11+
12+
from alembic import op
13+
import sqlalchemy as sa
14+
from kubernetes import client as kubernetes_client
15+
from kubernetes import config as kubernetes_config
16+
from kubernetes.config.config_exception import ConfigException
17+
18+
logger = logging.getLogger(__name__)
19+
20+
# revision identifiers, used by Alembic.
21+
revision: str = '2b4e8f1a6c03'
22+
down_revision: Union[str, Sequence[str], None] = 'a1b2c3d4e5f6'
23+
branch_labels: Union[str, Sequence[str], None] = None
24+
depends_on: Union[str, Sequence[str], None] = None
25+
26+
_CHART_NAME = "vela"
27+
28+
29+
def _ensure_kube_client_config() -> None:
30+
try:
31+
kubernetes_config.load_incluster_config()
32+
except ConfigException:
33+
try:
34+
kubernetes_config.load_kube_config()
35+
except ConfigException as exc:
36+
raise RuntimeError("Kubernetes client not configured. Mount kubeconfig or run in-cluster.") from exc
37+
38+
39+
def _deployment_namespace(branch_id: str) -> str:
40+
prefix = os.environ.get("VELA_DEPLOYMENT_NAMESPACE_PREFIX", "vela")
41+
return f"{prefix}-{branch_id.lower()}" if prefix else branch_id.lower()
42+
43+
44+
def _release_fullname() -> str:
45+
release = os.environ.get("VELA_DEPLOYMENT_RELEASE_NAME", "vela")
46+
return release if _CHART_NAME in release else f"{release}-{_CHART_NAME}"
47+
48+
49+
def _branch_service_name(component: str) -> str:
50+
return f"{_release_fullname()}-{component}"
51+
52+
53+
def _get_all_node_ports() -> dict[str, int]:
54+
"""Fetch node ports for all pgbouncer/db services in a single API call.
55+
56+
Returns a mapping of namespace -> nodePort, preferring the pgbouncer service
57+
over the db service when both exist in the same namespace.
58+
"""
59+
core_v1 = kubernetes_client.CoreV1Api()
60+
target_names = {_branch_service_name("pgbouncer"), _branch_service_name("db")}
61+
62+
svc_list = core_v1.list_service_for_all_namespaces()
63+
64+
ports_by_namespace: dict[str, int] = {}
65+
for svc in svc_list.items:
66+
if svc.metadata.name not in target_names:
67+
continue
68+
ns = svc.metadata.namespace
69+
if ns in ports_by_namespace:
70+
continue # already resolved for this namespace
71+
ports = svc.spec.ports
72+
if ports and ports[0].node_port:
73+
ports_by_namespace[ns] = ports[0].node_port
74+
75+
return ports_by_namespace
76+
77+
78+
def upgrade() -> None:
79+
"""Upgrade schema."""
80+
op.add_column(
81+
'branch',
82+
sa.Column('db_port', sa.Integer(), nullable=True),
83+
)
84+
85+
bind = op.get_bind()
86+
branch_rows = bind.execute(
87+
sa.text("SELECT id::text AS id FROM branch")
88+
).mappings().all()
89+
90+
if not branch_rows:
91+
return
92+
93+
_ensure_kube_client_config()
94+
ports_by_namespace = _get_all_node_ports()
95+
96+
updates = []
97+
for row in branch_rows:
98+
namespace = _deployment_namespace(row["id"])
99+
port = ports_by_namespace.get(namespace)
100+
if port is None:
101+
logger.warning("Could not resolve db_port for branch %s — skipping", row["id"])
102+
continue
103+
updates.append({"id": row["id"], "db_port": port})
104+
105+
if updates:
106+
bind.execute(
107+
sa.text(
108+
"""
109+
UPDATE branch
110+
SET db_port = :db_port
111+
WHERE id = CAST(:id AS uuid)
112+
"""
113+
),
114+
updates,
115+
)
116+
117+
118+
def downgrade() -> None:
119+
"""Downgrade schema."""
120+
op.drop_column('branch', 'db_port')

0 commit comments

Comments
 (0)