Subdomain-only gateway routing; drop path prefixes

Collapse the two proxy shapes (path_prefix + host) to a single checkbox: a
service is either port-only, or exposed at <service-name>.<gateway.domain>. Path
routes and their prefix-stripping foot-guns are gone; the subdomain is always the
service name (rename the service to change it).

- Schema: CaddySpec is just `enable` (presence = expose). service_proxy_targets
  returns (expose, port, base_url); Deployment carries `subdomain` (was
  proxy_path/proxy_host). Registry/deploy/mesh/CLI updated in lockstep.
- Generator: compute_routes emits one host route per exposed service/frontend
  (address = name). acme mode = one *.<domain> site with a reverse_proxy matcher
  per service + a file_server matcher per frontend; the :<port> site redirects to
  the dashboard. off mode = a HTTP control plane on :<port> (dashboard at / +
  /api → castle-api); other services are port-only.
- Dashboard/API: castle-app and castle-api are ordinary subdomains. The dashboard
  derives the API base at runtime (castle-api.<domain> on a subdomain, else /api)
  and calls it cross-origin — castle-api already allows CORS *. Frontends build
  with VITE_BASE=/ (served at their subdomain root).
- CLI: `service create` drops --path/--host; --no-proxy makes it port-only.
- Docs/tests reworked for the model; docs use generic placeholders only (no
  personal host/domain/IP details).
This commit is contained in:
2026-06-30 20:47:03 -07:00
parent a022a136fe
commit 43ef1cc588
23 changed files with 382 additions and 712 deletions

View File

@@ -267,10 +267,10 @@ def _build_deployed_service(
if svc.manage and svc.manage.systemd and not svc.manage.systemd.enable:
managed = False
# Routing fields (port/proxy_path/proxy_host/base_url) come from the shared
# deriver, so the registry written here and the Caddyfile computed from
# castle.yaml stay in lockstep.
proxy_path, proxy_host, port, base_url = service_proxy_targets(name, svc)
# Routing comes from the shared deriver, so the registry written here and the
# Caddyfile computed from castle.yaml stay in lockstep. `expose` is the
# checkbox; the subdomain is the service name.
expose, port, base_url = service_proxy_targets(name, svc)
health_path = None
if svc.expose and svc.expose.http:
health_path = svc.expose.http.health_path
@@ -301,27 +301,11 @@ def _build_deployed_service(
)
stop_cmd = _build_stop_cmd(name, run, source_dir)
# Proxy: a path prefix (handle_path on the gateway) and/or a hostname (a
# dedicated host site block, so a root-based app serves unchanged).
proxy_path = None
proxy_host = None
if svc.proxy and svc.proxy.caddy and svc.proxy.caddy.enable:
caddy = svc.proxy.caddy
proxy_host = caddy.host
if caddy.path_prefix:
proxy_path = caddy.path_prefix
elif not caddy.host:
# No explicit path and no host → default to /<name>.
proxy_path = f"/{name}"
# Resolve stack from referenced program
stack = None
if svc.program and svc.program in config.programs:
stack = config.programs[svc.program].stack
# Remote services proxy to an external base_url
base_url = getattr(run, "base_url", None)
return Deployment(
runner=run.runner,
run_cmd=run_cmd,
@@ -333,8 +317,7 @@ def _build_deployed_service(
stack=stack,
port=port,
health_path=health_path,
proxy_path=proxy_path,
proxy_host=proxy_host,
subdomain=(name if expose else None),
base_url=base_url,
managed=managed,
)
@@ -576,8 +559,8 @@ def _format_deployed(name: str, deployed: Deployment) -> str:
parts.append(f"port={deployed.port}")
if deployed.schedule:
parts.append(f"schedule={deployed.schedule}")
if deployed.proxy_path:
parts.append(f"proxy={deployed.proxy_path}")
if deployed.subdomain:
parts.append(f"subdomain={deployed.subdomain}")
return " ".join(parts)