Files
devplacepy/devplacepy/schemas/containers.py
T
retoor 6514261730 Route container proxies through the leg that is actually reachable
The workspace editor hung for 60s and then 504'd. Three independent faults were
stacked behind that one symptom.

Reachability: editor_target delegated to proxy_target, which returns
CONTAINER_PROXY_HOST plus the published host port and never falls back to the
container. From inside the app container that address crosses docker0 into the
host INPUT chain, whose policy is DROP with an allow-list that does not include
the published port range, so the packet was dropped and the request hung rather
than being refused. Measured from the app container: container_ip:8443 answers
302, gateway:20006 is dropped. One shared reachable_target now prefers the
direct container leg and falls back to the published port, and editor_target
uses tunnel_target as services/containers/CLAUDE.md already required. The same
defect affected /p/{slug} ingress and every tunnel, since all three resolved
through proxy_target.

The recorded measurement that motivated the old order (container_ip times out,
gateway connects) no longer holds: make docker-attach puts the app on the
instances' bridge network, which is what makes the direct leg work.

Duplicate response headers: the forwarding core relayed the upstream Date and
Server alongside the ones the serving layer generates, so every proxied
response carried two of each. Both are singleton headers and duplicating them
is malformed HTTP.

Serialization: WorkspaceViewOut declared flag_reason and three sibling strings
as str, so a NULL column made the workspace page 500 for JSON clients.

Documents the two public hostnames and the devplace.net SSH tunnel, so a future
session does not conclude the site is down after pointing curl --resolve at an
address the hostname does not resolve to, and adds the layered procedure for
diagnosing a production failure.

Verified on production with Playwright over both hostnames: the code-server
login renders and the workbench loads. Suite: 3345 passed.
2026-08-13 12:59:53 +02:00

198 lines
4.8 KiB
Python

# retoor <retoor@molodetz.nl>
from __future__ import annotations
from typing import Any, Optional
from devplacepy.schemas.base import _Out
class InstanceOut(_Out):
uid: str = ""
project_uid: str = ""
name: str = ""
slug: str = ""
container_id: Optional[str] = None
status: Optional[str] = None
desired_state: Optional[str] = None
restart_policy: Optional[str] = None
restart_count: int = 0
ingress_slug: Optional[str] = None
ingress_port: int = 0
boot_command: Optional[str] = None
boot_language: Optional[str] = None
boot_script: Optional[str] = None
run_as_uid: Optional[str] = None
start_on_boot: int = 0
cpu_limit: Optional[str] = None
mem_limit: Optional[str] = None
created_at: Optional[str] = None
started_at: Optional[str] = None
stopped_at: Optional[str] = None
class ScheduleOut(_Out):
uid: str = ""
instance_uid: str = ""
action: Optional[str] = None
enabled: int = 0
next_run_at: Optional[str] = None
last_run_at: Optional[str] = None
run_count: int = 0
class ContainersOut(_Out):
project: Optional[dict] = None
instances: list[InstanceOut] = []
class AdminContainersOut(_Out):
instances: list = []
projects: list = []
boot_languages: list = []
restart_policies: list = []
admin_section: Optional[str] = None
user: Optional[Any] = None
class AdminContainerEditOut(_Out):
instance: Optional[Any] = None
project: Optional[dict] = None
run_as_user: Optional[dict] = None
boot_languages: list = []
restart_policies: list = []
admin_section: Optional[str] = None
user: Optional[Any] = None
class AdminContainerInstanceOut(_Out):
instance: Optional[Any] = None
project: Optional[dict] = None
project_slug: Optional[str] = None
events: list = []
schedules: list = []
stats: Optional[Any] = None
runtime: Optional[Any] = None
can_manage: bool = False
admin_section: Optional[str] = None
user: Optional[Any] = None
class BotFrameOut(_Out):
slot: int = 0
username: str = ""
persona: str = ""
action: str = ""
status: str = ""
url: str = ""
label: str = ""
captured_at: int = 0
age_seconds: int = 0
has_image: bool = False
active: bool = False
frame_url: str = ""
class AdminBotsOut(_Out):
frames: list[BotFrameOut] = []
enabled: bool = False
service_status: str = ""
admin_section: Optional[str] = None
user: Optional[Any] = None
class TunnelOut(_Out):
uid: str = ""
instance_uid: str = ""
project_uid: str = ""
user_uid: str = ""
hostname: str = ""
label: str = ""
container_port: int = 0
desired_state: str = ""
status: str = ""
cert_status: str = ""
request_count: int = 0
bytes_out: int = 0
last_request_at: str = ""
last_error: str = ""
created_at: str = ""
class WorkspaceFlagOut(_Out):
uid: str = ""
instance_uid: str = ""
user_uid: str = ""
kind: str = ""
severity: str = ""
detail: str = ""
metric_value: float = 0.0
threshold: float = 0.0
status: str = ""
created_at: str = ""
class EditorProfileOut(_Out):
trust_all: bool = True
theme: str = ""
font_size: int = 0
terminal_font_size: int = 0
zoom_level: int = 0
layout: str = ""
panel_preset: str = ""
boot_agent: str = ""
boot_shell: bool = True
window_mode: str = ""
window_width: int = 0
window_height: int = 0
cpu_millicores: int = 0
cpu_cores: float = 0.0
memory_mb: int = 0
disk_quota_mb: int = 0
sources: dict = {}
class WorkspaceViewOut(_Out):
uid: str = ""
name: str = ""
status: str = ""
desired_state: str = ""
suspended: bool = False
flag_reason: Optional[str] = ""
tunnel_name: Optional[str] = ""
primary_url: Optional[str] = ""
last_active_at: Optional[str] = ""
disk_bytes: int = 0
disk_quota_mb: int = 0
disk_percent: int = 0
egress_bytes: int = 0
egress_quota_mb: int = 0
egress_percent: int = 0
idle_stop_minutes: int = 0
retention_days: int = 0
max_tunnels: int = 0
tunnels: list[TunnelOut] = []
flags: list[WorkspaceFlagOut] = []
editor: Optional[EditorProfileOut] = None
class WorkspaceOut(_Out):
project: Optional[Any] = None
workspace: Optional[WorkspaceViewOut] = None
has_workspace: bool = False
viewer_can_workspace: bool = False
workspace_count: int = 0
max_workspaces: int = 0
editor_url: str = ""
editor_password: str = ""
editor: Optional[EditorProfileOut] = None
restart_required: bool = False
user: Optional[Any] = None
class AdminWorkspacesOut(_Out):
workspaces: list[WorkspaceViewOut] = []
flags: list[WorkspaceFlagOut] = []
admin_section: Optional[str] = None
user: Optional[Any] = None