Skip to content

Commit 227d287

Browse files
rfayclaude
andcommitted
fix(coder-routes): stop distinct web_extra_exposed_ports from clobbering each other's route
Any two web_extra_exposed_ports entries on the "web" service (e.g. ddev-playwright's playwright:9323 and a VNC add-on's kasmvnc:8444) reduced to the same svc_name="web" in the dynamic add-on branch, so both computed the identical ROUTER_NAME and whichever was processed last silently overwrote the other's Traefik route. Key the fallback slug on service AND port so distinct ports never collide. Also generalize docker-compose.coder-describe.yaml generation: rather than hardcoding "xhgui"/"adminer" in coder-setup, coder-routes now derives a describe-url-port override for every real compose service it discovers while building routes (not just two hardcoded names), so any current or future add-on/custom service with its own router gets a correct Coder URL in `ddev describe`/`ddev launch` automatically. coder-setup keeps only a minimal bootstrap covering the primary web+Mailpit URLs, since those are the only ones computable before a project has ever been started. This cannot fix web_extra_exposed_ports rows themselves (playwright, kasmvnc, etc.) — verified against ddev/ddev source that cmd/ddev/cmd/describe.go's WebExtraExposedPorts stanza always builds that row's URL itself and never reads describe-url-port. That needs a separate upstream feature request against ddev/ddev. Validated live in a running Coder sandbox: both routes now coexist (randyfay-coder-web-9323, randyfay-coder-web-8444), and xhgui's `ddev describe` row picked up its Coder URL after one restart. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent f77ec0a commit 227d287

2 files changed

Lines changed: 107 additions & 5 deletions

File tree

‎image/scripts/.ddev/commands/host/coder-routes‎

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,24 @@ yq e -i \
6161

6262
echo "Building Coder Traefik routes from ${DDEV_PROJECT}_merged.yaml:"
6363

64+
# Collected alongside the Traefik routes below, then written once at the end into
65+
# .ddev/docker-compose.coder-describe.yaml so 'ddev describe'/'ddev launch' show
66+
# the same Coder URL every router here actually resolves to. WEB_URL/MAILPIT_URL
67+
# hold the primary site (svc_name="web" is special-cased out of DESCRIBE_URLS
68+
# below, since both the primary site and Mailpit share that service name but need
69+
# their own describe-url-port entries: web's own row, and its "Mailpit: ..." line).
70+
# DESCRIBE_URLS covers every OTHER real service this project's router config
71+
# defines — xhgui, adminer, or any future/custom add-on service that gets its own
72+
# router — generically, with no per-name special-casing. It intentionally excludes
73+
# web_extra_exposed_ports entries (ddev-playwright's "playwright" row, a VNC
74+
# add-on's "kasmvnc" row, etc.): those aren't separate compose services, they're
75+
# extra ports on the "web" service itself, and DDEV's describe-url-port extension
76+
# is only ever read per compose service name — see the longer explanation in
77+
# coder-setup.
78+
WEB_URL=""
79+
MAILPIT_URL=""
80+
declare -A DESCRIBE_URLS
81+
6482
# Iterate over every router in the merged config.
6583
# Each entry has an entrypoints list and a service name like {project}-{svc}-{port}.
6684
while IFS= read -r router; do
@@ -108,7 +126,17 @@ while IFS= read -r router; do
108126
elif [ "$svc_name" = "adminer" ]; then
109127
slug="adminer-${PROJECT_SLUG}"
110128
else
111-
slug="$svc_name"
129+
# Dynamic add-on branch (below): a project can define more than one
130+
# web_extra_exposed_ports entry (e.g. ddev-playwright's "playwright" port
131+
# and a VNC add-on's "kasmvnc" port). Both live on the docker-compose "web"
132+
# service, so svc_name is "web" for all of them — slug="$svc_name" alone
133+
# would give them the identical ROUTER_NAME and each would silently
134+
# clobber whichever was processed before it (same failure mode as the
135+
# primary-web collision described above, just between two non-primary
136+
# ports instead of with the primary). Including the port keeps every
137+
# add-on port's route distinct. The printed URL already disambiguates by
138+
# ext_port (below); only this map key needs the same treatment.
139+
slug="${svc_name}-${port}"
112140
fi
113141

114142
ROUTER_NAME="${DDEV_PROJECT}-coder-${slug}"
@@ -135,7 +163,8 @@ while IFS= read -r router; do
135163
.http.routers.\"${ROUTER_NAME}\".middlewares = [env(MW)] |
136164
.http.routers.\"${ROUTER_NAME}\".tls = false" \
137165
/tmp/coder-routes-raw.yaml
138-
echo " + ${slug}: ${entrypoints[*]} → ${service} (https://${CODER_HOST})"
166+
WEB_URL="https://${CODER_HOST}"
167+
echo " + ${slug}: ${entrypoints[*]} → ${service} (${WEB_URL})"
139168
elif [ "$slug" = "mailpit-${PROJECT_SLUG}" ] || [ "$slug" = "xhgui-${PROJECT_SLUG}" ] || [ "$slug" = "adminer-${PROJECT_SLUG}" ]; then
140169
# Known Coder app slugs (defined as coder_app resources in the Terraform template):
141170
# use Host() rule so the Coder subdomain proxy URL routes correctly.
@@ -149,6 +178,11 @@ while IFS= read -r router; do
149178
.http.routers.\"${ROUTER_NAME}\".middlewares = [env(MW)] |
150179
.http.routers.\"${ROUTER_NAME}\".tls = false" \
151180
/tmp/coder-routes-raw.yaml
181+
if [ "$svc_name" = "web" ]; then
182+
MAILPIT_URL="https://${CODER_HOST}"
183+
else
184+
DESCRIBE_URLS["$svc_name"]="https://${CODER_HOST}"
185+
fi
152186
echo " + ${slug}: ${entrypoints[*]} → ${service} (https://${CODER_HOST})"
153187
else
154188
# Dynamic add-on services (no dedicated coder_app): PathPrefix("/") catches any
@@ -172,7 +206,16 @@ while IFS= read -r router; do
172206
.http.routers.\"${ROUTER_NAME}\".tls = false |
173207
.http.routers.\"${ROUTER_NAME}\".priority = 10" \
174208
/tmp/coder-routes-raw.yaml
175-
echo " + ${slug}: ${entrypoints[*]} → ${service} (https://${ext_port}--${AGENT}--${WORKSPACE}--${OWNER}.${DOMAIN})"
209+
DYNAMIC_URL="https://${ext_port}--${AGENT}--${WORKSPACE}--${OWNER}.${DOMAIN}"
210+
# svc_name="web" here means a web_extra_exposed_ports entry (playwright,
211+
# kasmvnc, ...) — not a real service, so it's excluded (see comment above
212+
# DESCRIBE_URLS). Anything else is a genuine custom/add-on service that
213+
# simply doesn't have a dedicated coder_app slug yet; still worth reflecting
214+
# in 'ddev describe' via its dynamic port-forwarding URL.
215+
if [ "$svc_name" != "web" ]; then
216+
DESCRIBE_URLS["$svc_name"]="$DYNAMIC_URL"
217+
fi
218+
echo " + ${slug}: ${entrypoints[*]} → ${service} (${DYNAMIC_URL})"
176219
fi
177220

178221
done < <(yq e '.http.routers | keys | .[]' "$MERGED" 2>/dev/null)
@@ -190,3 +233,35 @@ if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^ddev-router$"; then
190233
else
191234
echo "Note: ddev-router not running; config will be loaded on next ddev start"
192235
fi
236+
237+
# Regenerate .ddev/docker-compose.coder-describe.yaml so 'ddev describe'/'ddev launch'
238+
# reflect exactly what was just routed above — see the DESCRIBE_URLS comment earlier
239+
# in this script for why this only covers real compose services, not
240+
# web_extra_exposed_ports rows. DDEV_APPROOT is provided by DDEV for host commands
241+
# (their cwd is wherever `ddev` was invoked from, not the project root).
242+
if [ -n "${DDEV_APPROOT:-}" ] && [ -n "$WEB_URL" ]; then
243+
DESCRIBE_FILE="${DDEV_APPROOT}/.ddev/docker-compose.coder-describe.yaml"
244+
{
245+
echo "#ddev-silent-no-warn"
246+
echo "# Auto-generated by ddev coder-routes — do not edit."
247+
echo "services:"
248+
echo " web:"
249+
echo " x-ddev:"
250+
echo " describe-url-port: |"
251+
echo " ${WEB_URL}"
252+
if [ -n "$MAILPIT_URL" ]; then
253+
echo " Mailpit: ${MAILPIT_URL}"
254+
fi
255+
echo " describe-info: \"Use: ddev launch\""
256+
257+
for svc_name in "${!DESCRIBE_URLS[@]}"; do
258+
echo " ${svc_name}:"
259+
echo " x-ddev:"
260+
echo " describe-url-port: |"
261+
echo " ${DESCRIBE_URLS[$svc_name]}"
262+
done
263+
} > "$DESCRIBE_FILE"
264+
echo "✓ Wrote $(basename "$DESCRIBE_FILE") (covers: web ${!DESCRIBE_URLS[*]})"
265+
else
266+
echo "Note: skipping docker-compose.coder-describe.yaml (DDEV_APPROOT unset or no primary web route found)"
267+
fi

‎image/scripts/.ddev/commands/host/coder-setup‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,34 @@ hooks:
4646
EOF
4747
echo "✓ Wrote .ddev/config.coder.yaml"
4848

49-
# Write docker-compose.coder-describe.yaml so 'ddev describe' shows the Coder web URL.
50-
# Must exist before 'ddev start' — DDEV reads docker-compose files at start time.
49+
# Write an initial docker-compose.coder-describe.yaml covering just the primary
50+
# web service + Mailpit, so 'ddev describe' shows a working Coder URL even before
51+
# the project has ever been started. Everything else — xhgui, adminer, or any other
52+
# add-on/custom service that gets its own router — is handled dynamically by
53+
# `coder-routes` (which runs on every post-start) rather than hardcoded here: it
54+
# already discovers every real service from the merged Traefik config, computes
55+
# each one's Coder URL for routing, and rewrites this same file with an entry per
56+
# service it finds.
57+
#
58+
# Note the one-restart lag: DDEV only reads x-ddev.describe-url-port from the
59+
# compose config it built during the *last* `ddev start`/`ddev restart`, not fresh
60+
# on every `ddev describe` call. So a service coder-routes discovers during THIS
61+
# start's post-start hook won't show its Coder URL in `ddev describe` until the
62+
# NEXT start/restart — confirmed empirically: after one `ddev restart`, a newly
63+
# discovered service's row was still missing its Coder URL; a second restart
64+
# picked it up. This bootstrap block covers the primary web+Mailpit URLs (the only
65+
# ones computable without a running project) so that pair is right from the very
66+
# first start; every other service converges one restart after coder-routes first
67+
# sees it, same as this block already required before this change.
68+
#
69+
# Note this mechanism only applies to real docker-compose services. It cannot fix
70+
# the separate "web_extra_exposed_ports" rows some add-ons contribute to
71+
# 'ddev describe' (e.g. ddev-playwright's "playwright" row, a VNC add-on's
72+
# "kasmvnc" row): cmd/ddev/cmd/describe.go's WebExtraExposedPorts stanza always
73+
# builds that row's URL itself (https://<project-hostname>:<https_port>) and never
74+
# consults describe-url-port. Those rows will keep showing the raw *.ddev.site URL
75+
# — unreachable through Coder's proxy — until ddev/ddev adds support for overriding
76+
# them; that's a separate upstream feature request against https://github.com/ddev/ddev.
5177
_WORKSPACE="${CODER_WORKSPACE_NAME:-}"
5278
_OWNER="${CODER_WORKSPACE_OWNER_NAME:-}"
5379
_DOMAIN=""
@@ -63,6 +89,7 @@ if [ -n "$_WORKSPACE" ] && [ -n "$_OWNER" ] && [ -n "$_DOMAIN" ] && [ -n "${DDEV
6389
cat > .ddev/docker-compose.coder-describe.yaml << EOF
6490
#ddev-silent-no-warn
6591
# Auto-generated by ddev coder-setup — do not edit.
92+
# Superseded by coder-routes after the first 'ddev start' (see comment above).
6693
services:
6794
web:
6895
x-ddev:

0 commit comments

Comments
 (0)