Skip to content

Commit 76fb871

Browse files
committed
cache external postgres port in Branch model
1 parent 91d3c89 commit 76fb871

3 files changed

Lines changed: 129 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
@@ -169,6 +169,20 @@ async def _persist_branch_status(branch_id: Identifier, status: BranchServiceSta
169169
await session.commit()
170170

171171

172+
async def _persist_branch_db_port(branch_id: Identifier) -> None:
173+
port = await _resolve_branch_db_port(branch_id)
174+
if port is None:
175+
logger.warning("Could not resolve db port for branch %s", branch_id)
176+
return
177+
async with AsyncSessionLocal() as session:
178+
branch = await session.get(Branch, branch_id)
179+
if branch is None:
180+
logger.warning("Branch %s missing while persisting db_port", branch_id)
181+
return
182+
branch.db_port = port
183+
await session.commit()
184+
185+
172186
async def _cleanup_failed_branch_deployment(branch_id: Identifier) -> None:
173187
"""Best-effort cleanup when provisioning fails so allocations aren't stranded."""
174188
try:
@@ -846,6 +860,7 @@ async def _deploy_branch_environment_task(
846860
branch_slug,
847861
)
848862
return
863+
await _persist_branch_db_port(branch_id)
849864
await _persist_branch_status(branch_id, BranchServiceStatus.STARTING)
850865

851866

@@ -917,6 +932,7 @@ async def _clone_branch_environment_task(
917932
branch_slug,
918933
)
919934
return
935+
await _persist_branch_db_port(branch_id)
920936
await _persist_branch_status(branch_id, BranchServiceStatus.STARTING)
921937

922938

@@ -995,6 +1011,7 @@ async def _restore_branch_environment_task(
9951011
branch_slug,
9961012
)
9971013
return
1014+
await _persist_branch_db_port(branch_id)
9981015
await _persist_branch_status(branch_id, BranchServiceStatus.STARTING)
9991016

10001017

@@ -1136,7 +1153,7 @@ async def _public(branch: Branch) -> BranchPublic:
11361153
project = await branch.awaitable_attrs.project
11371154

11381155
db_host = _resolve_db_host(branch) or ""
1139-
port = (await _resolve_branch_db_port(branch.id)) or 0
1156+
port = branch.db_port or (await _resolve_branch_db_port(branch.id)) or 0
11401157

11411158
# pg-meta and pg are in the same network. So password is not required in connection string.
11421159
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, ConfigDict, ValidationError, model_validator
78
from pydantic import Field as PydanticField
89
from sqlalchemy import BigInteger, Boolean, Column, String, Text, UniqueConstraint, text
@@ -115,6 +116,7 @@ class Branch(AsyncAttrs, Model, table=True):
115116
sa_column=Column(JSONB, nullable=False, server_default=text("'{}'::jsonb")),
116117
)
117118
pitr_enabled: bool = Field(default=False, sa_column=Column(Boolean, nullable=False, server_default=text("false")))
119+
db_port: int | None = Field(default=None, sa_column=Column(sa.Integer, nullable=True))
118120

119121
__table_args__ = (UniqueConstraint("project_id", "name", name="unique_branch_name_per_project"),)
120122

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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] = 'f4f677e4e9b9'
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_node_port(branch_id: str) -> int | None:
54+
core_v1 = kubernetes_client.CoreV1Api()
55+
namespace = _deployment_namespace(branch_id)
56+
for service_name in (_branch_service_name("pgbouncer"), _branch_service_name("db")):
57+
try:
58+
svc = core_v1.read_namespaced_service(name=service_name, namespace=namespace)
59+
ports = svc.spec.ports
60+
if ports and ports[0].node_port:
61+
return ports[0].node_port
62+
except kubernetes_client.exceptions.ApiException as exc:
63+
if exc.status == 404:
64+
continue
65+
raise
66+
return None
67+
68+
69+
def upgrade() -> None:
70+
"""Upgrade schema."""
71+
op.add_column(
72+
'branch',
73+
sa.Column('db_port', sa.Integer(), nullable=True),
74+
)
75+
76+
bind = op.get_bind()
77+
branch_rows = bind.execute(
78+
sa.text("SELECT id::text AS id FROM branch")
79+
).mappings().all()
80+
81+
if not branch_rows:
82+
return
83+
84+
_ensure_kube_client_config()
85+
86+
updates = []
87+
for row in branch_rows:
88+
port = _get_node_port(row["id"])
89+
if port is None:
90+
logger.warning("Could not resolve db_port for branch %s — skipping", row["id"])
91+
continue
92+
updates.append({"id": row["id"], "db_port": port})
93+
94+
if updates:
95+
bind.execute(
96+
sa.text(
97+
"""
98+
UPDATE branch
99+
SET db_port = :db_port
100+
WHERE id = CAST(:id AS uuid)
101+
"""
102+
),
103+
updates,
104+
)
105+
106+
107+
def downgrade() -> None:
108+
"""Downgrade schema."""
109+
op.drop_column('branch', 'db_port')

0 commit comments

Comments
 (0)