Checkpoint: Cloudflare tunnel + public toggle, and static-runner frontends

Two features built this session, committed together as a checkpoint before the
deployment-model refactor (they share files, so kept as one rollback point):

- Public exposure via Cloudflare tunnel: a `public` toggle on services (requires
  proxy), gateway.public_domain/tunnel_id config, a cloudflared ingress generator
  (generators/tunnel.py) that maps <name>.<public_domain> to the gateway's
  internal host, deploy wiring, and docs/tunnel-setup.md.
- Frontends as `static`-runner services: a RunStatic runner served by Caddy
  (file_server), no systemd unit (like `remote`); the Caddyfile generator now
  derives static routes from services instead of a behavior==frontend branch.
This commit is contained in:
2026-07-01 06:44:23 -07:00
parent 2cafcbb372
commit 5f6afbf007
10 changed files with 465 additions and 40 deletions

View File

@@ -76,8 +76,23 @@ class RunRemote(RunBase):
health_url: str | None = None
class RunStatic(RunBase):
"""A static site served by the gateway (Caddy ``file_server``), no process.
Like ``remote``, this is a service with no local process and no systemd unit —
the gateway *is* its runtime. ``root`` is the built directory to serve, resolved
relative to the referenced program's source (e.g. ``dist`` or ``public``).
Building that directory is the program's concern; this only serves it.
"""
runner: Literal["static"]
root: str = "dist" # served dir, relative to the program source
RunSpec = Annotated[
Union[RunCommand, RunPython, RunContainer, RunNode, RunCompose, RunRemote],
Union[
RunCommand, RunPython, RunContainer, RunNode, RunCompose, RunRemote, RunStatic
],
Field(discriminator="runner"),
]
@@ -255,6 +270,10 @@ class ServiceSpec(BaseModel):
# Expose this service at <service-name>.<gateway.domain> through the gateway
# (the subdomain is the service name). False → reachable only at its host:port.
proxy: bool = False
# Also expose this service to the public internet via the Cloudflare tunnel, at
# <service-name>.<gateway.public_domain>. Default False — public is opt-in and
# explicit. Requires proxy (the tunnel projects an already-routed subdomain).
public: bool = False
manage: ManageSpec | None = None
defaults: DefaultsSpec | None = None
@@ -263,6 +282,8 @@ class ServiceSpec(BaseModel):
if self.manage and self.manage.systemd and self.manage.systemd.enable:
if self.run.runner == "remote":
raise ValueError("manage.systemd cannot be enabled for runner=remote.")
if self.public and not self.proxy:
raise ValueError("public requires proxy (a service must be routed to be exposed).")
return self